Skip to content

Tailwind CSS Variants — Responsive, State, and Custom Variants

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Tailwind CSS Variants. We cover key concepts, practical examples, and best practices to help you master this topic.

Tailwind CSS variants are prefix modifiers that conditionally apply utility classes based on responsive breakpoints, interaction states, dark mode, group state, and custom conditions.

What You'll Learn

You will learn how variants work in Tailwind, available variant categories, how to combine multiple variants, and how to create custom variants for project-specific conditions.

Why It Matters

Variants eliminate conditional CSS. DodaTech uses variants for every interactive element -- hover for desktop, focus for keyboard, dark for theme, and md/lg for responsive adjustments.

Real-World Use

Durga Antivirus Pro uses combined responsive-state variants like md:hover:bg-gray-100 and dark:focus:ring-blue-400 throughout the dashboard for comprehensive interaction feedback.

flowchart LR
    A[Directives] --> B[Variants]
    B --> C[Responsive]
    B --> D[State]
    B --> E[Dark Mode]
    B --> F[Group/Peer]
    B --> G[Custom]
    style B fill:#38bdf8,stroke:#0284c7,color:#fff
    style C fill:#22c55e,stroke:#16a34a,color:#fff

Variant Categories

<div class="p-8 space-y-6">
  <!-- Responsive variants -->
  <div class="text-sm md:text-base lg:text-lg xl:text-xl">
    Responsive: size changes at each breakpoint
  </div>

  <!-- State variants -->
  <button class="bg-blue-600 text-white px-6 py-3 rounded-lg
                 hover:bg-blue-700 focus:ring-2 focus:ring-blue-500
                 active:bg-blue-800 disabled:opacity-50">
    State variants
  </button>

  <!-- Dark mode -->
  <div class="bg-white dark:bg-gray-800 p-4 rounded-lg">
    <p class="text-gray-900 dark:text-white">Dark mode variant</p>
  </div>

  <!-- Group hover -->
  <div class="group cursor-pointer border p-4 rounded-lg hover:shadow-md">
    <h3 class="text-gray-900 group-hover:text-blue-600">Group hover title</h3>
  </div>
</div>

Expected output: Elements responding to viewport size, hover, focus, dark mode, and parent hover -- all via variant prefixes.

Combining Multiple Variants

<button class="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium
               hover:bg-blue-700
               dark:bg-blue-500 dark:hover:bg-blue-400
               md:text-lg
               md:hover:shadow-lg
               dark:focus:ring-blue-300
               transition-all duration-200">
  Combined Variants Button
</button>

Expected output: A button that changes background on hover, adapts for dark mode, is larger on desktop, has a shadow on desktop hover, and a custom focus ring in dark mode -- all with combined variants.

Using Variants with @apply

/* custom.css */
.btn {
  @apply bg-blue-600 text-white font-medium py-2 px-4 rounded-lg
         hover:bg-blue-700
         focus:ring-2 focus:ring-blue-500
         dark:bg-blue-500 dark:hover:bg-blue-400
         md:text-base lg:text-lg
         transition-colors;
}

Expected output: A .btn class that includes variant-based styles via @apply, reducing repetition when reusing the same variant combinations.

Variant Order

<div class="p-4 bg-gray-100 hover:bg-blue-100 lg:hover:bg-purple-100 dark:lg:hover:bg-purple-800">
  <!-- Variant order: responsive > state > custom -->
  <!-- Applied in order: base -> hover -> lg -> lg:hover -> dark -> dark:lg -> dark:lg:hover -->
  <p>Hover colors change based on viewport and theme.</p>
</div>

Expected output: The background color cascades through variant layers. Dark mode overrides light. Responsive overrides base. State overrides responsive.

Custom Variants

// tailwind.config.js
const plugin = require('tailwindcss/plugin');

module.exports = {
  plugins: [
    plugin(function({ addVariant }) {
      // Open/closed state for details/summary elements
      addVariant('open', 'details[open] &');

      // Loading state
      addVariant('loading', '&[data-loading="true"]');

      // Print media
      addVariant('print', '@media print');

      // Landscape orientation
      addVariant('landscape', '@media (orientation: landscape)');
    }),
  ],
}
<details class="open:bg-blue-50 open:p-4 open:rounded-lg">
  <summary class="cursor-pointer font-medium">Click to expand</summary>
  <p class="mt-2 text-gray-600">This content is revealed on open.</p>
