|
| 1 | +--- |
| 2 | +name: tailwind-syntax-variants |
| 3 | +description: > |
| 4 | + Use when applying conditional styles in Tailwind: hover, focus, active, dark mode, |
| 5 | + responsive breakpoints, group-* and peer-* combinators, aria-* and data-* attribute |
| 6 | + variants, has-* and not-* selectors, position-in-parent variants (first, last, *, |
| 7 | + nth-*), or arbitrary [&...] variants. Use also when stacking multiple variants |
| 8 | + (`md:hover:dark:bg-blue-500`) and the resulting selector does not match the intended |
| 9 | + element. Use also when migrating variant chains from Tailwind v3 to v4: the stacking |
| 10 | + order flipped from right-to-left to left-to-right and is the #1 silent breakage |
| 11 | + during upgrade. |
| 12 | + Prevents the common mistakes of (a) reading a v3 stack like `first:*:pt-0` and |
| 13 | + expecting the same compiled selector in v4 (it is now `*:first:pt-0`), (b) writing |
| 14 | + hover variants that mysteriously stop working on touch devices in v4 (hover is now |
| 15 | + gated on `@media (hover: hover)`), (c) writing `!flex` important syntax that fails |
| 16 | + silently in v4 (the bang moved to the trailing position: `flex!`), (d) using |
| 17 | + unscoped `group-hover:` when a named group `group/sidebar` is required for nested |
| 18 | + group hierarchies, (e) trying `not-hover:opacity-75` in v3 (`not-*` is v4-only), and |
| 19 | + (f) registering custom variants via `addVariant()` in a v4 project (use `@custom-variant` |
| 20 | + instead). |
| 21 | + Covers every variant family in v3.4 and v4: pseudo-class (hover, focus, focus-within, |
| 22 | + focus-visible, active, visited, target, first, last, odd, even, nth-*, first-of-type, |
| 23 | + empty, disabled, enabled, checked, indeterminate, required, valid, invalid, autofill, |
| 24 | + read-only, open, inert), pseudo-element (before, after, placeholder, file, marker, |
| 25 | + selection, first-line, first-letter, backdrop, details-content, popover-open), |
| 26 | + media-query (sm, md, lg, xl, 2xl, max-*, min-[Npx]:, max-[Npx]:, dark, light, |
| 27 | + motion-safe, motion-reduce, contrast-more, contrast-less, forced-colors, portrait, |
| 28 | + landscape, print, screen, ltr, rtl), feature-query (supports-*, not-supports-*), |
| 29 | + attribute (aria-* and aria-[...], data-* and data-[...]), combinator (group-*, |
| 30 | + peer-*, named groups `group/name`, named peers `peer/name`, has-*, not-*, in-*, |
| 31 | + starting:*, group-has-*, peer-has-*), position-in-parent (*, **, *:first, *:last, |
| 32 | + *:odd, *:even, nth-N, nth-[3n+1]), and arbitrary variants ([&:nth-child(3)], |
| 33 | + [&_p], [@media(...)], [@supports(...)]). Includes the v4 stacking-order flip with |
| 34 | + migration examples, the `@custom-variant` directive (v4) vs `addVariant()` plugin |
| 35 | + (v3), the trailing-bang important syntax change, and the hover-on-touch gate. |
| 36 | + Keywords: variant, hover, focus, active, dark mode, group-hover, peer-checked, |
| 37 | + named group, group/sidebar, peer/name, aria-checked, data-state, has-checked, |
| 38 | + not-hover, in-focus, starting, first child, last child, nth-child, odd even, |
| 39 | + before after, placeholder, file input, marker, selection, backdrop, motion-reduce, |
| 40 | + print only, rtl, arbitrary variant, stacking order, left to right, right to left, |
| 41 | + variant order flipped, why does my hover not work on mobile, important modifier, |
| 42 | + bang flex, flex bang, custom variant, addVariant, @custom-variant, supports query, |
| 43 | + feature query, why does first:*: not work in v4. |
| 44 | +license: MIT |
| 45 | +compatibility: "Designed for Claude Code. Requires Tailwind CSS v3.4 or v4.0+." |
| 46 | +metadata: |
| 47 | + author: OpenAEC-Foundation |
| 48 | + version: "1.0" |
| 49 | +--- |
| 50 | + |
| 51 | +# Tailwind CSS Syntax: Variants |
| 52 | + |
| 53 | +Variants are the conditional prefixes that turn a flat utility (`bg-blue-500`) into a state-aware rule (`hover:bg-blue-500`, `md:dark:hover:bg-blue-500`). Tailwind defines around 100 built-in variants spread across ten families plus arbitrary `[&...]` escape hatches. Master this and you master 90% of real-world Tailwind markup. |
| 54 | + |
| 55 | +## Quick Reference |
| 56 | + |
| 57 | +### Variant families at a glance |
| 58 | + |
| 59 | +| Family : signature : v3 : v4 | |
| 60 | +|--| |
| 61 | +| Pseudo-class : `hover:`, `focus:`, `active:` : same : same (but `hover:` gated on `@media (hover: hover)`) | |
| 62 | +| Pseudo-element : `before:`, `placeholder:` : same : same (+ `details-content:`, recognises `popover-open`) | |
| 63 | +| Media query : `sm:`, `dark:`, `motion-reduce:`, `print:` : same : same (custom breakpoints via `--breakpoint-*` instead of `theme.screens`) | |
| 64 | +| Feature query : `supports-[display:grid]:` : same : same + `not-supports-*:` | |
| 65 | +| Attribute : `aria-checked:`, `data-[state=open]:` : same : same | |
| 66 | +| Combinator : `group-*`, `peer-*`, named `group/sidebar` : same : same + `has-*`, `not-*`, `in-*`, `peer-has-*`, `group-has-*` | |
| 67 | +| Position-in-parent : `first:`, `last:`, `odd:`, `*:` : same | same + parameterised `nth-[3n+1]:`, `nth-last-*:`, `**` (deep descendants) | |
| 68 | +| Arbitrary : `[&:nth-child(3)]:`, `[@media(...)]:` : same : same (parens for CSS-vars : `[&_p]:bg-(--brand)`) | |
| 69 | +| Important modifier : leading `!flex` (v3) : same (v3) : trailing `flex!` (v4) | |
| 70 | +| Custom variant : `addVariant()` plugin (v3) : JS plugin | `@custom-variant name (&:selector);` CSS directive | |
| 71 | + |
| 72 | +### Top 3 most-used patterns |
| 73 | + |
| 74 | +```html |
| 75 | +<!-- 1. State + responsive + dark stack (works v3 + v4) --> |
| 76 | +<button class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 md:px-8"> |
| 77 | + Save |
| 78 | +</button> |
| 79 | + |
| 80 | +<!-- 2. Named group for nested cards (works v3 + v4) --> |
| 81 | +<article class="group/card hover:shadow-lg"> |
| 82 | + <h3 class="text-slate-900 group-hover/card:text-blue-600">Title</h3> |
| 83 | + <button class="group/edit invisible group-hover/card:visible"> |
| 84 | + <span class="group-hover/edit:underline">Edit</span> |
| 85 | + </button> |
| 86 | +</article> |
| 87 | + |
| 88 | +<!-- 3. Arbitrary variant for one-off selectors --> |
| 89 | +<ul class="[&_li]:list-disc [&_li:last-child]:mb-0"> |
| 90 | + <li>One</li><li>Two</li> |
| 91 | +</ul> |
| 92 | +``` |
| 93 | + |
| 94 | +## Decision Trees |
| 95 | + |
| 96 | +### Picking the right variant family |
| 97 | + |
| 98 | +``` |
| 99 | +state I care about? |
| 100 | +├── interaction state (hover, focus, active, visited, target) |
| 101 | +│ └── pseudo-class : `hover:`, `focus:`, ... |
| 102 | +├── form/input state (checked, indeterminate, disabled, required, invalid, read-only) |
| 103 | +│ └── pseudo-class : `checked:`, `disabled:`, `invalid:`, ... |
| 104 | +├── position in parent (first child, last child, odd, every-3rd) |
| 105 | +│ ├── known position : `first:`, `last:`, `odd:`, `even:` |
| 106 | +│ └── arbitrary position : `nth-[3n+1]:` (v4) or `[&:nth-child(3n+1)]:` (v3 + v4) |
| 107 | +├── parent or sibling state? |
| 108 | +│ ├── one level of nesting : `group-hover:`, `peer-checked:` |
| 109 | +│ ├── multiple nested groups : NAMED `group/sidebar` + `group-hover/sidebar:` |
| 110 | +│ └── ancestor-with-attribute without `group` class : `in-[role=tabpanel]:` (v4) |
| 111 | +├── presence of descendant with a specific state? |
| 112 | +│ └── `has-[input:checked]:` (v4), or `group-has-checked:` if going up first |
| 113 | +├── negate any other variant? |
| 114 | +│ ├── v4 : `not-hover:`, `not-supports-grid:` |
| 115 | +│ └── v3 : NOT available ; use arbitrary `[&:not(:hover)]:` |
| 116 | +├── HTML attribute value match (aria-* or data-*)? |
| 117 | +│ ├── known value : `aria-checked:`, `data-active:` |
| 118 | +│ └── arbitrary : `aria-[sort=ascending]:`, `data-[state=open]:` |
| 119 | +├── browser capability test? |
| 120 | +│ └── `supports-[display:grid]:`, `not-supports-[backdrop-filter]:` (v4) |
| 121 | +├── viewport size or device preference? |
| 122 | +│ ├── breakpoint : `sm:`, `md:`, `max-md:`, `min-[400px]:` |
| 123 | +│ ├── color scheme : `dark:`, `light:` |
| 124 | +│ ├── motion : `motion-reduce:`, `motion-safe:` |
| 125 | +│ ├── contrast : `contrast-more:`, `contrast-less:` |
| 126 | +│ ├── pointer : `pointer-fine:`, `pointer-coarse:` (v4) |
| 127 | +│ ├── reading direction : `ltr:`, `rtl:` |
| 128 | +│ └── medium : `print:`, `screen:` |
| 129 | +└── nothing built-in fits? |
| 130 | + └── arbitrary variant : `[&:nth-child(odd)]:`, `[@media(prefers-reduced-data:reduce)]:` |
| 131 | +``` |
| 132 | + |
| 133 | +### Picking direction for stacking variants (CRITICAL) |
| 134 | + |
| 135 | +``` |
| 136 | +Tailwind major version? |
| 137 | +├── v3.4 : read RIGHT-TO-LEFT |
| 138 | +│ └── `first:*:pt-0` means : direct children (`*`), then first one (`first`) |
| 139 | +│ └── selector : `:first-child > *` (incorrect intuition warning) |
| 140 | +└── v4.0+ : read LEFT-TO-RIGHT |
| 141 | + └── `*:first:pt-0` means : direct children (`*`), then first one (`first`) |
| 142 | + └── selector : `> :first-child` (matches CSS reading order) |
| 143 | +``` |
| 144 | + |
| 145 | +The order **flipped** between v3 and v4. The upgrade tool `npx @tailwindcss/upgrade` catches most cases, but bespoke stacks involving `*:`, structural variants, or arbitrary `[&...]` need manual review. |
| 146 | + |
| 147 | +### Picking how to add a custom variant |
| 148 | + |
| 149 | +``` |
| 150 | +project Tailwind major version? |
| 151 | +├── v3.4 (JavaScript plugin) |
| 152 | +│ └── `plugin(({ addVariant }) => { addVariant('third', '&:nth-child(3)') })` |
| 153 | +└── v4.0+ (CSS directive) |
| 154 | + └── `@custom-variant third (&:nth-child(3));` |
| 155 | +``` |
| 156 | + |
| 157 | +`addVariant()` from v3 is REMOVED in v4. If you encounter a v3 plugin shipping `addVariant` calls, convert each one into a `@custom-variant` directive during migration. |
| 158 | + |
| 159 | +## Patterns |
| 160 | + |
| 161 | +### Pattern 1: Stacking variants (v3 vs v4) |
| 162 | + |
| 163 | +This is the most consequential v3-to-v4 change. NEVER mix the two mental models. |
| 164 | + |
| 165 | +| v3.4 right-to-left | v4.0+ left-to-right | |
| 166 | +|--------------------|----------------------| |
| 167 | +| ```html |
| 168 | +<!-- v3 syntax --> |
| 169 | +<ul class="py-4 first:*:pt-0 last:*:pb-0"> |
| 170 | + <li>One</li> |
| 171 | + <li>Two</li> |
| 172 | +</ul> |
| 173 | +<!-- Read right-to-left : |
| 174 | + pt-0 -> apply to first -> then to direct children (*) |
| 175 | + Compiles to : ul > :first-child { padding-top: 0; } |
| 176 | +--> |
| 177 | +``` | ```html |
| 178 | +<!-- v4 syntax --> |
| 179 | +<ul class="py-4 *:first:pt-0 *:last:pb-0"> |
| 180 | + <li>One</li> |
| 181 | + <li>Two</li> |
| 182 | +</ul> |
| 183 | +<!-- Read left-to-right : |
| 184 | + direct children (*) -> first one -> pt-0 |
| 185 | + Compiles to : ul > :first-child { padding-top: 0; } |
| 186 | +--> |
| 187 | +``` | |
| 188 | +
|
| 189 | +Both produce the same CSS, but the WRITTEN order is reversed. The migration tool covers `first:*:`, `last:*:`, `odd:*:`, `even:*:`. Manually inspect any handwritten stacks containing `*:`, `**:`, or arbitrary `[&...]` after a long-chained variant. |
| 190 | +
|
| 191 | +### Pattern 2: Named groups for nested hierarchies |
| 192 | +
|
| 193 | +Unnamed `group-hover:` always targets the NEAREST `.group` ancestor. When components nest groups, this becomes ambiguous. Named groups make the binding explicit. |
| 194 | +
|
| 195 | +```html |
| 196 | +<!-- BROKEN : both inner and outer use unnamed group ; group-hover ambiguity --> |
| 197 | +<article class="group"> |
| 198 | + <button class="group"> |
| 199 | + <span class="group-hover:underline">Which group?</span> |
| 200 | + </button> |
| 201 | +</article> |
| 202 | +
|
| 203 | +<!-- FIXED : named groups --> |
| 204 | +<article class="group/card hover:shadow-lg"> |
| 205 | + <button class="group/edit"> |
| 206 | + <span class="group-hover/edit:underline">Hovering the button</span> |
| 207 | + <span class="group-hover/card:text-blue-600">Hovering the card</span> |
| 208 | + </button> |
| 209 | +</article> |
| 210 | +``` |
| 211 | + |
| 212 | +The same pattern applies to peers : `<input class="peer/email">` then `<p class="peer-invalid/email:text-red-500">`. |
| 213 | + |
| 214 | +### Pattern 3: Attribute variants (aria-* and data-*) |
| 215 | + |
| 216 | +```html |
| 217 | +<!-- ARIA boolean states (built-in shortcuts) --> |
| 218 | +<div aria-checked="true" class="aria-checked:bg-sky-100">Checkbox row</div> |
| 219 | +<button aria-pressed="true" class="aria-pressed:ring-2">Toggle</button> |
| 220 | +<button aria-disabled="true" class="aria-disabled:opacity-50">Disabled-styled</button> |
| 221 | + |
| 222 | +<!-- ARIA arbitrary values (any attribute value) --> |
| 223 | +<th aria-sort="ascending" class="aria-[sort=ascending]:bg-[url('/down-arrow.svg')]">Name</th> |
| 224 | + |
| 225 | +<!-- data-* known shortcuts (active, state, etc.) --> |
| 226 | +<div data-active class="data-active:border-purple-500">Selected</div> |
| 227 | + |
| 228 | +<!-- data-* arbitrary values (most common with Radix UI / Headless UI / state machines) --> |
| 229 | +<div data-state="open" class="data-[state=open]:rotate-180">Chevron</div> |
| 230 | +<div data-side="bottom" class="data-[side=bottom]:slide-in-from-top-2">Tooltip</div> |
| 231 | +``` |
| 232 | + |
| 233 | +These are essential when integrating Radix UI, Headless UI, or any framework that sets `data-state="open|closed"`, `data-side="top|right|bottom|left"`, or similar attributes. |
| 234 | + |
| 235 | +### Pattern 4: Position-in-parent variants |
| 236 | + |
| 237 | +```html |
| 238 | +<!-- Direct children (* prefix) : style only DIRECT children --> |
| 239 | +<ul class="*:rounded-full *:border *:px-2 *:py-1"> |
| 240 | + <li>Pill</li> |
| 241 | + <li>Pill</li> |
| 242 | +</ul> |
| 243 | + |
| 244 | +<!-- Deep descendants (** prefix) : v4 only --> |
| 245 | +<ul class="**:data-avatar:size-12"> |
| 246 | + <li><img data-avatar src="..." /></li> |
| 247 | + <li><img data-avatar src="..." /></li> |
| 248 | +</ul> |
| 249 | + |
| 250 | +<!-- Structural pseudo-classes --> |
| 251 | +<ul> |
| 252 | + <li class="first:pt-0 last:pb-0 only:py-0">...</li> |
| 253 | + <li class="odd:bg-white even:bg-slate-50">...</li> |
| 254 | +</ul> |
| 255 | + |
| 256 | +<!-- nth-child shortcuts (v4) --> |
| 257 | +<div class="nth-3:underline">Third</div> |
| 258 | +<div class="nth-[3n+1]:underline">Every 3rd starting at 1</div> |
| 259 | +<div class="nth-last-2:underline">Second from end</div> |
| 260 | +<div class="nth-of-type-1:font-bold">First of its type</div> |
| 261 | +``` |
| 262 | + |
| 263 | +### Pattern 5: Has, Not, In selectors (v4) |
| 264 | + |
| 265 | +```html |
| 266 | +<!-- has-* : style a parent based on a descendant's state --> |
| 267 | +<label class="has-checked:bg-indigo-50 has-checked:ring-2"> |
| 268 | + <input type="radio" name="plan" /> |
| 269 | + Standard plan |
| 270 | +</label> |
| 271 | + |
| 272 | +<!-- not-* : negate any variant --> |
| 273 | +<button class="opacity-75 not-hover:opacity-50"> |
| 274 | + Default 50%, hover 75% |
| 275 | +</button> |
| 276 | +<div class="not-supports-[display:grid]:flex"> |
| 277 | + Falls back to flex when grid not supported |
| 278 | +</div> |
| 279 | + |
| 280 | +<!-- in-* : implicit group (no `.group` class required on ancestor) --> |
| 281 | +<button class="text-slate-500 in-[role=tablist]:text-slate-900"> |
| 282 | + Highlights when inside a tablist |
| 283 | +</button> |
| 284 | +``` |
| 285 | + |
| 286 | +All three are v4-only. In v3, replace with arbitrary variants : `has-checked:` becomes `[&:has(:checked)]:`, `not-hover:` becomes `[&:not(:hover)]:`, `in-[role=tablist]:` becomes `[[role=tablist]_&]:`. |
| 287 | + |
| 288 | +### Pattern 6: Custom variants (v3 vs v4) |
| 289 | + |
| 290 | +| v3.4 : JavaScript plugin | v4.0+ : CSS `@custom-variant` | |
| 291 | +|---------------------------|------------------------------| |
| 292 | +| ```js |
| 293 | +// tailwind.config.js |
| 294 | +const plugin = require('tailwindcss/plugin') |
| 295 | +module.exports = { |
| 296 | + plugins: [ |
| 297 | + plugin(function ({ addVariant }) { |
| 298 | + addVariant('third', '&:nth-child(3)') |
| 299 | + addVariant('hocus', ['&:hover', '&:focus']) |
| 300 | + addVariant('supports-grid', '@supports (display: grid)') |
| 301 | + }), |
| 302 | + ], |
| 303 | +} |
| 304 | +``` | ```css |
| 305 | +@import "tailwindcss"; |
| 306 | + |
| 307 | +@custom-variant third (&:nth-child(3)); |
| 308 | +@custom-variant hocus (&:hover, &:focus); |
| 309 | + |
| 310 | +@custom-variant supports-grid { |
| 311 | + @supports (display: grid) { @slot; } |
| 312 | +} |
| 313 | +``` | |
| 314 | +
|
| 315 | +Then use `third:bg-blue-500`, `hocus:underline`, `supports-grid:grid` in markup. The two syntaxes produce equivalent compiled output. |
| 316 | +
|
| 317 | +### Pattern 7: Arbitrary variants (escape hatch, both versions) |
| 318 | +
|
| 319 | +When no built-in variant fits, the `[&...]` arbitrary syntax accepts any CSS selector or at-rule. |
| 320 | +
|
| 321 | +```html |
| 322 | +<!-- Custom class selector --> |
| 323 | +<li class="[&.is-dragging]:cursor-grabbing">Drag-aware</li> |
| 324 | +
|
| 325 | +<!-- Descendant selector --> |
| 326 | +<article class="[&_p]:text-slate-600 [&_h2]:text-2xl"> |
| 327 | + Auto-styles every p and h2 inside |
| 328 | +</article> |
| 329 | +
|
| 330 | +<!-- Sibling selector --> |
| 331 | +<input class="peer [&~p]:hidden focus:[&~p]:block" /> |
| 332 | +
|
| 333 | +<!-- Custom at-rule --> |
| 334 | +<div class="[@media(prefers-reduced-data:reduce)]:bg-none">No bg-image on slow networks</div> |
| 335 | +<div class="[@container(width>=600px)]:grid">Container-query escape hatch</div> |
| 336 | +
|
| 337 | +<!-- Stacked with built-in variants --> |
| 338 | +<li class="[&.is-dragging]:active:cursor-grabbing">Stacks fine</li> |
| 339 | +``` |
| 340 | + |
| 341 | +The `&` token represents the styled element. Use `_` to mean a space in CSS selectors (`[&_p]` => `& p`). |
| 342 | + |
| 343 | +## Reference Links |
| 344 | + |
| 345 | +- `references/methods.md` : complete variant catalog (every pseudo-class, pseudo-element, media query, feature query, attribute variant, combinator) with v3 vs v4 availability columns. |
| 346 | +- `references/examples.md` : working code examples for every family + arbitrary variants + nested groups + Radix data-state patterns. |
| 347 | +- `references/anti-patterns.md` : the stacking-order flip mistake, hover-on-touch surprise, leading-vs-trailing bang, ambiguous groups, v4-only variants in v3 projects. |
| 348 | + |
| 349 | +## Verified Sources |
| 350 | + |
| 351 | +- https://tailwindcss.com/docs/hover-focus-and-other-states (v4 variant system + `@custom-variant`) |
| 352 | +- https://v3.tailwindcss.com/docs/hover-focus-and-other-states (v3 variant system + `addVariant()`) |
| 353 | +- https://tailwindcss.com/docs/upgrade-guide (section 14, stacking-order flip + trailing bang + hover gate) |
| 354 | + |
| 355 | +Last verified : 2026-05-19 (Tailwind CSS v4.x current). |
0 commit comments