zudo-css

Type to search...

to open search from anywhere

Two-Tier Size Strategy

CreatedMar 24, 2026UpdatedMar 26, 2026Takeshi Takatsudo

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:

TierNamePurposeExample
1ThemeDesign-level sizes that define the visual system--icon-sm: 16px
2ComponentOne-off sizes specific to one componentw-[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.

Wrong: Abstract Numeric Sizes (size-4, size-8, etc.)

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.

Right: Semantic Theme Tokens (icon-md, avatar-sm, etc.)

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.

Abstract Numbers vs Semantic Theme Tokens

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.

Arbitrary Values for Component-Specific Sizing

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: 32px etc. back into @theme defeats the tight token strategy; these are meaningless numbers
  • Creating abstract size tokens--size-sm, --size-md, --size-lg with values like 16px, 32px, 64px are too generic; what is a “small size”? An icon? An avatar? A button?
  • Using spacing tokens for element sizinghsp-sm is 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-4 which 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:

PropertyTiersWhy
Colors3 (palette → theme → component)Raw values have palette structure worth abstracting
Font sizes3 (scale → theme → component)Clear hierarchy from captions to headings
Spacing2 (semantic axes hsp/vsp)Consistent rhythm, but already semantic
Width/height2 (theme → component)No natural scale — semantic names only

See also:

References

Revision History