</details>

<div data-loading="true" class="loading:opacity-50 loading:cursor-wait">
  Content with loading state
</div>

<div class="landscape:flex landscape:gap-4">
  <div class="bg-blue-100 p-4">Side by side in landscape</div>
  <div class="bg-green-100 p-4">Side by side in landscape</div>
</div>

Expected output: Custom variants for open state, loading state, landscape orientation, and print media are available as prefix modifiers.

Peer Variants

<div class="space-y-4">
  <!-- Peer: style one element based on sibling state -->
  <div class="flex items-center gap-3">
    <input type="checkbox" class="peer w-4 h-4 text-blue-600">
    <label class="text-gray-700 peer-checked:text-blue-600 peer-checked:font-medium">
      Check me to see the peer effect
    </label>
  </div>

  <!-- Peer with hover -->
  <div class="flex gap-4">
    <div class="peer/hover w-24 h-24 bg-blue-100 rounded-lg flex items-center justify-center cursor-pointer hover:bg-blue-200">
      Hover
    </div>
    <div class="peer-hover/hover:bg-blue-50 w-24 h-24 bg-gray-100 rounded-lg flex items-center justify-center">
      Reacts
    </div>
  </div>
</div>

Expected output: The label text changes color when the checkbox is checked. The second box changes background when the first box is hovered.

Common Mistakes

1. Too Many Combined Variants

dark:md:hover:focus:bg-red-100 is hard to read and maintain. Keep variant chains to 2-3 prefixes maximum.

2. Forgetting Base Style

:hover only works if there is a base style to override. hover:bg-blue-700 needs bg-blue-600 as the base.

3. Wrong Variant Order

Variant order matters: hover:md:bg-red is different from md:hover:bg-red. The leftmost variant has higher specificity.

4. Not Using Peer When Needed

Peer variants style one element based on another's state. Without peer, you need JavaScript for cross-element state styling.

5. Overriding Dark Mode Variants

Dark mode variants must be explicitly added. Without dark:bg-gray-800, the element keeps its light background in dark mode.

Practice Questions

  1. What is the syntax for variants? {variant}:utility — e.g., hover:bg-blue-700, md:text-lg, dark:text-white.

  2. How many variants can be combined? Any number, but 2-3 is the practical limit for readability.

  3. What does the peer variant do? Styles an element based on the state of a preceding sibling with the peer class.

  4. What is variant order precedence? Rightmost variant has highest specificity: dark:hover:bg-blue-400 applies in dark mode on hover.

  5. Can you create variants for custom data attributes? Yes. Use addVariant in a plugin: addVariant('data-active', '&[data-active="true"]').

Challenge

Create a form with: peer-checked styling for toggle switches, group-hover for card reveal effects, custom data-loading variant for submit buttons, and combined md:hover variants for desktop-only interactions.

FAQ

How many default variants does Tailwind have?

Around 30 including responsive (5), state (hover, focus, active, etc.), dark, group, peer, and motion preferences.

Can variants be used with any utility?

Most utilities support all variants. Some utilities like static layout classes may not benefit from hover variants.

How are variants compiled?

The JIT engine generates variant classes on demand. 'hover:bg-blue-700' compiles to .hover:bg-blue-700:hover { ... }

Do variants increase CSS size?

Only used variants are generated by the JIT engine. Unused variant combinations do not appear in the output.

Can I disable unnecessary variants?

Yes. In the config, set variants: { extend: { backgroundColor: ['hover', 'focus'] } } to limit which variants are generated.

Mini Project

Build an interactive dashboard widget with: hover for row highlighting, peer-checked for toggle switches, group-hover for card actions, focus for keyboard inputs, dark for all elements, and responsive for layout changes.

What's Next

Now master Pseudo-Classes for styling based on element state and position. Then explore Animations for motion and transitions.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro