zudo-css

Type to search...

to open search from anywhere

Three-Tier Font-Size Strategy

CreatedMar 24, 2026UpdatedMar 26, 2026Takeshi Takatsudo

The Problem

When building a UI, it’s tempting to use font-size values directly — font-size: 1.25rem in one component, font-size: 20px in another, and font-size: 1.3rem in a third, all meaning “slightly large text.” This scatters raw values across the codebase with no central point of control.

A common first improvement is defining semantic tokens: --font-heading, --font-body, --font-caption. But this mixes two concerns into one layer — the raw size value and its semantic role. If headings should shrink from 28px to 24px, you change the token. But the nav links that also used --font-heading (because it happened to be the right size) shrink too, unintentionally.

The opposite approach — using only an abstract scale like text-lg — avoids the role-locking problem but loses semantic clarity. Developers see text-lg scattered across the codebase and have to figure out whether it’s a heading, a subtitle, or just emphasized text.

Neither approach alone is enough. What’s needed is a separation between how big (the raw value) and what for (the semantic role).

The Solution

Organize font sizes into three tiers, each with a clear purpose:

TierNamePurposeExample
1ScaleAbstract size values — the available steps--scale-lg1.25rem
2ThemeSemantic roles — what each size means--font-headingvar(--scale-xl)
3ComponentScoped overrides — sizes for one component--_card-titlevar(--font-subheading)

The key insight: each tier only references the tier above it. Components use theme tokens. Theme tokens point to scale values. Scale holds the actual rem/px values.

This is the same architecture as the Three-Tier Color Strategy (palette → theme → component), applied to font sizes.

Code Examples

Tier 1: The Scale

The scale is the raw material — every font size available in the system. These values are not used directly in components. Think of them as paint tubes: you have them ready, but you don’t squeeze them onto the canvas without a plan.

Tier 1 — Scale: Abstract Size Values

In a Tailwind project, Tier 1 lives in the @theme block:

@theme {
  --font-size-xs: 0.75rem;       /* 12px */
  --font-size-sm: 0.875rem;      /* 14px */
  --font-size-base: 1rem;        /* 16px */
  --font-size-lg: 1.25rem;       /* 20px */
  --font-size-xl: 1.75rem;       /* 28px */
  --font-size-2xl: 2.5rem;       /* 40px */
}

For detailed Tailwind @theme configuration, including paired line-heights and weight tokens, see Typography Token Patterns.

Tier 2: The Theme

Theme tokens give semantic meaning to scale values. Instead of “lg”, components see “subheading” or “heading”. This is the layer that makes typography adjustments painless — change --font-heading from --scale-xl to --scale-lg in one place, and every heading in the project updates.

Tier 2 — Theme: Semantic Mapping from Scale

In CSS, Tier 2 lives on :root (or any shared scope):

:root {
  --font-display: var(--scale-2xl);
  --font-heading: var(--scale-xl);
  --font-subheading: var(--scale-lg);
  --font-body: var(--scale-base);
  --font-secondary: var(--scale-sm);
  --font-caption: var(--scale-xs);
}

Tier 3: Component-Scoped Sizes

Sometimes a component needs font-size decisions that don’t fit into the global theme — a compact sidebar with smaller text, a pricing card with an oversized amount, or an admin panel with denser typography. These are Tier 3 variables: narrowly scoped, defined on the component itself, and referencing theme or scale tokens.

ℹ️ Underscore naming convention

Use a leading underscore (--_) for component-scoped custom properties to signal local scope:

.pricing-card {
  --_card-amount: var(--scale-2xl);
  --_card-period: var(--font-caption);
}

The --_ prefix tells readers “this variable is locally scoped to this component” — similar to how _privateMethod signals private scope in other languages. This is not a CSS rule — it is a project-level naming convention.

ℹ️ Tailwind + component-first exception

In Tailwind + component-first projects (React, Vue, Astro with utility classes), Tier 3 component-scoped CSS custom properties are rarely needed. The component framework itself provides scoping — there are no separate CSS files where these variables would be defined. Tier 3 is primarily relevant for general CSS approaches (BEM, CSS Modules, vanilla CSS).

Tier 3 — Component: Scoped Size Overrides

Notice how each component defines its own font-size variables but still references the theme or scale tiers. The pricing card uses --_card-amount: var(--scale-2xl) (scale) and --_card-desc: var(--font-secondary) (theme). The sidebar nav references theme tokens for all its sizes. If the global type scale changes, these components update automatically.

