Tailwind CSS 6 🎨 Native CSS Variables as Design Tokens
Tailwind CSS v4 represents a fundamental architectural shift: configuration moved from JavaScript to CSS, and every design token is now exposed as a native CSS variable. Where v3 required a tailwind.config.js file and a build step that generated static utility classes, v4 defines tokens directly in CSS using the @theme directive, and every value becomes a --* custom property available at runtime . This change is not cosmetic. It means design tokens are no longer a Tailwind-specific abstraction; they are standard CSS variables that any stylesheet, any JavaScript, and any tool can read and modify. Theming, dark mode, and dynamic runtime changes all become native CSS operations rather than framework-specific workarounds.
The practical consequence is that a design system built on Tailwind v4 is a design system built on CSS custom properties. The @theme block defines the tokens, the utilities consume them, and the browser exposes them for any other use. A color defined as --color-primary is simultaneously a utility (bg-primary), a CSS variable (var(--color-primary)), and a value that can be overridden at any selector level. Understanding this architecture is understanding how to build a maintainable design system on Tailwind v4.
Key point: In Tailwind v4, @theme defines design tokens as native CSS variables. Every token generates a corresponding utility and is available at runtime via var(--token-name). The @theme inline variant exposes variables without generating utilities, and :root or [data-theme] selectors can override values for runtime theming.
Why native CSS variables as design tokens matter
The configuration portability problem. In v3, design tokens lived in tailwind.config.js. They were consumed by the Tailwind build process but were not accessible to plain CSS or JavaScript without duplicating them. A designer who wanted to use the brand color in a custom stylesheet had to copy the hex value, creating a second source of truth that drifted over time. In v4, the token is a CSS variable; the utility and the custom CSS both read from the same definition .
The runtime theming problem. A v3 dark mode implementation relied on the dark: variant, which generated duplicated utility classes under a media query or class selector. The theme was baked into the CSS at build time. In v4, a theme is a set of CSS variable overrides. Changing [data-theme="dark"] on the <html> element changes every token value in the cascade, and every utility that reads those variables updates automatically . No rebuild, no duplicated classes.
The semantic token problem. Raw values like #3b82f6 are difficult to maintain because they describe what the value is, not what it is for. A semantic token like --color-primary describes intent and can be remapped without changing component code . Tailwind v4’s namespace system (--color-*, --spacing-*, --font-*, --radius-*) provides a convention for organizing these tokens, and the @theme directive registers them as utilities .
The ambiguity problem. When a utility shares a namespace across multiple CSS properties, such as text-lg (font-size) and text-black (color), Tailwind must determine which property the value applies to. With static values, it can infer from the value type. With CSS variables, the value is opaque, so v4 provides type hints: text-(length:--my-var) or text-(color:--my-var) . This is a direct consequence of tokens being variables rather than literals.
The non-utility token problem. Not every design token should generate a utility class. A sidebar width, an animation duration, or an internal calculation has a value that belongs in var() but should not pollute the utility namespace with sidebar-width-* classes. The @theme inline directive solves this: it defines the variable for CSS consumption without generating utilities .
a. The @theme directive
The @theme directive is where design tokens are defined in Tailwind v4. It replaces the theme.extend section of the v3 configuration file. The directive accepts CSS custom properties with specific prefixes that determine which utilities they generate .
@import "tailwindcss";
@theme {
--color-brand-50: oklch(0.97 0.01 240);
--color-brand-500: oklch(0.65 0.15 240);
--color-brand-900: oklch(0.25 0.09 240);
--font-display: "Satoshi", sans-serif;
--radius-card: 8px;
--ease-fluid: cubic-bezier(0.3, 0, 0, 1);
}
Each variable in @theme does two things. It registers the token as a design value for the utility system, and it emits the variable to the compiled CSS so it can be used in var() . The --color-brand-500 token produces the bg-brand-500, text-brand-500, border-brand-500 utilities and the var(--color-brand-500) variable.
The naming convention is not arbitrary. Tailwind’s namespace system maps prefixes to utility categories. The --color-* prefix generates color utilities. The --spacing-* prefix generates spacing utilities. The --font-* prefix generates font-family utilities. The --radius-* prefix generates border-radius utilities . Any variable that does not match a known namespace does not generate a utility, but it is still available as a CSS variable if declared in :root or if the @theme block is used with inline .
b. The token namespaces
Tailwind v4 organizes tokens into namespaces, each corresponding to a category of utilities. Understanding the namespace system is understanding which prefixes produce which utilities .
| Namespace | Prefix | Utilities Generated |
|---|---|---|
| Colors | --color-* | bg-*, text-*, border-*, ring-* |
| Spacing | --spacing-* | p-*, m-*, gap-*, w-* |
| Font families | --font-* | font-* |
| Font sizes | --text-* | text-* |
| Font weights | --font-weight-* | font-* |
| Border radius | --radius-* | rounded-* |
| Breakpoints | --breakpoint-* | Responsive variants |
| Shadows | --shadow-* | shadow-* |
| Easing | --ease-* | ease-* |
| Animations | --animate-* | animate-* |
The --spacing variable is special. It defines the base unit for the spacing scale, and bare numeric utilities multiply it. With --spacing: 0.25rem, the utility p-4 computes 4 * var(--spacing) = 1rem . Named spacing tokens like --spacing-lg: 1.5rem generate utilities like p-lg and m-lg in addition to the numeric scale.
Font sizes use a compound namespace: --text-lg defines the size and --text-lg--line-height defines the associated line height. Both are part of the same token .
@theme {
--text-base: 1rem;
--text-base--line-height: 1.5;
--text-lg: 1.125rem;
--text-lg--line-height: 1.6;
}
c. @theme vs @theme inline
The @theme directive emits both utilities and CSS variables. The @theme inline variant emits only the CSS variables, without generating utility classes .
This distinction matters for two cases. First, internal tokens that should not be exposed as utilities: a sidebar width, an animation duration, or a component-specific calculation. Second, tokens that reference other CSS variables, such as a semantic color that maps to a Radix UI variable or a theme-dependent value that resolves differently per theme .
/* Public tokens — generate utilities */
@theme {
--color-brand: #3b82f6;
}
/* Internal tokens — var() only, no utilities */
@theme inline {
--sidebar-width: 280px;
--animation-duration-fast: 150ms;
}
.sidebar {
width: var(--sidebar-width);
transition: transform var(--animation-duration-fast);
}
Without inline, --sidebar-width would generate a sidebar-width utility, which nobody should use. With inline, the variable is available for var() but does not pollute the utility namespace.
The inline variant is also the solution for referencing external CSS variables. If a design system defines --accent-9 and Tailwind should expose it as bg-accent, the definition must use @theme inline so that Tailwind does not try to resolve the variable at build time .
@theme inline {
--color-accent: var(--accent-9);
}
This compiles bg-accent to background-color: var(--accent-9) rather than background-color: var(--color-accent), which would be undefined because --accent-9 is only defined at runtime .
d. Overriding tokens for themes
Because tokens are CSS variables, themes are implemented by overriding variable values in a different scope. The :root selector defines the default theme. A [data-theme="dark"] selector overrides the tokens for dark mode. No Tailwind-specific mechanism is required; the CSS cascade handles it .
:root {
--color-surface: white;
--color-text: oklch(0.2 0.01 240);
}
[data-theme="dark"] {
--color-surface: oklch(0.15 0.01 240);
--color-text: oklch(0.95 0.01 240);
}
A utility like bg-surface compiles to background-color: var(--color-surface). When the theme changes, the variable resolves differently, and the utility’s output changes without any class changes or rebuild .
The same mechanism supports multiple themes beyond dark mode. A [data-theme="high-contrast"] selector can override the same tokens with high-contrast values. The components do not know which theme is active; they read the resolved variable .
This approach is distinct from Tailwind’s dark: variant, which generates duplicated utilities. The variable override approach produces a single utility that adapts to the active theme. The trade-off is that the theme must be set on an ancestor element, and the variable must be defined in a scope that the utility can inherit from.
e. Semantic token architecture
A design system built on Tailwind v4 typically uses a two-tier token architecture: primitives and semantics .
Primitives are raw values with no semantic meaning. --blue-500, --grey-200, --space-4, --radius-lg. They describe what the value is. In Tailwind v4, these are defined in @theme under the appropriate namespace, generating the full palette of utilities .
Semantics reference primitives and describe what something is for. --color-link, --color-surface, --color-border, --radius-card. These are defined in @theme inline or in :root because they map to other variables rather than literal values .
@theme {
--color-blue-500: oklch(0.65 0.15 240);
--color-grey-200: oklch(0.88 0.01 240);
}
@theme inline {
--color-link: var(--color-blue-500);
--color-border: var(--color-grey-200);
}
Components use semantic tokens. A button uses var(--color-link) for its background, not var(--color-blue-500). When the link color changes, only the semantic mapping changes; the button code is untouched .
This indirection is what makes theming possible without touching components. Dark mode remaps the semantic tokens to different primitives. A brand refresh changes the primitives, and the semantics follow.
f. Using tokens in arbitrary values and custom CSS
Tailwind v4 provides syntax for using CSS variables directly in utilities without registering them as theme tokens.
The parenthesized syntax text-(--my-var) is shorthand for text-[var(--my-var)]. It applies the var() function automatically. When the namespace is ambiguous — text- could mean color or size — a type hint disambiguates: text-(length:--my-var) for font size, text-(color:--my-var) for color .
<div class="text-(--brand-color)">Brand</div>
<div class="text-(length:--font-size)">Sized text</div>
For custom CSS that needs to reference theme tokens, the variables are available as standard CSS variables. A custom class can use var(--color-primary) without any Tailwind-specific syntax .
.custom-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-card);
padding: var(--spacing-4);
}
This is the mechanism that makes Tailwind v4 a design system tool rather than just a utility framework. The tokens defined in @theme are the design system, and the utilities are one way to consume them. Custom CSS is another. JavaScript is another.
Complete Example Session
/* ============================================
PART 1: IMPORT TAILWIND
============================================ */
@import "tailwindcss";
/* ============================================
PART 2: DEFINE PRIMITIVE COLOR TOKENS
============================================ */
@theme {
--color-brand-50: oklch(0.97 0.01 240);
--color-brand-100: oklch(0.94 0.03 240);
--color-brand-500: oklch(0.65 0.15 240);
--color-brand-900: oklch(0.25 0.09 240);
}
/* ============================================
PART 3: DEFINE SEMANTIC TOKENS
============================================ */
@theme inline {
--color-surface: var(--color-brand-50);
--color-text-primary: var(--color-brand-900);
--color-link: var(--color-brand-500);
}
/* ============================================
PART 4: DEFINE SPACING AND RADIUS
============================================ */
@theme {
--spacing: 0.25rem;
--spacing-lg: 1.5rem;
--radius-card: 8px;
--radius-button: 6px;
}
/* ============================================
PART 5: DEFINE TYPOGRAPHY TOKENS
============================================ */
@theme {
--font-display: "Satoshi", sans-serif;
--font-body: "Inter", sans-serif;
--text-base: 1rem;
--text-base--line-height: 1.5;
}
/* ============================================
PART 6: USE TOKENS IN UTILITIES
============================================ */
/* These classes are generated by the tokens above */
/* bg-surface, text-text-primary, p-lg, rounded-card */
/* font-display, text-base */
<div class="bg-surface text-text-primary p-lg rounded-card font-display">
<a href="#" class="text-link">Brand link</a>
</div>
/* ============================================
PART 7: USE TOKENS IN CUSTOM CSS
============================================ */
.custom-panel {
background: var(--color-surface);
border: 1px solid var(--color-brand-100);
border-radius: var(--radius-card);
padding: var(--spacing-lg);
}
/* ============================================
PART 8: DARK MODE VIA VARIABLE OVERRIDE
============================================ */
:root {
--color-surface: var(--color-brand-50);
--color-text-primary: var(--color-brand-900);
}
[data-theme="dark"] {
--color-surface: oklch(0.15 0.01 240);
--color-text-primary: oklch(0.95 0.01 240);
}
/* ============================================
PART 9: SWITCH THEME WITH JAVASCRIPT
============================================ */
document.documentElement.setAttribute('data-theme', 'dark');
// Or: document.documentElement.removeAttribute('data-theme');
<!-- ============================================
PART 10: ARBITRARY VALUE WITH TYPE HINT
============================================ -->
<div class="text-(color:--brand-color)">Typed as color</div>
<div class="text-(length:--font-size)">Typed as length</div>
These ten parts cover the complete token lifecycle: defining primitives, mapping to semantics, using tokens in utilities and custom CSS, overriding for themes, switching themes at runtime, and disambiguating types when using CSS variables in arbitrary values.
Quick Reference
@theme Directives
| Directive | Emits Utilities | Emits Variables | Use For |
|---|---|---|---|
@theme | Yes | Yes | Public design tokens |
@theme inline | No | Yes | Internal tokens, variable references |
Token Namespaces
| Prefix | Utility Category | Example |
|---|---|---|
--color-* | Colors | bg-brand-500 |
--spacing-* | Spacing | p-lg, m-4 |
--font-* | Font family | font-display |
--text-* | Font size | text-base |
--radius-* | Border radius | rounded-card |
--ease-* | Easing | ease-fluid |
--breakpoint-* | Breakpoints | Responsive variants |
Using Tokens
| Context | Syntax |
|---|---|
| Utility class | bg-brand-500 |
| CSS variable | var(--color-brand-500) |
| Arbitrary value | text-(--my-var) |
| Type hint | text-(length:--my-var) |
Theming
| Approach | Mechanism |
|---|---|
| Default theme | :root variables |
| Dark mode | [data-theme="dark"] overrides |
| High contrast | [data-theme="high-contrast"] overrides |
| Runtime switch | JavaScript sets data-theme attribute |
Best Practices
✅ Do This:
@theme {
--color-brand-500: oklch(0.65 0.15 240);
}
@theme inline {
--color-link: var(--color-brand-500);
}
:root {
--color-surface: white;
}
[data-theme="dark"] {
--color-surface: oklch(0.15 0.01 240);
}
❌ Don’t Do This:
@theme {
--sidebar-width: 280px; /* ❌ Generates sidebar-width utility */
}
@theme {
--color-accent: var(--accent-9); /* ❌ Fails; use @theme inline */
}
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Utility not generated | Prefix not in a known namespace | Use correct prefix (--color-*, etc.) |
| Variable undefined | Token not in @theme or :root | Declare in @theme or :root |
| Utility namespace polluted | Using @theme for internal tokens | Use @theme inline |
| Referencing external var fails | @theme resolves at build time | Use @theme inline |
| Dark mode not applying | Override selector not matching | Check [data-theme] on ancestor |
| Type hint missing | Ambiguous namespace with CSS var | Add (color:) or (length:) |
Real-World Examples
1. Brand Color Palette
@theme {
--color-brand-50: oklch(0.97 0.01 240);
--color-brand-500: oklch(0.65 0.15 240);
--color-brand-900: oklch(0.25 0.09 240);
}
2. Semantic Surface Tokens
@theme inline {
--color-surface: var(--color-brand-50);
--color-surface-raised: white;
}
3. Dark Mode Override
[data-theme="dark"] {
--color-surface: oklch(0.15 0.01 240);
--color-surface-raised: oklch(0.20 0.01 240);
}
4. Internal Token Without Utility
@theme inline {
--sidebar-width: 280px;
}
.sidebar { width: var(--sidebar-width); }
5. Font Family Token
@theme {
--font-display: "Satoshi", sans-serif;
}
/* Generates font-display utility */
6. Custom Easing Token
@theme {
--ease-fluid: cubic-bezier(0.3, 0, 0, 1);
}
/* Generates ease-fluid utility */
7. Using Token in Custom CSS
.card {
background: var(--color-surface);
border-radius: var(--radius-card);
}
8. Arbitrary Value with Type Hint
<div class="text-(length:--heading-size)">Heading</div>
9. Radix UI Integration
@theme inline {
--color-accent: var(--accent-9);
}
10. Theme Switching
document.documentElement.setAttribute('data-theme', 'dark');
Visual
Token Architecture
┌──────────────────────────────────────────────────────────────┐
│ TWO-TIER TOKEN ARCHITECTURE │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ TIER 1: PRIMITIVES (@theme) │ │
│ │ --color-brand-50 │ │
│ │ --color-brand-500 │ │
│ │ --color-brand-900 │ │
│ │ --spacing-4 │ │
│ │ --radius-lg │ │
│ └──────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌─────────────┴─────────────┐ │
│ ▼ ▼ │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ UTILITIES │ │ TIER 2: SEMANTIC │ │
│ │ bg-brand-500 │ │ (@theme inline) │ │
│ │ text-brand-900 │ │ --color-link │ │
│ │ p-4, m-lg │ │ --color-surface │ │
│ └────────────────────┘ │ --color-border │ │
│ └──────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ COMPONENTS │ │
│ │ background: │ │
│ │ var(--color- │ │
│ │ surface) │ │
│ └────────────────────┘ │
│ │
│ Primitives are raw values. Semantics describe intent. │
│ Components use semantics. │
└──────────────────────────────────────────────────────────────┘
@theme vs @theme inline
┌──────────────────────────────────────────────────────────────┐
│ @theme @theme inline │
│ │
│ @theme { @theme inline { │
│ --color-brand: #3b82f6; --sidebar-width: 280px; │
│ } } │
│ │
│ Emits: Emits: │
│ ├── Utility: bg-brand ├── Utility: none │
│ ├── Utility: text-brand ├── Variable: --sidebar-width │
│ └── Variable: --color-brand └── var(--sidebar-width) works│
│ │
│ Use for public tokens Use for internal tokens │
│ that should have utilities. or variable references. │
└──────────────────────────────────────────────────────────────┘
Runtime Theme Switching
┌──────────────────────────────────────────────────────────────┐
│ THEME VIA VARIABLE OVERRIDE, NOT DUPLICATED CLASSES │
│ │
│ :root { │
│ --color-surface: white; │
│ } │
│ │
│ [data-theme="dark"] { │
│ --color-surface: oklch(0.15 0.01 240); │
│ } │
│ │
│ Utility: .bg-surface { │
│ background-color: var(--color-surface); │
│ } │
│ │
│ <html> <html data-theme="dark"> │
│ └── .bg-surface └── .bg-surface │
│ └── white └── dark │
│ │
│ Same class, different resolved value. No rebuild. │
└──────────────────────────────────────────────────────────────┘
Type Hints for CSS Variables
┌──────────────────────────────────────────────────────────────┐
│ DISAMBIGUATING CSS VARIABLES IN UTILITIES │
│ │
│ Ambiguous: │
│ text-(--my-var) │
│ └── Is it font-size or color? Tailwind cannot know. │
│ │
│ Type hint: │
│ text-(length:--my-var) → font-size: var(--my-var) │
│ text-(color:--my-var) → color: var(--my-var) │
│ │
│ The hint tells Tailwind which utility to generate. │
│ This applies only when the value is a CSS variable, │
│ because static values (22px, #bada55) are self-describing. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Configuration | CSS-first, via @theme directive |
| Token exposure | Every token is a native CSS variable |
@theme | Emits utilities and variables |
@theme inline | Emits variables only, no utilities |
| Namespaces | --color-*, --spacing-*, --font-*, --radius-*, etc. |
| Theming | Override variables in :root or [data-theme] |
| Runtime switch | Set data-theme attribute with JavaScript |
| Type hints | text-(length:--var) or text-(color:--var) |
| Semantic tokens | Reference primitives; use in components |
| External variables | Use @theme inline for var() references |
Key takeaways:
- Tailwind v4 tokens are CSS variables. The
@themedirective defines them, and every token is available at runtime viavar(). @themegenerates utilities;@theme inlinedoes not. Use@themefor public design tokens and@theme inlinefor internal values or variable references .- The namespace prefix determines the utility.
--color-*generates color utilities,--spacing-*generates spacing utilities, and so on . - Theming is variable overriding. A
[data-theme="dark"]selector overrides token values, and every utility reading those tokens updates automatically . - Semantic tokens reference primitives. Components use
var(--color-surface), notvar(--color-brand-50), so remapping the semantic layer changes the entire theme . @theme inlineis required for external variables. A token that references a runtime variable from a third-party library must useinlineso Tailwind does not try to resolve it at build time .- Type hints disambiguate CSS variables. When a namespace maps to multiple CSS properties,
text-(length:--var)andtext-(color:--var)tell Tailwind which utility to generate .
Remember: Tailwind v4 replaced the JavaScript configuration file with native CSS variables. The @theme directive is where design tokens live, and every token it defines becomes both a utility class and a CSS variable. This dual nature is the key to the architecture: the utility is convenient for markup, and the variable is accessible to custom CSS, JavaScript, and runtime theming. The distinction between @theme and @theme inline determines whether a token generates utilities. The distinction between primitives and semantics organizes the system so that component code references intent, not raw values. And the ability to override variables in a selector scope is what makes dark mode and multi-theme support a CSS cascade operation rather than a build-time duplication. Understanding this architecture is understanding how a Tailwind v4 design system is built.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!