Tailwind CSS v4 Variants — Unified Variant System
In this tutorial, you will learn about Tailwind CSS v4 Variants. We cover key concepts, practical examples, and best practices to help you master this topic.
Tailwind CSS v4 variants use a unified system where all variants (responsive, state, dark, custom) follow the same prefix syntax, with new @variant directive for defining custom variants in CSS.
What You'll Learn
You will learn the unified variant syntax, how to define custom variants with @variant, how variant composition works, and the new capabilities over v3's variant system.
Why It Matters
Unified variants simplify the mental model. DodaTech's v4 Migration reduced variant-related confusion, with custom variants defined next to the components that use them.
Real-World Use
Doda Browser's extension defines open:, loading:, and print: variants using @variant, all following the same prefix pattern as built-in hover: and focus:.
flowchart LR
A[theme() Function] --> B[Variants v4]
B --> C[Prefix Syntax]
B --> D[@variant Directive]
B --> E[Composition]
B --> F[Custom Logic]
style B fill:#38bdf8,stroke:#0284c7,color:#fff
style C fill:#22c55e,stroke:#16a34a,color:#fff
Same Prefix Syntax
<!-- v4 uses the same prefix syntax as v3 -->
<div class="
text-base
md:text-lg
lg:text-xl
hover:text-brand-600
focus:ring-2
dark:text-white
dark:hover:text-brand-400
motion-reduce:transition-none
">
All standard variants work the same way in v4.
</div>
Expected output: All v3 variant prefixes (md:, hover:, dark:, focus:, motion-reduce:) work identically in v4. No syntax changes needed.
The @variant Directive
@import "tailwindcss";
/* Define a custom variant for open/closed state */
@variant open (&:where([open], .open));
/* Define a variant for loading state */
@variant loading (&:where([data-loading="true"]));
/* Define a variant for print media */
@variant print (@media print);
/* Define a variant for landscape orientation */
@variant landscape (@media (orientation: landscape));
/* Define a variant for focused-within */
@variant focus-within (&:focus-within);
<details class="open:bg-blue-50 open:p-4 open:rounded-lg">
<summary class="font-medium cursor-pointer">Click to expand</summary>
<p class="mt-2 text-gray-600">Content revealed on open.</p>
</details>
<div data-loading="true" class="loading:opacity-50 loading:cursor-wait">
Loading content...
</div>
<div class="landscape:flex landscape:gap-4 print:hidden">
Side-by-side in landscape, hidden when printing.
</div>
Expected output: Custom variants defined with @variant work with the same prefix syntax. @variant supports complex selectors and media queries.
Variant Composition
/* Composing variants from existing ones */
@variant hover-active (&:hover, &:active);
@variant desktop-hover (@media (hover: hover) {
&:hover;
});
/* Combining multiple conditions */
@variant dark-focus (@media (prefers-color-scheme: dark) {
&:focus;
});
<button class="
bg-blue-600 text-white px-4 py-2 rounded
hover-active:bg-blue-800
desktop-hover:shadow-lg
dark-focus:ring-blue-300
">
Composed variants
</button>
Expected output: Variants can be composed from multiple existing variants. hover-active applies on both hover and active. desktop-hover only applies on devices with hover capability.
Variants with Custom Selectors
/* Variants for specific element states */
@variant invalid (&:user-invalid);
@variant valid (&:user-valid);
/* Parent state variant */
@variant parent-hover (.parent:hover &);
/* Sibling state variant (like peer in v3) */
@variant sibling-checked (.sibling:checked ~ &);
/* Nth-child variant */
@variant even (&:nth-child(even));
@variant odd (&:nth-child(odd));
<input type="email" required class="
invalid:border-red-500 invalid:ring-red-500
valid:border-green-500 valid:ring-green-500
focus:ring-2 focus:ring-blue-500
">
<div class="parent-hover:bg-blue-50 p-4">
This div highlights when .parent is hovered.
</div>
<div class="space-y-2">
<div class="even:bg-gray-50 p-2">Even row background</div>
<div class="even:bg-gray-50 p-2">Even row background</div>
<div class="even:bg-gray-50 p-2">Even row background</div>
</div>
Expected output: Variants target user-invalid/user-valid states, parent hover, sibling checked, and nth-child patterns -- all defined with @variant.
Variant with Parameters
/* Parameterized variants */
@variant min-width($size) (@media (min-width: $size));
@variant max-width($size) (@media (max-width: $size));
<div class="min-width-[600px]:flex min-width-[600px]:gap-4">
<div class="bg-blue-100 p-4">Column 1</div>
<div class="bg-green-100 p-4">Column 2</div>
</div>
Expected output: Parameterized variants accept arguments, enabling dynamic variant creation without losing type safety.
Variant Order and Specificity
/* Variant ordering follows specificity */
@variant important (&) {
/* This variant has higher specificity */
@media (prefers-color-scheme: dark) {
& {
/* Highest specificity variant */
}
}
}
Expected output: Variant specificity follows the cascade. More specific variants override less specific ones, following CSS selector specificity rules.
Common Mistakes
1. Using Old Plugin API for Variants
The v3 addVariant function does not work in v4's CSS-first config. Use @variant directive in CSS instead.
2. Missing & in @variant Selectors
@variant open (&:where([open])) is correct. Without &, the selector does not reference the current element.
3. Circular Variant References
A variant that references itself creates an infinite loop. Ensure @variant definitions do not include themselves.
4. Complex Selectors in Variants
Keep variant selectors simple. Overly complex selectors in @variant can cause specificity issues.
5. Forgetting Variants Need Build
New @variant definitions require a rebuild to take effect. HMR may not pick up @variant changes in all setups.
Practice Questions
How do you define a custom variant in v4? Using @variant in CSS:
@variant open (&:where([open])).What is the unified variant syntax? All variants use the
variant:utilityprefix pattern. hover:, dark:, custom: all follow the same rules.How do you compose variants? Use CSS nesting inside @variant:
@variant desktop-hover (@media (hover: hover) { &:hover; }).Can variants accept parameters? Yes:
@variant min-width($size) (@media (min-width: $size));withmin-width-[600px]:flexusage.How do variants handle specificity? Each variant level adds specificity. More specific variant combinations override less specific ones.
Challenge
Define 5 custom variants using @variant: open (for details/summary), loading (for data-loading attribute), print (for @media print), focus-within, and sibling-checked. Use each in a component.
FAQ
Mini Project
Build an interactive component set using custom @variant definitions: an accordion (open: variant), a loading button (loading: variant), a print-friendly table (print: variant), and a landscape-optimized dashboard (landscape: variant).
What's Next
Now master Composing Variants for complex state combinations. Then explore Custom Variants for advanced selector patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro