---
title: "Tailwind v4: Variants and State Modifiers"
slug: "tailwind-variants"
category: "frontend"
tags: ["tailwind", "css", "frontend", "variants", "state", "responsive", "has-selector"]
status: "stable"
last_updated: 2026-05-14
summary: "Master group-*, peer-*, has-*, supports-*, data-*, and arbitrary variants to style any state without touching JavaScript."
related: ["[[frontend/tailwind]]", "[[frontend/css]]", "[[frontend/css-has-selector]]", "[[frontend/tailwind-responsive]]", "[[frontend/tailwind-dark-mode]]"]
---

## Overview

Tailwind variants are prefixes that scope a utility to a CSS condition. `hover:bg-brand` means `bg-brand` only when the element is hovered. v4 ships `group-*`, `peer-*`, `has-*`, `supports-*`, and `data-*` as first-class variants, plus an escape hatch for any CSS selector or at-rule. The parent reference is [[frontend/tailwind]].

## Use `group-*` to style children based on parent state

Mark the parent with `group` and style descendants with `group-hover:`, `group-focus:`, `group-aria-expanded:`, or any other state.

```html
<div class="group rounded-card p-4 hover:shadow-card">
  <h2 class="text-ink group-hover:text-brand transition-colors">Card title</h2>
  <p class="text-ink/60 group-hover:text-ink/80">Description</p>
</div>
```

Name groups when nesting: `group/outer` and `group/inner`. Use `group-hover/outer:` on a deeply nested child to target the specific ancestor.

```html
<div class="group/card">
  <div class="group/section">
    <span class="group-hover/card:text-brand group-hover/section:underline">
      Nested label
    </span>
  </div>
</div>
```

## Use `peer-*` to style siblings based on sibling state

`peer` marks an element as the source; `peer-checked:`, `peer-focus:`, and `peer-invalid:` style a following sibling.

```html
<input id="toggle" type="checkbox" class="peer sr-only" />
<label for="toggle" class="cursor-pointer peer-checked:text-brand">
  Enable notifications
</label>
```

Peers must precede their targets in the DOM. A `peer-*` variant on an element that precedes the `peer` source has no effect. Name peers the same way you name groups: `peer/input` and `peer-focus/input:`.

## Use `has-*` to style parents based on child state

`has-*` is the CSS `:has()` selector wrapped as a variant. Style a container when it contains a specific element or state.

```html
<div class="has-[input:focus]:ring-2 has-[input:focus]:ring-brand rounded-md">
  <input type="text" placeholder="Search" />
</div>
```

The outer `div` gains a ring when any `input` inside it is focused. No JavaScript state management needed. For the deeper CSS reference, see [[frontend/css-has-selector]].

Combine with other variants: `has-[.error]:border-danger` styles a field group red when it contains an error message.

## Use `supports-*` to progressively enhance for modern features

`supports-*` maps to CSS `@supports`. Apply a utility only when the browser supports a given property or value.

```html
<div class="grid supports-[container-type:inline-size]:container-query-layout">
```

Use `supports-*` sparingly. Most modern CSS features have shipped in every evergreen browser. Reach for it when you need a meaningful fallback for a genuinely experimental feature like scroll-driven animations or `@starting-style`.

## Use `data-*` to style based on custom data attributes

`data-*` variants match `data-*` attributes on the element.

```html
<button data-variant="primary" class="data-[variant=primary]:bg-brand data-[variant=primary]:text-white">
  Submit
</button>
```

Pair with JavaScript that sets `data-state="open"` / `data-state="closed"` (the pattern Radix UI and similar libraries use) to drive visible state from semantic attributes instead of className toggling.

```html
<div data-state="open" class="hidden data-[state=open]:block">Panel content</div>
```

## Write arbitrary variants for one-off CSS selectors

Use `[...]` to write an arbitrary CSS selector or at-rule as a variant when no built-in variant fits.

```html
<p class="[&:nth-child(3)]:font-bold [&>strong]:text-brand">
```

The `&` is the placeholder for the element itself, matching CSS nesting syntax. Use this for pseudo-classes that Tailwind does not expose as named variants (`nth-child`, `not`, `is`), for attribute selectors, or for scoped overrides inside a third-party component.

## Order variants consistently; stack them left to right

Multiple variants stack left to right in class name order. The rightmost element is the base utility.

```html
<button class="sm:hover:dark:bg-brand-900">
```

This reads: on small-and-up viewports, when hovered, in dark mode, use `bg-brand-900`. The Prettier plugin for Tailwind sorts variants in a canonical order that matches this mental model. Install it; do not debate order in review. See [[frontend/tailwind-responsive]] for breakpoint variant ordering.

## Prefer semantic variants over JavaScript state syncing

Replace boolean class toggling in JavaScript with data attributes and `data-*` variants wherever possible. The result is fewer state variables, fewer re-renders in [[frontend/react]], and a component whose visual states are readable from the HTML alone.

## Related

- [[frontend/tailwind]]
- [[frontend/css]]
- [[frontend/css-has-selector]]
- [[frontend/tailwind-responsive]]
- [[frontend/tailwind-dark-mode]]
- [[frontend/tailwind-arbitrary-values]]
- [[frontend/react]]
