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 componentUsing 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 misleadingNot 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 systemToo 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 TailwindThree-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()