Tailwind CSS v4 CSS-First Config: Theming Without tailwind.config.js
AI generated
</>
tw
Tailwind CSS · CSS-First · Design Tokens · v4
Tailwind CSS v4 CSS-First Config
Define Theming Without tailwind.config.js

Tailwind CSS v4 puts an end to JavaScript configuration files. The CSS-First approach makes it possible to define colors, fonts, spacing and custom design tokens directly in CSS with the @theme directive, closer to the platform, faster to build and easier to understand.

12 min read @theme · CSS Custom Properties · Design Tokens · @import Tailwind CSS v4.0+

1. Why Tailwind CSS v4 commits to CSS-First

With Tailwind CSS v4, the framework has made a fundamental paradigm shift: configuration moves from JavaScript back into CSS. Until now, tailwind.config.js was the central control file for colors, fonts, breakpoints and every project-specific extension. This approach worked, but it had a substantial drawback: the configuration was separated from the language in which it ultimately took effect. A designer who knows CSS had to understand JavaScript to adjust the theme. A developer had to switch between two files with every change.

The CSS-First approach of Tailwind CSS v4 solves this problem with the new @theme directive. Design tokens are defined directly in the CSS entry file and are immediately available as Tailwind utility classes. The framework parses the @theme blocks at build time and generates both the utility classes and native CSS custom properties from them. The build process becomes faster because no JavaScript needs to be evaluated, and the configuration is readable by anyone who understands CSS, no Node.js knowledge required.

Another advantage of the CSS-First config: the generated custom properties are available at runtime. This means JavaScript code can access the same tokens as CSS, and theme values can be overridden dynamically via JavaScript, something that was not possible with tailwind.config.js because the values only existed at build time.

2. The @theme directive: structure and syntax

The @theme directive is the core of the CSS-First config in Tailwind CSS v4. Inside a @theme block you define CSS custom properties that must follow a specific naming scheme. Tailwind recognizes these properties by their prefixes and derives utility classes from them. --color-primary-500 produces classes like text-primary-500, bg-primary-500 and border-primary-500. The prefix determines which utilities are generated, --font-* for font families, --spacing-* for spacing, --radius-* for border radii.

Important for the Tailwind CSS v4 CSS-First approach: the @theme directive belongs in the main CSS entry file, directly after @import "tailwindcss". It is not a standard CSS directive but is interpreted by the Tailwind preprocessor. In the output CSS, @theme is converted into a :root block with the corresponding CSS custom properties. These properties then carry the actual values and are referenced both by Tailwind's internal utilities and by custom CSS.


/* main.css: Tailwind CSS v4 entry point with CSS-First config */
@import "tailwindcss";

@theme {
  /* Color tokens, generates text-*, bg-*, border-* utilities */
  --color-primary-50:  #eff6ff;
  --color-primary-100: #dbeafe;
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-700: #1d4ed8;
  --color-primary-900: #1e3a8a;

  /* Brand accent */
  --color-accent: #0ea5e9;

  /* Typography tokens, generates font-* utilities */
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-display: "Cal Sans", "Inter", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;

  /* Spacing scale extension */
  --spacing-18: 4.5rem;
  --spacing-22: 5.5rem;
  --spacing-128: 32rem;

  /* Border radius tokens */
  --radius-card: 1rem;
  --radius-pill: 9999px;
}

After the build, Tailwind automatically generates all matching utility classes from this. bg-primary-500, text-primary-700, border-primary-100 are immediately available, with no further configuration. The --color-accent token without a step produces flat single-value utilities like bg-accent. That is the elegant core of the CSS-First config: whatever lives in @theme automatically becomes Tailwind classes.

3. Defining colors with CSS-First config

Colors are the most common use case for the Tailwind CSS v4 CSS-First config. In v3 you had to define a nested JavaScript object with the color steps in tailwind.config.js. In v4 the prefix --color- followed by the color name and step is enough. The steps do not have to follow the standard Tailwind scheme (50, 100, 200 ... 900), custom step names such as --color-brand-muted or --color-surface-elevated are possible.

For semantic color systems, the pattern of also defining semantic tokens alongside the concrete color palettes, which reference the concrete colors, works well. In Tailwind CSS v4 with CSS-First config this works through native CSS variable references: --color-background: var(--color-neutral-50). If you later change the underlying palette, all semantic tokens update automatically. This is a considerable advantage over the old approach, where color values had to be duplicated.