All Three Tiers Working Together

This demo shows a complete page layout with all three tiers visible in the CSS. Tier 1 defines the raw scale, Tier 2 maps it to semantic roles, and Tier 3 gives the stat cards their own local size variables.

All Three Tiers Working Together

The Power of Tier 2: Swapping Size Themes

The most powerful feature of this architecture: the same markup works with completely different size themes. Just remap Tier 2 — the semantic tokens — and the entire UI adjusts. This is how you implement compact mode, large/accessible mode, or density settings without touching any component CSS.

Same Markup, Three Size Themes

The markup is identical across all three columns. Only the Tier 2 mapping changes:

  • Default: heading → xl, body → base, caption → xs
  • Compact: heading → lg, body → sm, caption → xs (everything shifts down one step)
  • Large: heading → 2xl, body → lg, caption → sm (everything shifts up one step)

Tier 1 and Tier 3 stay exactly the same — only Tier 2 changes.

Complete CSS Code Structure

Here is how the three tiers fit together in a real project:

/* ── Tier 1: Scale ── */
/* In Tailwind, this goes in @theme */
:root {
  --scale-xs: 0.75rem;
  --scale-sm: 0.875rem;
  --scale-base: 1rem;
  --scale-lg: 1.25rem;
  --scale-xl: 1.75rem;
  --scale-2xl: 2.5rem;
}

/* ── Tier 2: Theme ── */
/* Semantic roles — change these to adjust the entire UI */
:root {
  --font-display: var(--scale-2xl);
  --font-heading: var(--scale-xl);
  --font-subheading: var(--scale-lg);
  --font-body: var(--scale-base);
  --font-secondary: var(--scale-sm);
  --font-caption: var(--scale-xs);
}

/* ── Tier 3: Component ── */
/* Scoped overrides — only when a component needs its own size logic */
.pricing-card {
  --_card-amount: var(--scale-2xl);
  --_card-label: var(--font-caption);
}

.sidebar-nav {
  --_nav-link: var(--font-secondary);
  --_nav-category: var(--font-caption);
}

Tailwind CSS Integration

In a Tailwind v4 project, the three tiers map naturally to existing patterns:

Tier 1 → Tailwind’s @theme block. This is where you define the constrained scale:

@theme {
  --font-size-xs: 0.75rem;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.25rem;
  --font-size-xl: 1.75rem;
  --font-size-2xl: 2.5rem;
}

This gives you text-xs through text-2xl utilities. For the full Tailwind configuration including paired line-heights and weight tokens, see Typography Token Patterns.

Tier 2 → CSS custom properties on :root. These are not Tailwind utilities — they are semantic tokens used in your CSS:

:root {
  --font-heading: var(--font-size-xl);
  --font-subheading: var(--font-size-lg);
  --font-body: var(--font-size-base);
  --font-secondary: var(--font-size-sm);
  --font-caption: var(--font-size-xs);
}

Components reference these in their CSS: font-size: var(--font-heading).

Tier 3 → Component-scoped CSS custom properties, same as the general pattern.

Common AI Mistakes

  • Skipping Tier 2 — using scale values directly in components (font-size: var(--scale-lg) or text-lg everywhere) means there’s no semantic layer; changing the heading size requires updating every component
  • Using semantic names at Tier 1 — defining --font-size-heading in @theme locks the scale to specific roles; when you need the same size for a non-heading, the token name becomes misleading
  • Not separating scale from theme — defining --font-heading: 1.75rem with a hardcoded value means you can’t adjust the entire scale proportionally; the heading size is disconnected from the rest of the type system
  • Too many Tier 3 variables — if a component defines 10+ local font-size variables, it’s likely reinventing the theme layer; promote those to Tier 2
  • Making Tier 1 too small — a scale with only 3 sizes forces components to invent their own raw values (Tier 3 variables with hardcoded rem values), breaking the system

When to Use

  • Any project with more than a few components — the overhead of three tiers pays off as soon as you need consistent typography
  • Multi-density or accessibility modes — Tier 2 makes compact/spacious/accessible switching trivial
  • Design system or component library — components should reference theme tokens, not raw scale values
  • Gradual adoption — you can start with Tier 1 + 2 and add Tier 3 as components need scoped overrides

When three tiers is overkill

  • Single-page sites with one type scale and no density variations
  • Quick prototypes where speed matters more than maintainability
  • Projects where only one developer touches the CSS

References

Revision History