Three-Tier Font-Size Strategy
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:
| Tier | Name | Purpose | Example |
|---|---|---|---|
| 1 | Scale | Abstract size values — the available steps | --scale-lg → 1.25rem |
| 2 | Theme | Semantic roles — what each size means | --font-heading → var(--scale-xl) |
| 3 | Component | Scoped overrides — sizes for one component | --_card-title → var(--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.
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.
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).
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.
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.
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)ortext-lgeverywhere) means there’s no semantic layer; changing the heading size requires updating every component - Using semantic names at Tier 1 — defining
--font-size-headingin@themelocks 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.75remwith 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
Related Articles
- Typography Token Patterns — Practical
@themeconfiguration for Tier 1 in Tailwind - Three-Tier Color Strategy — The same three-tier architecture applied to colors
- Line Height Best Practices — Choosing line-heights to pair with your type scale
- Fluid Font Sizing — Making Tier 1 values responsive with
clamp()