4. Fonts and spacing in the CSS-First approach

Font families are defined in the Tailwind CSS v4 CSS-First configuration using the --font- prefix. The value is a complete font stack declaration, just as it would be used in regular CSS. --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif produces the class font-sans with this full stack. This fully replaces the earlier fontFamily configuration in tailwind.config.js and allows the same control over fallback fonts.

The spacing system can be extended with --spacing-* tokens. Tailwind v4 internally uses a mathematical spacing system with a base unit, so you do not have to define every step individually. With the CSS-First config you add targeted gaps in the standard system, for example --spacing-18: 4.5rem for a value between 16 (4rem) and 20 (5rem). These supplementary tokens then appear in every spacing utility: p-18, m-18, gap-18, w-18 and so on, with no special handling or plugin.


/* Extended theme with semantic color tokens and custom spacing */
@theme {
  /* Semantic color layer, references the concrete palette */
  --color-background:        var(--color-neutral-50);
  --color-surface:           #ffffff;
  --color-surface-elevated:  var(--color-neutral-100);
  --color-foreground:        var(--color-neutral-900);
  --color-muted:             var(--color-neutral-500);
  --color-border:            var(--color-neutral-200);

  /* Interactive states */
  --color-interactive:       var(--color-primary-600);
  --color-interactive-hover: var(--color-primary-700);
  --color-interactive-focus: var(--color-primary-500);

  /* Font size tokens (generates text-* utilities) */
  --text-display-xl: 4.5rem;
  --text-display-lg: 3.75rem;

  /* Line height tokens */
  --leading-display: 1.1;
  --leading-relaxed-lg: 1.75;

  /* Shadow tokens */
  --shadow-card:    0 1px 3px 0 rgba(0,0,0,0.10), 0 1px 2px -1px rgba(0,0,0,0.10);
  --shadow-overlay: 0 20px 60px -12px rgba(0,0,0,0.25);
}

5. Creating custom design tokens with @theme

Beyond the standard prefixes, the Tailwind CSS v4 CSS-First config allows entirely custom token categories. If a prefix does not match one of the built-in Tailwind prefixes, no utility class is generated, but the variable still exists as a CSS custom property in :root and can be used in any CSS. This means @theme also serves as a central design token registry for values that are used outside of utility classes, for example in complex CSS animations or SVG attributes.

For projects with multiple themes or white-label requirements, the CSS-First approach of Tailwind CSS v4 is especially valuable. You define a base theme with @theme and override individual tokens in theme-specific CSS files that are loaded via @import. Since all values are ultimately CSS custom properties, they can also be overridden dynamically via JavaScript, for example for user preferences like dark mode or font size settings.

6. @import and file structure in a CSS-First project

The recommended file structure for a Tailwind CSS v4 CSS-First project is that a single central entry file, typically app.css or main.css, contains all @import directives and the @theme blocks. This is a break with earlier Tailwind versions, where only three @tailwind directives sat in the entry file and the actual configuration lived in tailwind.config.js. In v4, the CSS file is the only place configuration lives.

For larger projects a split is recommended: tokens.css for all @theme definitions, base.css for global reset and base styles with @layer base, components.css for reusable component classes with @layer components and utilities.css for project-specific utilities with @layer utilities. The main file imports these with @import "./tokens.css". The result is a clean separation without the complexity of a JavaScript configuration file.


/* app.css: CSS-First project entry point */
@import "tailwindcss";

/* Project-specific token definitions */
@import "./tokens/colors.css";
@import "./tokens/typography.css";
@import "./tokens/spacing.css";

/* Base layer overrides */
@layer base {
  *, *::before, *::after {
    box-sizing: border-box;
  }

  html {
    /* Enable smooth scrolling globally */
    scroll-behavior: smooth;
    -webkit-text-size-adjust: 100%;
  }

  body {
    font-family: var(--font-sans);
    color: var(--color-foreground);
    background-color: var(--color-background);
    line-height: var(--leading-normal);
  }
}

/* Custom component definitions */
@layer components {
  .btn-primary {
    @apply inline-flex items-center justify-center gap-2 font-semibold;
    @apply bg-primary-600 text-white rounded-card px-6 py-3;
    @apply hover:bg-primary-700 focus-visible:outline-none;
    @apply focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2;
    @apply transition-colors duration-200;
  }
}

