Why the @theme Directive in Tailwind CSS Version 4 Is A Big Deal

I am a software developer with a passion for frontend technologies using vanilla HTML, CSS, JavaScript, and frameworks like Bootstrap and React. I also write Backend for websites sometimes using Python and Django.
For a very long time, front-end developers have had to define and configure CSS styles in a JavaScript config file, ignoring the CSS-first philosophy. They ignored it, not just because they wanted to, but because there was no alternative until now.
The @theme directive moves Tailwind’s configuration from JavaScript (the old tailwind.config.js) to CSS, embracing a CSS-first philosophy. This aligns with the modern web development paradigm, reducing dependency on JavaScript and streamlining workflows. The @theme directive is defined within the base CSS file using special syntax to create design tokens (like colors, fonts, or breakpoints) that automatically generate utility classes and variants. Also, it exposes these tokens as CSS variables, making them accessible anywhere — HTML, inline styles, or JavaScript.
This article explores the @theme directive's primary use cases, hidden feats and capabilities, practical applications, and best practices.
Introduction to @theme
@theme is a special directive recently added in Tailwind CSS version 4.0 specifically to eliminate the config file, tailwind.config.js. It allows developers to use theme variables to define and modify design tokens to their requirements. According to the Tailwind documentation, theme variables are special CSS variables defined using the @theme directive that influence which utility classes exist in your project. That is, they expose utility classes like bg-mint-500, text-mint-500, or fill-mint-500, and make them usable within HTML or JSX/TSX files.
@import "tailwindcss";
@theme {
--font-poppins: Poppins, sans-serif;
}
<h1 class="font-poppins ...">
Hello world, welcome to @theme
</h1>
You might wonder that, since @theme defines styles with variables, why not just use :root to achieve the same in the CSS file? You wouldn’t be wrong to make that assumption, but your knowledge would be limited to what you think @theme can do based on the preview in the code block above. Fortunately, @theme can achieve more. @theme variables are not your regular CSS variables; they do more. They tell Tailwind to create a new utility class that you can use in your components.
Should you now discard :root? Not at all. You can define CSS variables that do not correspond with utility classes within :root, while you retain @theme variables for design tokens you want to map directly to a utility class. You know what? You can use the variables you defined within :root together with @theme variables to create a utility class design token. In fact, you can also use @theme variables as you would :root variables within an element’s style attribute or in a CSS selector. We’ll get to it soon, but let’s first take a look at popular use cases of the @theme directive.
Popular Use Cases: What Developers Love
A couple of developers have had the chance to play with the @theme directive, and so far, these are the bread-and-butter applications that make it a crowd-pleaser.
Custom Styles
Developers can define branded color schemes under the --color-* namespace. For example:
@theme {
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
}
This creates utility classes like .bg-primary-500 or .text-primary-600 and exposes --color-primary-500 as a CSS variable. It’s perfect for consistent branding across projects.
Also, you can define a custom typography for your project rather than using the defaults of font-sans, font-serif, and font-mono. Just as easily as you would define additional or custom fonts in the tailwind.config.js file, you can do the same with a simple variable within the @theme directive.
@theme {
--font-display: "Satoshi", sans-serif;
}
This generates font-display utilities, smoothing typography consistency.
In addition, you can customise --spacing-* to control padding, margins, or widths, ensuring a cohesive scale:
@theme {
--spacing-xs: 0.25rem;
}
Responsive Breakpoints
Besides the fact that you can use @theme variables to declare utilities, they are also useful for defining variants. For instance, the --breakpoint-* namespace determines which responsive breakpoint variants exist in your project:
@theme {
--breakpoint-3xl: 120rem;
}
This enables responsive variants like 3xl:text-lg. It’s a hit for projects needing unique screen-size handling.
You can as sure combine the 2 worlds of utilities and variants in a single @theme variable–--container-*.
@theme {
--container-sm: 20rem;
}
This enables @min-sm:bg-blue-500 variants, ideal for component-driven layouts, and max-w-sm utililities, which are great for defining boundaries based on the device’s dimensions. It’s a hidden gem for modern responsive design.
Hidden Feats: Underrated Capabilities
The @theme directive offers even more. Here are the additional features it presents:
Dynamic Utility Generation With Namespaces
The --* namespaces (e.g., --color-*, --radius-*) are not just for predefined tokens. Developers can invent custom namespaces for niche utilities. For instance:
@theme {
--shadow-glow: 0 0 10px rgba(0, 255, 0, 0.5);
}
This generates shadow-glow utilities, which is perfect for unique effects like neon glows.
CSS Variable Interoperability
Yep! I gave a preamble to this in the intro to @theme section. @theme variables are also CSS variables and can be used as such. That means you can use them in arbitrary values or inline styles after defining them within the directive:
@theme {
--radius-xl: 2rem;
}
.element {
border-radius: calc(var(--radius-xl) - 1px);
}
This is a lifesaver for styling third-party content (e.g., Markdown from APIs) or achieving pixel-perfect designs.
Composing with Modern CSS Functions
Tailwind CSS v4.0 includes several modern CSS features like @property, color-mix(), and oklch(), and you can combine them for something even more beautiful–dynamic theming:
@theme {
--color-accent: oklch(0.84 0.18 117.33);
}
.mixed {
background: color-mix(in srgb, var(--color-accent) 50%, white);
}
This creates adaptive, high-fidelity colors, especially for dark/light modes.
Overriding Defaults
What if you do not want Tailwind’s default @theme variables? You want to start afresh, right? initial got you. You can completely replace Tailwind’s default theme (e.g., the --colors-* namespace), by using --color-*: initial:
@theme {
--color-*: initial;
--color-brand: #ff5733;
}
This clears default colors and defines only your custom ones, reducing bloat. It’s a power move for lean projects.
To completely disable the default theme and use only custom values, you can set the global theme variable namespace, --* , to initial:
@import "tailwindcss";
@theme {
--*: initial;
--spacing: 4px;
--font-body: Inter, sans-serif;
--color-lagoon: oklch(0.72 0.11 221.19);
...
}
At this point, only your custom-defined variables will be available as utility classes within your project.
However, don’t get the initial keyword twisted. It has another use.
Referencing :root Variables
I first came across this in my global.css file after installing Shadcn. I thought to myself, the doc only had @theme {}, so where did @theme inline {} come from? I had to dive deep into the Tailwind documentation to get the answer, and I’m handing it to you on a silver platter.
If you want to define theme variables that reference other variables, use the inline option:
@import "tailwindcss";
@theme inline {
--font-sans: var(--font-inter);
}
When you use the inline option, the utility class will use the theme variable value instead of referencing the actual theme variable. However, if you fail to do this, your utility classes might resolve to unexpected values because of how variables are resolved in CSS.
Practical Examples: Bringing It to Life
The @theme directive in Tailwind CSS v4 shines when applied to real-world scenarios. Below are 2 practical examples that demonstrate its power to create flexible and maintainable design systems. Each example includes code you can drop into a Tailwind v4 project or playground to see immediate results.
These assume you’re using Tailwind v4 with a minimal setup (e.g., via CDN or a Vite project with Tailwind’s CLI).
Dark Mode Theming
Dark mode is a must-have for modern apps. With @theme, you can define light and dark color schemes and pair them with custom variants for seamless toggling.
@import "tailwindcss";
@theme {
/* Light mode colors */
--color-bg: #ffffff;
--color-text: #1f2937;
/* Dark mode colors */
--color-bg-dark: #1f2937;
--color-text-dark: #f3f4f6;
}
/* Apply theme variables to body */
body {
background-color: var(--color-bg);
color: var(--color-text);
}
html.dark body {
background-color: var(--color-bg-dark);
color: var(--color-text-dark);
}
/* Ensure button styles are specific */
.toggle-button {
@apply bg-gray-800 text-white px-4 py-2 rounded cursor-pointer;
}
html.dark .toggle-button {
@apply bg-white text-gray-800;
}
HTML:
<body>
<main class="p-4">
<h1 class="text-2xl font-bold mb-2">Welcome to Dark Mode</h1>
<button onclick="document.documentElement.classList.toggle('dark')" class="toggle-button">
Toggle Dark Mode
</button>
</main>
</body>
https://play.tailwindcss.com/LuNB1BXTQU?file=css
How It Works:
The
@themeblock defines--color-bgand--color-textfor light mode, and--color-bg-darkand--color-text-darkfor dark mode.CSS variables (
var(--color-bg)) ensure styles are dynamic and reusable.The JavaScript toggle adds/removes the
.darkclass to switch themes.
This setup is lightweight, does not require JavaScript-based config, and integrates with Tailwind’s utility-first workflow. Users can extend it by adding more dark mode colors or variants.
Multi-Brand Support
For agencies or platforms supporting multiple brands, @theme can define separate themes toggled via CSS selectors or layers.
@import "tailwindcss";
@theme {
--color-brand: #3b82f6; /* Brand A */
--color-accent: #10b981;
}
/* Brand B theme override using standard CSS scoping */
.brand-b {
--color-brand: #d946ef;
--color-accent: #facc15;
}
.card {
background-color: var(--color-brand);
color: white;
padding: 1rem;
border-radius: 0.5rem;
}
HTML:
<body>
<main class="p-4 flex flex-col gap-y-4">
<div class="card">Brand A Card</div>
<div class="brand-b">
<div class="card">Brand B Card</div>
</div>
</main>
</body>
https://play.tailwindcss.com/jKqpzLXzuJ
How It Works:
The root
@themeblock sets default brand values — in this case, Brand A’s colors (--color-brand,--color-accent).Then, we override the CSS variables manually using standard CSS.
The .card class uses
var(--color-brand)to pull the current brand color from the closest scope — the global root for Brand A, or .brand-b for Brand B.In the HTML, wrapping a card with the .brand-b class correctly switches the colors because of the scoped CSS variable override.
This enables multi-tenant apps to switch themes without duplicating CSS or relying on JavaScript.
Gotchas and Best Practices
While @theme is powerful, there are pitfalls to avoid and best practices to follow to ensure a smooth experience. These tips are based on real-world usage and Tailwind v4’s documentation.
Gotchas
- Top-Level Requirement:
@themevariables must be defined at the root of your CSS file. Nesting them inside media queries or pseudo-classes will break utility generation. For example:
/* ❌ This won’t work */
@media (min-width: 768px) {
@theme {
--color-primary: #3b82f6;
}
}
/* ✅ Do this instead */
@theme {
--color-primary: #3b82f6;
}
- CSS Variable Scope:
@themevariables are global CSS variables, so avoid generic names like--color-blueto prevent collisions with other libraries. Use prefixed names like --color-btn-primary.
Best Practices
- Define Only What’s Needed: Overloading
@themewith unused tokens (e.g., 100 color shades) bloats the generated CSS. Define just the tokens your project requires. For example, limit to 3–5 shades per color:
@theme {
--color-primary-300: #93c5fd;
--color-primary-500: #3b82f6;
--color-primary-700: #1d4ed8;
}
Use Meaningful Namespaces: Stick to Tailwind’s conventions (e.g.,
--color-*,--radius-*) for clarity, but feel free to create custom ones like --shadow-*for project-specific needs.Test With Tailwind Play Or Vite: To verify
@themechanges, use Tailwind Play (available at play.tailwindcss.com) or set up a Vite project with Tailwind’s CLI.
Why @theme matters
The @theme directive is more than a new feature—it’s a paradigm shift that makes Tailwind CSS v4 a powerhouse for building scalable, maintainable design systems. Moving theming to CSS reduces JavaScript dependency, aligns with modern web standards, and empowers developers to define design tokens that automatically generate utilities and variants. Whether you’re crafting a dark mode toggle, a reusable button system, or a multi-brand platform, @theme offers unmatched flexibility.
What makes @theme truly exciting is its hidden potential. Beyond popular use cases like custom colors or breakpoints, developers can explore custom namespaces, integrate with color-mix() for dynamic theming, or leverage container queries for next-gen responsive design. These features position Tailwind at the forefront of CSS innovation in 2025.
By the way, I’m Dr Prime. Nice to meet you, too.



