Skip to content

Tailwind CSS v4 Variants — Unified Variant System

DodaTech Updated 2026-06-28 5 min read

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

  1. How do you define a custom variant in v4? Using @variant in CSS: @variant open (&:where([open])).

  2. What is the unified variant syntax? All variants use the variant:utility prefix pattern. hover:, dark:, custom: all follow the same rules.

  3. How do you compose variants? Use CSS nesting inside @variant: @variant desktop-hover (@media (hover: hover) { &:hover; }).

  4. Can variants accept parameters? Yes: @variant min-width($size) (@media (min-width: $size)); with min-width-[600px]:flex usage.

  5. 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

Do all v3 variants work in v4?

Yes. hover:, focus:, active:, dark:, md:, lg:, group-hover:, peer:, motion-reduce: all work identically.

Can I use @variant with @apply?

Yes. @apply supports variant prefixes: @apply hover:bg-blue-700 focus:ring-2;

How do I debug a variant that is not applying?

Check selector specificity. Verify the variant matches the element. Rebuild the CSS.

Can I override built-in variants?

Use @variant with the same name to override: @variant hover (&:hover) { /* custom */ }

Do custom variants work with IntelliSense?

Standard variants work. Custom variants may need IDE extension updates for autocomplete.

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