| |

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 .

NamespacePrefixUtilities 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

DirectiveEmits UtilitiesEmits VariablesUse For
@themeYesYesPublic design tokens
@theme inlineNoYesInternal tokens, variable references

Token Namespaces

PrefixUtility CategoryExample
--color-*Colorsbg-brand-500
--spacing-*Spacingp-lg, m-4
--font-*Font familyfont-display
--text-*Font sizetext-base
--radius-*Border radiusrounded-card
--ease-*Easingease-fluid
--breakpoint-*BreakpointsResponsive variants

Using Tokens

ContextSyntax
Utility classbg-brand-500
CSS variablevar(--color-brand-500)
Arbitrary valuetext-(--my-var)
Type hinttext-(length:--my-var)

Theming

ApproachMechanism
Default theme:root variables
Dark mode[data-theme="dark"] overrides
High contrast[data-theme="high-contrast"] overrides
Runtime switchJavaScript 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

PitfallWhy It HappensFix
Utility not generatedPrefix not in a known namespaceUse correct prefix (--color-*, etc.)
Variable undefinedToken not in @theme or :rootDeclare in @theme or :root
Utility namespace pollutedUsing @theme for internal tokensUse @theme inline
Referencing external var fails@theme resolves at build timeUse @theme inline
Dark mode not applyingOverride selector not matchingCheck [data-theme] on ancestor
Type hint missingAmbiguous namespace with CSS varAdd (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

ItemValue
ConfigurationCSS-first, via @theme directive
Token exposureEvery token is a native CSS variable
@themeEmits utilities and variables
@theme inlineEmits variables only, no utilities
Namespaces--color-*, --spacing-*, --font-*, --radius-*, etc.
ThemingOverride variables in :root or [data-theme]
Runtime switchSet data-theme attribute with JavaScript
Type hintstext-(length:--var) or text-(color:--var)
Semantic tokensReference primitives; use in components
External variablesUse @theme inline for var() references

Key takeaways:

  • Tailwind v4 tokens are CSS variables. The @theme directive defines them, and every token is available at runtime via var() .
  • @theme generates utilities; @theme inline does not. Use @theme for public design tokens and @theme inline for 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), not var(--color-brand-50), so remapping the semantic layer changes the entire theme .
  • @theme inline is required for external variables. A token that references a runtime variable from a third-party library must use inline so 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) and text-(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!