Theme
Purpose and Scope
Tailwind CSS v4 treats a project theme as the place where low-level design decisions become utility-class APIs. In the official docs, these values are called theme variables: special CSS variables declared with the @theme directive that both publish design tokens as CSS variables and influence which utilities Tailwind can generate. This page explains that CSS-first model, then connects it to the compatibility code that still accepts JavaScript configuration, legacy theme() calls, keyframes, and theme-defined variants. The goal is to help you decide where to define tokens, how those tokens reach generated CSS, and how older projects are migrated safely.
Sources: packages/tailwindcss/src/compat/apply-config-to-theme.ts, packages/tailwindcss/src/compat/default-theme.ts
The important distinction is that a regular CSS variable and a Tailwind theme variable are not the same contract. A regular variable can be useful for app-local values, but a theme variable is intended to map to generated utilities such as color, font, spacing, animation, and breakpoint-related classes. Repository code reflects that contract by translating theme-like inputs into namespaced CSS custom properties on the design system theme, and by attaching options that describe how those values should be emitted or referenced. This is why theme customization is both a styling surface and a compiler input.
Relevant Source Files
- packages/tailwindcss/src/compat/apply-config-to-theme.ts — Converts resolved JavaScript theme values into design-system theme variables, handles replaced namespaces, normalizes opacity and alpha placeholders, and updates default font variables when configured font families override defaults.
- packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts — Moves theme.keyframes entries from resolved configuration into the design-system theme as @keyframes at-rules.
- packages/tailwindcss/src/compat/default-theme.ts — Defines the compatibility default theme object, including shared namespaces, default animations, color references, ARIA defaults, and bare-value handlers for numeric utility values.
- packages/tailwindcss/src/compat/theme-variants.ts — Registers compatibility overrides for theme-defined aria, supports, and data variants by wrapping the core functional variants.
- packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts — Provides a PostCSS codemod that rewrites legacy theme() usage in CSS declarations and supported at-rule parameters.
- packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts — Implements the shared converter that parses values, detects theme() calls and slash modifiers, and rewrites them to var() or modern theme() notation where safe.
Core Primitives
The main primitive is the design token, represented in v4 docs as a theme variable such as --color-mint-500. Defining that variable with @theme declares that the value belongs to Tailwind’s theme namespace, so classes like bg-mint-500 or text-mint-500 can exist. Inside the compatibility layer, the equivalent operation is performed programmatically: resolved configuration values are walked, converted into CSS custom property names, and added to the design system theme. That bridge lets older JavaScript configuration participate in the same compiler model used by CSS-first theme variables.
Sources: packages/tailwindcss/src/compat/apply-config-to-theme.ts
The second primitive is the namespace. Tailwind’s theme values are not stored as one flat bag of unrelated strings; they are organized through key paths that become CSS custom property names. For example, a legacy path under colors, opacity, fontFamily, or animation is converted through keyPathToCssProperty before being added with a leading -- custom-property prefix. Namespaces also matter when a key has been replaced, because the compatibility code clears the old namespace before adding replacement values. That behavior prevents stale default values from coexisting with intentionally replaced theme sections.
Sources: packages/tailwindcss/src/compat/apply-config-to-theme.ts
The third primitive is the default theme. The compatibility default theme is a large object of named values and theme references that preserve long-standing Tailwind APIs. It includes defaults such as animation names, ARIA shortcuts, aspect ratios, background images, and many namespaces that resolve through theme('colors'), theme('blur'), theme('opacity'), and related lookups. It also defines bare-value handlers for positive integers, percentages, pixels, milliseconds, degrees, aspect-ratio fractions, and grid repeat values. Those handlers explain why some utilities can accept meaningful bare numeric values instead of only named scale keys.
Sources: packages/tailwindcss/src/compat/default-theme.ts
System-to-Code Mapping
The CSS-first path starts in author CSS with @theme. The compatibility path starts in a resolved JavaScript configuration object, but both need to populate the design system theme. applyConfigToTheme takes a DesignSystem, the resolved theme, and a set of replaced theme keys. It first clears replaced namespaces, then iterates over themeable values and adds primitive string or number values as inline, reference, and default theme entries. While doing so it normalizes legacy forms, replacing placeholders with 1 and converting opacity numbers between 0 and 1 into percentage strings.
Sources: packages/tailwindcss/src/compat/apply-config-to-theme.ts
Font families have an additional compatibility rule because default font variables may reference values that will not be emitted when a JavaScript config overrides fontFamily.sans or fontFamily.mono. The code resolves array and object forms used by font-family configuration, then updates --default-font-family, --default-font-feature-settings, --default-font-variation-settings, and the corresponding mono defaults when the default font namespace exists. In practice, this protects common v3-style configuration from producing utilities that refer to missing default font variables after migration into the v4 theme-variable world.
Sources: packages/tailwindcss/src/compat/apply-config-to-theme.ts
Keyframes are theme content too, but they are not scalar CSS variables. applyKeyframesToTheme checks whether the resolved theme contains keyframes, converts each named keyframe object into an AST @keyframes rule with objectToAst, and registers it on the design-system theme. This keeps animation declarations and the keyframes they reference moving through the same theme application stage. It also mirrors the mental model authors use in configuration: animation tokens and keyframe definitions are both design-system assets, even though one compiles to custom properties and the other compiles to at-rules.
Sources: packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts
Migration Behavior for theme() and var()
The upgrade package encodes an important v4 migration rule: many legacy theme() calls can become direct CSS variable references. The CSS codemod creates a converter from the current design system, walks declarations, and rewrites declaration values. It also walks media, custom-media, container, and supports at-rules, but for those parameters it uses a mode that only migrates to the newer theme() notation instead of var(). That distinction avoids producing var() references in syntactic positions where a CSS variable would not be safe or equivalent.
Sources: packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts, packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts
The shared template converter is deliberately conservative. It parses a value AST, counts theme() calls, and separately counts theme() calls that use slash modifiers, such as color opacity modifiers. If no theme() call appears, the original value is returned unchanged. If theme() calls appear without modifiers, the converter can substitute var() references. If multiple modifier-bearing theme() calls appear, it falls back to modern theme() notation because converting all of them to a single class-candidate modifier would lose information. With exactly one safe modifier, it can produce a CandidateModifier for the caller.
Sources: packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts
Theme Variants and Configuration Compatibility
Theme customization is not limited to values used by declarations. Legacy configuration can also define named variants for aria, supports, and data. registerThemeVariantOverrides reads config.theme.aria, config.theme.supports, and config.theme.data, then wraps the corresponding core functional variants when a namespace has custom entries. When a named variant matches a configured value, the wrapper converts it into an arbitrary value and delegates back to the original apply function. This preserves the core variant behavior while allowing configuration-defined shortcuts to remain valid.
Sources: packages/tailwindcss/src/compat/theme-variants.ts
This variant design is important because it keeps theme-defined state shortcuts from becoming a separate variant engine. The compatibility code retrieves the existing core variant, preserves its compound behavior, and only changes the value passed into that core implementation when the named value is configured. For authors, that means a project can continue to expose semantic shortcuts like configured aria, supports, or data variants while still benefiting from the same composition rules as built-in variants. It is an adapter layer, not a parallel implementation.
Sources: packages/tailwindcss/src/compat/theme-variants.ts
Practical Guidance and Next Steps
Use @theme when a value should create or influence Tailwind utilities, and use ordinary selectors such as :root when you only need an application CSS variable with no utility-class API. For migrated projects, expect old JavaScript theme configuration to be translated into design-system theme variables where the value is a primitive token, while richer structures such as keyframes and variant shortcuts are registered through their specialized compatibility paths. When reviewing a migration, pay special attention to opacity values, placeholders, fontFamily overrides, theme() calls inside at-rule parameters, and theme() calls with slash modifiers.
A useful review flow is to start with the tokens your design system owns, confirm whether each token should be a Tailwind theme variable, then inspect any legacy config that still feeds theme namespaces. Next, check custom animations and keyframes together so animation utilities resolve to available keyframe rules. Finally, run or review upgrade codemods for CSS and templates where legacy theme() usage appears, because the converter’s output depends on whether a value can safely become var(), must remain theme(), or needs to expose a candidate modifier. Related pages: Colors, Functions and Directives, Adding Custom Styles, Configuration and Plugin API, Upgrade Guide.