Skip to main content

Overview

Tailwind CSS provides two ways to create custom variants: @custom-variant for defining new variants and @variant for applying existing variants within custom utilities or other contexts.

@custom-variant Directive

The @custom-variant directive allows you to create entirely new variants that can be used throughout your project.

Body-less Syntax

The simplest way to define a variant is using the body-less syntax with a selector list.

Selector Variants

@custom-variant hocus (&:hover, &:focus);
<button class="hocus:bg-blue-500">
  Blue on hover or focus
</button>
This creates a hocus: variant that applies styles when the element is hovered or focused.
The & symbol represents the element the utility is applied to. Multiple selectors are comma-separated.

At-Rule Variants

@custom-variant any-hover (@media (any-hover: hover));
<div class="any-hover:hover:underline">
  Only underlined if device supports hover
</div>

Mixed Selectors and At-Rules

You can combine both selector and at-rule conditions:
@custom-variant cant-hover (
  &:not(:hover),
  &:not(:active),
  @media not (any-hover: hover),
  @media not (pointer: fine)
);
<button class="cant-hover:focus:underline">
  Underlined on focus (for devices without hover)
</button>

Body with @slot Syntax

For more complex variants, use the body syntax with @slot to mark where utilities should be inserted.

Basic @slot Usage

@custom-variant selected {
  &[data-selected] {
    @slot;
  }
}
<div class="selected:bg-blue-500" data-selected>
  Selected state styling
</div>

Nested Structures

@custom-variant custom-before {
  &::before {
    content: "";
    @slot;
  }
}
<div class="custom-before:bg-red-500">
  Pseudo-element will have red background
</div>

Using @variant Inside Definitions

You can compose variants by using @variant within @custom-variant bodies:
@custom-variant hocus {
  @variant hover {
    @variant focus {
      @slot;
    }
  }
}
This creates a variant that applies on both hover AND focus (different from the comma-separated version which is OR).
Be careful not to create circular dependencies when composing variants. The following would cause an error:
@custom-variant foo {
  @variant bar { @slot; }
}

@custom-variant bar {
  @variant foo { @slot; }
}

@variant Directive

The @variant directive applies an existing variant within custom utility or variant definitions.

In Custom Utilities

Apply variants directly in utility definitions:
@utility fancy-text {
  color: blue;
  
  @variant dark {
    color: lightblue;
  }
}
<p class="fancy-text">Blue text</p>
<p class="fancy-text" data-theme="dark">Light blue in dark mode</p>

Composing Multiple Variants

Nest @variant directives to combine conditions:
@utility responsive-button {
  padding: 0.5rem;
  
  @variant md {
    padding: 1rem;
    
    @variant hover {
      padding: 1.25rem;
    }
  }
}

Compound Variants

Tailwind includes compound variants that modify how other variants work.

group Variant

The group compound variant is implemented in the source code:
variants.compound('group', Compounds.StyleRules, (ruleNode, variant) => {
  // Name the group with optional modifier
  let variantSelector = variant.modifier
    ? `:where(.group\/${variant.modifier.value})`
    : `:where(.group)`
  
  // Apply variant to descendant when ancestor has group class
  node.selector = `&:is(${selector} *)`
})
Usage:
<div class="group">
  <button class="group-hover:bg-blue-500">
    Blue on group hover
  </button>
</div>

<div class="group/sidebar">
  <button class="group-hover/sidebar:visible">
    Named group
  </button>
</div>

peer Variant

Similar to group, but for sibling elements:
<input type="checkbox" class="peer" />
<label class="peer-checked:text-blue-500">
  Blue when checkbox is checked
</label>

not Variant

The not variant negates another variant:
<div class="not-hover:opacity-50">
  Faded unless hovered
</div>

in Variant

Applies styles when the element is inside another element:
<div class="in-hover:text-blue-500">
  Blue when any ancestor is hovered
</div>

has Variant

Applies styles when the element contains a matching child:
<div class="has-hover:border-blue-500">
  <button>Hover me to change parent border</button>
</div>

Variant Ordering

Variants have a specific order that determines their precedence:
// From variants.ts:203-270
compare(a: Variant | null, z: Variant | null): number {
  // Order by registration order first
  let aOrder = this.variants.get(a.root)!.order
  let zOrder = this.variants.get(z.root)!.order
  
  let orderedByVariant = aOrder - zOrder
  if (orderedByVariant !== 0) return orderedByVariant
  
  // Then by value
  // Arbitrary values come last
  // Named values are sorted alphabetically
}
Variants are applied in the order they’re defined in the framework, not the order they appear in your class names.

Advanced Variant Features

Functional Variants

Functional variants accept values:
@custom-variant aria-* (&[aria-*="true"]);
@custom-variant data-* (&[data-*]);
@custom-variant nth-* (&:nth-child(*));
<button class="aria-pressed:bg-blue-500" aria-pressed="true">
  Pressed state
</button>

<div class="data-active:font-bold" data-active>
  Active item
</div>

<li class="nth-3:text-red-500">
  Third item is red
</li>

Custom Dark Mode Variant

Override the default dark mode implementation:
@custom-variant dark (&:is([data-theme='dark'] *));
<div data-theme="dark">
  <p class="dark:text-white">White text in custom dark mode</p>
</div>

Breakpoint-like Variants

@custom-variant desktop (@media (min-width: 1280px));
<div class="desktop:grid-cols-4">
  Four columns on desktop
</div>

Rules and Constraints

Important Rules:
  • @custom-variant must be defined at the top level (not nested)
  • Variant names must be alphanumeric, may contain dashes/underscores
  • Variant names must start with a lowercase letter or number
  • Variant names cannot end with a dash or underscore
  • Cannot have both a selector list and a body
  • Must have either a selector list or a body with @slot

Invalid Examples

/* ❌ Nested variant */
.foo {
  @custom-variant bar (&:hover);
}

/* ❌ No selector or body */
@custom-variant foo;

/* ❌ Both selector and body */
@custom-variant foo (&:hover) {
  @slot;
}

/* ❌ Empty selector */
@custom-variant foo ();

/* ❌ Invalid name */
@custom-variant foo:bar (&:hover);

/* ❌ Starting with dash */
@custom-variant -foo (&:hover);

/* ❌ Ending with dash */
@custom-variant foo- (&:hover);

Best Practices

Recommendations:
  1. Use descriptive names that clearly indicate when the variant applies
  2. Prefer body-less syntax for simple variants
  3. Use @slot syntax for complex structural variants
  4. Test variants with compound variants like group- and peer-
  5. Document complex variants with comments explaining their behavior
  6. Avoid creating variants that conflict with core variants

Source Code Reference

The variant system is implemented in:
  • /packages/tailwindcss/src/variants.ts:39-317 - Variants class
  • /packages/tailwindcss/src/variants.ts:348-1163 - createVariants function
  • /packages/tailwindcss/src/variants.ts:86-110 - fromAst method for @custom-variant
  • /packages/tailwindcss/src/variants.ts:1210-1235 - substituteAtVariant function

Build docs developers (and LLMs) love