Tight Token Strategy
The Problem
Tailwind CSS ships with an enormous default token set. The spacing scale alone includes values like 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96 — that is over 30 numeric steps for spacing. Multiply this by colors, font sizes, border radii, and other categories, and the available utility space becomes massive.
In practice, this means any developer on the team can reach for any value at any time. One person writes p-4, another uses p-5, and a third picks p-6 — all for “medium padding.” There is no wrong answer because every value is valid, but the result is an inconsistent, drifting UI. Design reviews turn into debates about which numeric step is “correct,” and refactoring spacing later requires auditing hundreds of utility classes scattered across the codebase.
The root cause is that Tailwind’s default tokens are generic numeric scales, not semantic design decisions. They tell you how much but not why.
The Solution
Replace all Tailwind defaults with a small, intentional set of semantic tokens. Tailwind CSS v4’s @theme directive makes this possible by allowing you to reset every built-in token with wildcard patterns and then define only the tokens your project actually needs.
The strategy has two key ideas:
-
Reset everything — Use
--spacing-*: initial;,--color-*: initial;, and similar wildcards to remove all default values. After this, utilities likep-4orbg-gray-500no longer exist. Attempting to use them causes a build error, which is exactly the point: invalid tokens are caught at build time, not in code review. -
Define semantic axes — Instead of a single numeric spacing scale, define separate scales for different purposes. A production approach is to split spacing into two axes:
- hsp (horizontal spacing): for inline gaps, horizontal padding, and horizontal margins
- vsp (vertical spacing): for vertical gaps between sections, vertical padding, and block-level margins
Each axis gets a limited scale from 2xs to 2xl, giving the team exactly 7 choices per axis. Combined with 0 and 1px utility values, this is the entire spacing vocabulary of the project.
Token Table
Horizontal spacing (hsp):
| Token | Value | Usage |
|---|---|---|
hsp-2xs | 5px | Tight inline spacing |
hsp-xs | 12px | Small padding |
hsp-sm | 20px | Default horizontal padding |
hsp-md | 40px | Medium sections |
hsp-lg | 60px | Large sections |
hsp-xl | 100px | Extra large spacing |
hsp-2xl | 250px | Hero / feature spacing |
Vertical spacing (vsp):
| Token | Value | Usage |
|---|---|---|
vsp-2xs | 4px | Minimal gaps |
vsp-xs | 8px | Tight component gaps |
vsp-sm | 20px | Default vertical gaps |
vsp-md | 35px | Section gaps |
vsp-lg | 50px | Large section gaps |
vsp-xl | 65px | Page section gaps |
vsp-2xl | 80px | Hero / major section gaps |
Code Examples
Two Approaches to Resetting Defaults
There are two ways to achieve the tight token strategy in Tailwind CSS v4. Both produce the same result — only your project tokens exist — but they differ in mechanism.
Approach A: Reset with --*: initial (explicit reset)
Import everything, then reset what you don’t want inside @theme:
@import "tailwindcss";
@theme {
/* Reset ALL Tailwind defaults */
--spacing-*: initial;
--color-*: initial;
--font-size-*: initial;
--font-family-*: initial;
--font-weight-*: initial;
--line-height-*: initial;
--letter-spacing-*: initial;
--border-radius-*: initial;
--shadow-*: initial;
--inset-shadow-*: initial;
--drop-shadow-*: initial;
--breakpoint-*: initial;
/* Then define your tokens... */
}
This works, but the generated CSS still contains some Tailwind internal plumbing variables that were part of the default theme layer.
Approach B: Skip the default theme (recommended)
Import only the layers you need — preflight (reset CSS) and utilities — skipping the default theme entirely:
@import "tailwindcss/preflight";
@import "tailwindcss/utilities";
@theme {
/* No resets needed — default theme was never loaded */
/* Define your tokens directly... */
}
@import "tailwindcss" is equivalent to importing three layers: tailwindcss/preflight (browser reset), tailwindcss/theme (all default tokens), and tailwindcss/utilities (utility class engine). By importing only preflight + utilities, the default theme is simply never loaded.
Approach B is recommended because:
- Fewer lines — no
--*: initialreset block needed - Slightly smaller CSS output (~1KB less) — Tailwind’s internal variables are not emitted
- Cleaner mental model — you start from zero instead of resetting to zero
What Approach B drops (vs Approach A)
When you skip tailwindcss/theme, the following Tailwind internals are no longer emitted in the generated CSS:
| Variable | Purpose | Do you need it? |
|---|---|---|
--default-transition-duration | Default duration for transition utilities | Add --default-transition-duration: 0.15s; in @theme if you use transition classes |
--default-transition-timing-function | Default easing for transition utilities | Add --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); in @theme if needed |
--default-font-family | Fallback for font-family on html | Not needed if you define --font-sans or set font-family yourself |
--default-mono-font-family | Fallback for code/pre font | Not needed if you define --font-mono |
--animate-spin | The animate-spin keyframe definition | Add manually if you use animate-spin |
--ease-in-out | Named easing curve | Add manually if you reference it |
--container-* | Container query widths | Not needed unless you use @container size utilities |
In practice, if you use transition-colors or similar transition utilities, add these two lines to your @theme:
@theme {
--default-transition-duration: 0.15s;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
/* ...your tokens... */
}
Full Example (Approach B)
Place this in your project’s main CSS file (e.g., app.css):
@import "tailwindcss/preflight";
@import "tailwindcss/utilities";
@theme {
/* ========================================
* Define ONLY project tokens — Spacing
* ======================================== */
--spacing-0: 0;
--spacing-1px: 1px;
/* Horizontal spacing */
--spacing-hsp-2xs: 5px;
--spacing-hsp-xs: 12px;
--spacing-hsp-sm: 20px;
--spacing-hsp-md: 40px;
--spacing-hsp-lg: 60px;
--spacing-hsp-xl: 100px;
--spacing-hsp-2xl: 250px;
/* Vertical spacing */
--spacing-vsp-2xs: 4px;
--spacing-vsp-xs: 8px;
--spacing-vsp-sm: 20px;
--spacing-vsp-md: 35px;
--spacing-vsp-lg: 50px;
--spacing-vsp-xl: 65px;
--spacing-vsp-2xl: 80px;
}
After this configuration:
p-4— build error (no--spacing-4token exists)bg-gray-500— build error (no--color-gray-500token exists)px-hsp-sm— works (resolves topadding-inline: 20px)py-vsp-md— works (resolves topadding-block: 35px)
Usage in Components
With the tight token set, Tailwind classes become self-documenting. You can read the intent directly from the class name:
<section class="px-hsp-sm py-vsp-lg">
<h1 class="pb-vsp-xs">Page Title</h1>
<p class="pb-vsp-sm">Introductory paragraph with standard vertical spacing below.</p>
<div class="flex gap-x-hsp-xs gap-y-vsp-xs">
<div class="px-hsp-xs py-vsp-2xs">Card A</div>
<div class="px-hsp-xs py-vsp-2xs">Card B</div>
</div>
</section>
Every spacing value communicates its axis (horizontal vs vertical) and its relative size within the scale.
Demo: Semantic Tokens vs Arbitrary Values
The following demo shows the same card layout built two ways. The left card uses the tight semantic token approach; the right card uses arbitrary numeric spacing values, simulating how inconsistency creeps in when any value is allowed.
In the “Semantic tokens” card, every section uses values from the token table: vsp-xs (8px) for header/footer vertical padding, hsp-sm (20px) for horizontal padding, vsp-sm (20px) for body vertical padding, hsp-xs (12px) for tag horizontal padding. The result is a visually consistent card with a clear rhythm.
In the “Arbitrary values” card, three different developers each picked slightly different padding values — 10px, 14px, 12px vertically and 16px, 24px, 18px horizontally. Tags have mismatched internal padding. The overall card looks subtly off-balance. This drift compounds across dozens of components in a real project.
Demo: Page Layout with Semantic Spacing
Every spacing value in this layout maps directly to a token from the table. The header uses vsp-xs / hsp-sm, the main section uses vsp-md / hsp-sm, and the card grid uses hsp-sm for gaps. Reading the code (or the Tailwind classes in a real project) immediately tells you which semantic slot each spacing value fills.
When to Use
Good fit
- Large teams where multiple developers touch the same components — the constrained token set prevents spacing drift
- Design-system-driven projects where designers hand off spacing specs using named tokens rather than pixel values
- Production applications where visual consistency directly impacts user trust and brand perception
- Long-lived codebases that will be maintained and refactored over years — a tight token set makes global spacing changes straightforward (update one token, the whole app adjusts)
Not needed
- Prototypes and hackathons where speed matters more than consistency
- Small personal projects where one developer maintains full context
- Tailwind learning projects where using the full default scale is part of the learning process
Deep Dive
For detailed token strategies in specific categories:
- Color Token Patterns — Semantic color scales, brand colors, state colors, and surface layers (see also Three-Tier Color Strategy for the underlying architecture)
- Typography Token Patterns — Font size, line-height, font-weight, and letter-spacing token strategies
- Token Preview — Visual reference of all available tokens
- Component Tokens & Arbitrary Values — When to use system tokens vs arbitrary values
- Two-Tier Size Strategy — Why width/height sizing skips the abstract layer and uses semantic theme tokens directly