7. Migrating from tailwind.config.js to CSS-First

Migrating an existing Tailwind v3 project to the CSS-First config approach of v4 is straightforward in most cases, but it requires a systematic process. The first step is to analyze the existing tailwind.config.js: which colors were extended, which fonts were added, which spacing values were supplemented? Each of these values must be translated into corresponding @theme declarations. The naming convention follows the pattern: the JavaScript path theme.extend.colors.primary[500] becomes --color-primary-500.

Plugins from tailwind.config.js that add new utilities need special consideration. Simple plugins that only add utility classes with fixed values can be translated into @layer utilities. Plugins that dynamically access configuration values are often easier to simplify in Tailwind CSS v4 with CSS-First config, because the tokens are now available as CSS custom properties and can be referenced directly. Some community plugins have already released v4-compatible versions.

8. CSS-First config compared to the JavaScript configuration

A direct comparison shows the concrete differences between the old JavaScript approach and the new Tailwind CSS v4 CSS-First config.

Aspect tailwind.config.js (v3) @theme CSS-First (v4) Advantage of v4
Configuration location JavaScript file outside CSS Directly in CSS with @theme No context switching
Runtime access Build time only CSS custom properties in :root JS access via getComputedStyle
Build speed Node.js evaluates JS config Pure CSS parsing Measurably faster
Extensibility Plugin API with JavaScript @layer + @utility in CSS No JavaScript needed
Dark mode darkMode: 'class' in config @variant dark {...} in CSS Inline, visible context

The CSS-First config approach of Tailwind CSS v4 is not merely a syntactic change. It changes how the framework is embedded in a project. Instead of a boundary between CSS and JavaScript configuration, there is now only a single source of truth: the CSS entry file. This simplifies tooling, eases onboarding for new developers, and opens the door to better IDE integration, because CSS editors can handle @theme directives natively.

9. Common mistakes with the CSS-First approach

The most common mistake when getting started with the Tailwind CSS v4 CSS-First config is forgetting the correct prefix. Anyone who writes --primary-500 instead of --color-primary-500 gets no Tailwind utility classes, the variable exists as a CSS custom property, but Tailwind does not generate bg-primary-500 or text-primary-500 from it. The prefix must exactly match the built-in Tailwind scheme. Another common mistake is defining @theme blocks in files that are not configured as a Tailwind entry file, @theme is only processed in the main entry file and its direct @import files.

Projects migrating from Tailwind v3 to v4 frequently run into the problem that plugins that were registered in tailwind.config.js do not automatically work in the CSS-First approach. Plugins must be loaded explicitly with @plugin "plugin-name" in the entry file. The content array from tailwind.config.js disappears entirely in v4, Tailwind detects the files to scan through automatic heuristics or explicit @source directives. Anyone still working with an explicit content path needs to switch to @source "../src/**/*.{html,js,php}".

10. Summary

The CSS-First config approach of Tailwind CSS v4 is a deliberate step closer to the CSS platform. The @theme directive fully replaces tailwind.config.js for most projects and brings design tokens to where they belong: in CSS. Colors, fonts, spacing, border radii and custom tokens are defined with familiar CSS syntax and are immediately available both as Tailwind utility classes and as CSS custom properties. The build process becomes faster because no JavaScript needs to be evaluated. The runtime availability of the tokens as CSS custom properties opens up new possibilities for dynamic themes and JavaScript integration.

For teams migrating from Tailwind v3, the switching effort is manageable: configuration values translate directly into @theme declarations, and the new file structure with a central CSS entry file is set up in most projects within a few hours. The CSS-First approach is not a breaking change in terms of the class API, the utility classes stay identical. What changes is exclusively the way the theme is configured.

Tailwind CSS v4 CSS-First Config: The Essentials at a Glance

@theme directive

Replaces tailwind.config.js, CSS custom properties with Tailwind prefixes automatically become utility classes. No JavaScript needed.

Runtime tokens

@theme values appear as :root custom properties, JavaScript can access them and override tokens dynamically.

Prefix scheme

--color-* for colors, --font-* for fonts, --spacing-* for spacing. The prefix determines which utility classes are generated.

Migration

theme.extend.colors.primary[500] → --color-primary-500. Load plugins with @plugin. Replace the content array with @source.