Two-Tier Size Strategy
The Problem
When using the tight token strategy, all Tailwind defaults are reset — including the numeric spacing scale that powers h-4, w-4, size-8, and similar width/height utilities. The moment someone needs to size an icon or set a card width, these classes stop working.
The instinct — especially from AI agents — is to re-add numeric spacing tokens:
@theme {
/* "Just add back what we need" */
--spacing-3: 12px;
--spacing-4: 16px;
--spacing-5: 20px;
--spacing-8: 32px;
--spacing-10: 40px;
--spacing-16: 64px;
}
This defeats the purpose. It re-imports Tailwind’s default numeric scale with no semantic value. h-4 w-4 tells you nothing — is it an icon? A spacer? A decorative element? You are back to the same problem the tight token strategy was designed to solve.
Why This Property Is Different
Some CSS properties have natural scales worth abstracting:
- Spacing (padding, margin, gap) — has a consistent rhythm across the UI → semantic axes (hsp/vsp) make sense
- Font sizes — has a clear hierarchy from captions to headings → an abstract scale (xs–2xl) makes sense
- Colors — has a palette of raw values that get mapped to roles → three tiers make sense
Width/height is different. A 16px icon, a 40px avatar, a 320px card, and a 64px sidebar toggle have nothing in common. There is no natural progression, no rhythm, no hierarchy. An abstract scale like size-4, size-8, size-16 is just arbitrary numbers pretending to be meaningful.
The Solution
Use a two-tier approach — skip the abstract scale entirely:
| Tier | Name | Purpose | Example |
|---|---|---|---|
| 1 | Theme | Design-level sizes that define the visual system | --icon-sm: 16px |
| 2 | Component | One-off sizes specific to one component | w-[28px] h-[28px] |
The key insight: there is no Tier 0 (abstract scale) for width/height. Semantic names are the first and only token layer. Everything else is an arbitrary value.
Tier 1: Theme Tokens
Define tokens when a size represents a design decision — a deliberate choice about how the UI is structured:
@theme {
/* Icon sizes — a design system decision */
--spacing-icon-sm: 16px;
--spacing-icon-md: 20px;
--spacing-icon-lg: 24px;
/* Avatar sizes — a design system decision */
--spacing-avatar-sm: 32px;
--spacing-avatar-md: 40px;
--spacing-avatar-lg: 56px;
/* Layout sizes — an architectural decision */
--spacing-content-width: 800px;
--spacing-card-width: 300px;
}
This generates utilities like w-icon-md h-icon-md, w-avatar-sm h-avatar-sm, and max-w-content-width — self-documenting class names that tell you exactly what the size is for.
Whether to create a token is an architectural and design judgment, not a usage-count threshold. You define --spacing-content-width: 800px because your design says “the main column is 800px” — even if only one component uses it today. It’s the same kind of decision as choosing a brand color or a type scale: it comes from the design, not from counting how many files reference a value.
This is similar to the “should we extract this to a utility function?” debate in application architecture. The answer isn’t “when 3 files import it” — it’s about whether the concept deserves a name in the system.
Tier 2: Arbitrary Values
For everything else — one-off component dimensions, calculated layouts, structural details — use Tailwind’s bracket syntax:
<!-- Component-specific sizing -->
<button class="w-[28px] h-[28px] p-[6px]">...</button>
<!-- Grid structure -->
<div class="grid grid-cols-[240px_1fr]">...</div>
<!-- Calculated value -->
<div class="h-[calc(100vh-64px)]">...</div>
When a value is purely a structural detail of one component (a button’s exact padding, a grid’s column template), arbitrary values are the right choice. When a value represents a design decision that defines the system, it belongs in Tier 1 — regardless of how many components currently use it.
Demos
The Wrong Approach: Re-importing Numeric Sizes
This demo shows what happens when you re-add Tailwind’s numeric spacing scale for width/height. The class names are meaningless — size-4, size-5, size-8 tell you nothing about what they’re sizing.
The Right Approach: Theme Tokens for Shared Sizes
With semantic theme tokens, the class names are self-documenting. Every developer knows what w-icon-md h-icon-md means — and changing the icon size updates every component at once.
Side-by-Side: Abstract Numbers vs Semantic Names
The same UI elements sized two ways. On the left, abstract numeric tokens that could mean anything. On the right, semantic theme tokens that tell you exactly what they’re for.
Tier 2 in Practice: Arbitrary Values for One-Offs
Component-specific sizing uses Tailwind’s bracket syntax. These values stay in the component — they are not promoted to tokens because they have no meaning outside their context.
Tailwind CSS Integration
In a Tailwind v4 project:
Tier 1 → @theme block with --spacing-* prefix (so they work with w-* and h-* utilities):
@theme {
/* Element sizing tokens — only shared, semantic sizes */
--spacing-icon-sm: 16px;
--spacing-icon-md: 20px;
--spacing-icon-lg: 24px;
--spacing-avatar-sm: 32px;
--spacing-avatar-md: 40px;
--spacing-avatar-lg: 56px;
}
Usage: w-icon-md h-icon-md, w-avatar-sm h-avatar-sm.
Tier 2 → Tailwind’s bracket syntax for one-offs:
<div class="w-[28px] h-[28px]">...</div>
<div class="w-[calc(100%-2px)]">...</div>
<div class="grid grid-cols-[240px_1fr]">...</div>
Common AI Mistakes
- Re-adding the numeric spacing scale — importing
--spacing-4: 16px,--spacing-8: 32pxetc. back into@themedefeats the tight token strategy; these are meaningless numbers - Creating abstract size tokens —
--size-sm,--size-md,--size-lgwith values like 16px, 32px, 64px are too generic; what is a “small size”? An icon? An avatar? A button? - Using spacing tokens for element sizing —
hsp-smis for horizontal padding/margins, not for icon width; different concepts, different tokens - Adding a token for every unique dimension — not every pixel value deserves a name; a calculated offset like
top-[calc(100%-2px)]is a structural detail, not a design decision - Porting Tailwind defaults “just in case” — adding numeric spacing tokens without a design reason creates the unconstrained palette the tight strategy was designed to prevent; tokens should come from design decisions, not from anticipating future usage
When to Use
- Any project using the tight token strategy that needs to size elements (icons, avatars, cards, thumbnails)
- When AI agents build components — they consistently reach for
h-4 w-4which doesn’t exist in tight token projects; this article explains the correct approach - When the team debates adding numeric size tokens — this article provides the reasoning for why not
Tailwind + Component-First vs General CSS
The two-tier approach works the same way regardless of CSS methodology, but the mechanism for Tier 2 differs:
- Tailwind + component-first — Tier 2 values are Tailwind bracket syntax in JSX:
w-[28px],grid-cols-[240px_1fr]. The component file provides scoping. No CSS custom properties needed. - General CSS (BEM, CSS Modules) — Tier 2 values are component-scoped CSS custom properties:
--_button-width: 28px,--_grid-sidebar: 240px. The underscore prefix (--_) signals local scope.
Contrast with Other Token Strategies
Not every CSS property needs the same number of tiers:
| Property | Tiers | Why |
|---|---|---|
| Colors | 3 (palette → theme → component) | Raw values have palette structure worth abstracting |
| Font sizes | 3 (scale → theme → component) | Clear hierarchy from captions to headings |
| Spacing | 2 (semantic axes hsp/vsp) | Consistent rhythm, but already semantic |
| Width/height | 2 (theme → component) | No natural scale — semantic names only |
See also:
- Three-Tier Color Strategy — Full three-tier architecture for colors
- Three-Tier Font-Size Strategy — Three-tier architecture for font sizes
- Component Tokens & Arbitrary Values — General framework for when to use tokens vs arbitrary values