Tailwind CSS State Modifiers
This skill covers variants that target CSS states and pseudo-elements other
than interaction states (hover, focus, active). Use it together with :
tailwind-syntax-variants: full variant grammar including hover/focus, group/peer, attribute, and arbitrary variantstailwind-syntax-responsive: breakpoint and container-query variantstailwind-syntax-dark-mode: thedark:variant configuration
Quick Reference : Variant Categories
| Category | Examples |
|---|---|
| Structural position | first:, last:, only:, odd:, even:, first-of-type:, last-of-type:, only-of-type:, nth-[...]:, empty: |
| Pseudo-elements | before:, after:, placeholder:, file:, marker:, selection:, first-letter:, first-line:, backdrop: |
| Motion preference | motion-safe:, motion-reduce: |
| Form input state | required:, optional:, valid:, invalid:, user-valid:, user-invalid:, disabled:, enabled:, checked:, indeterminate:, default:, in-range:, out-of-range:, placeholder-shown:, autofill:, read-only: |
| Target / Open | target:, open: |
All variants in this skill are SHARED between v3 and v4 unless flagged
otherwise. The one divergence : user-valid: and user-invalid: are
v4-only (CSS-native, gated on browser support in v3).
Structural Variants : Position in Parent
<ul>
<li class="first:pt-0">A</li> <!-- &:first-child -->
<li>B</li>
<li>C</li>
<li class="last:pb-0">D</li> <!-- &:last-child -->
</ul>
<ul>
<li class="odd:bg-gray-50 even:bg-white">A</li> <!-- &:nth-child(odd/even) -->
<li class="odd:bg-gray-50 even:bg-white">B</li>
</ul>
<ul>
<li class="only:font-bold">Alone</li> <!-- &:only-child -->
</ul>
<table>
<tr class="first-of-type:font-bold last-of-type:border-b-0">
<td>Row data</td>
</tr>
</table>
<div class="empty:hidden"></div> <!-- &:empty (no children, no text) -->
Arbitrary nth- Selectors
| Variant | Selector |
|---|---|
nth-[3n+1]: |
&:nth-child(3n+1) |
nth-last-[3]: |
&:nth-last-child(3) |
nth-of-type-[2n]: |
&:nth-of-type(2n) |
nth-last-of-type-[odd]: |
&:nth-last-of-type(odd) |
nth-[2n+1_of_li]: |
&:nth-child(2n+1 of li) |
NEVER apply structural variants to elements that share a parent with
DIFFERENT element types unless you want the variant to count ALL siblings.
first-of-type: counts only same-tag siblings ; first: counts every kind.
<!-- "first:" picks the heading even though it's not a li -->
<div>
<h2 class="first:text-xl">Heading</h2>
<ul>
<li class="first:pt-0">First li (correct)</li>
<li>Second</li>
</ul>
</div>
Pseudo-Element Variants
| Variant | Selector | Use case |
|---|---|---|
before: |
&::before |
Inject content before element |
after: |
&::after |
Inject content after element |
placeholder: |
&::placeholder |
Style <input> placeholder text |
file: |
&::file-selector-button |
Style <input type="file"> button |
marker: |
&::marker, & *::marker |
Style <li> bullets and ordered numbers |
selection: |
&::selection |
Style user-selected text |
first-letter: |
&::first-letter |
Drop-cap |
first-line: |
&::first-line |
Style first line of text |
backdrop: |
&::backdrop |
Style native <dialog> overlay |
The Content Utility (before / after)
Both v3 and v4 automatically insert content: '' for before: and after:
WHEN PREFLIGHT IS ACTIVE. If Preflight is disabled, both versions fail
silently : the pseudo-element does not render. This is the single most
common before/after bug.
<!-- Works with preflight on (both v3 and v4) -->
<span class="before:ml-1 before:inline-block before:size-2 before:bg-red-500"></span>
<!-- Override the default with a string -->
<span class="before:content-['→'] before:mr-1">Next</span>
<!-- Read from data attribute -->
<span data-label="New" class="after:content-[attr(data-label)]"></span>
<!-- Empty content (also explicit) -->
<span class="before:content-['']"></span>
Source : https://tailwindcss.com/docs/hover-focus-and-other-states ("Tailwind will automatically add content: '' by default so you don't have to specify it unless you want a different value").
NEVER use a literal space between brackets in content-[...]. Whitespace
is parsed as an underscore. Use content-['Hello_World'] (Tailwind
converts _ to space) or content-["Hello World"] with double quotes.
Preflight-Disabled Workaround
When preflight: false (v3) or @import "tailwindcss/utilities"; only
(v4 without the preflight import), you MUST add content-[''] explicitly
to every before: / after: usage :
<!-- Preflight off : explicit content required -->
<span class="before:content-[''] before:ml-1 before:inline-block before:size-2 before:bg-red-500"></span>
Form State Variants
<input class="required:border-red-500" required />
<input class="optional:border-gray-300" />
<!-- HTML5 validation : :valid / :invalid match BEFORE first interaction -->
<input type="email" required
class="invalid:border-red-500 invalid:text-red-700" />
<!-- v4 only : user-* variants match only AFTER user has interacted -->
<input type="email" required
class="user-invalid:border-red-500 user-invalid:text-red-700" />
<input type="checkbox" class="checked:bg-blue-500" />
<input type="checkbox" class="indeterminate:bg-gray-400" />
<input type="radio" class="default:ring-2 default:ring-blue-500" />
<input type="text" disabled class="disabled:opacity-50 disabled:cursor-not-allowed" />
<input type="text" readonly class="read-only:bg-gray-50" />
<input class="placeholder-shown:border-gray-300" placeholder="Type here" />
<input type="text" class="autofill:bg-yellow-50" />
<input type="number" min="0" max="100"
class="in-range:border-green-500 out-of-range:border-red-500" />
Variant Selectors
| Variant | Selector |
|---|---|
required |
&:required |
optional |
&:optional |
valid |
&:valid (matches before first interaction) |
invalid |
&:invalid (matches before first interaction) |
user-valid |
&:user-valid (v4 only ; matches after user input) |
user-invalid |
&:user-invalid (v4 only) |
disabled |
&:disabled |
enabled |
&:enabled |
checked |
&:checked |
indeterminate |
&:indeterminate |
default |
&:default (form input pre-selected by browser) |
in-range |
&:in-range (number/range inputs) |
out-of-range |
&:out-of-range |
placeholder-shown |
&:placeholder-shown |
autofill |
&:autofill |
read-only |
&:read-only |
ALWAYS prefer user-invalid: over invalid: in v4 when styling field
errors : invalid: fires on every required field as soon as the page
loads, before the user has typed anything. user-invalid: waits for
interaction. v3 has no user-* variants : work around with the
:placeholder-shown trick or wire validation via JS.
<!-- v3 trick : invalid only matters after user typed something -->
<input type="email" required
class="invalid:not-placeholder-shown:border-red-500"
placeholder=" " />
Motion Preference Variants
<!-- Animation only when user has NOT set reduce-motion preference -->
<div class="motion-safe:animate-spin">spinner</div>
<!-- Static fallback when user prefers reduced motion -->
<div class="motion-safe:animate-bounce motion-reduce:opacity-50"></div>
| Variant | Media query |
|---|---|
motion-safe |
@media (prefers-reduced-motion: no-preference) |
motion-reduce |
@media (prefers-reduced-motion: reduce) |
ALWAYS gate animations with motion-safe: OR provide a motion-reduce:
override. NEVER ship animate-spin unconditionally on critical UI : users
with vestibular conditions can be physically hurt by it.
Target Variant : URL Fragment
<a href="#section-2">Jump</a>
<section id="section-2" class="target:bg-yellow-100">
Section 2 (highlights when URL fragment is #section-2).
</section>
target: matches when the element's id equals the current URL fragment.
Useful for in-page anchor highlighting without JS.
Open Variant : details / dialog / popover
<details class="open:bg-gray-50">
<summary>Click to expand</summary>
<p>Hidden until summary is clicked.</p>
</details>
<dialog id="d" class="backdrop:bg-black/30 open:p-4">
<p>Modal content</p>
</dialog>
<div popover class="open:shadow-lg">popover content</div>
The v4 open: variant compiles to &:is([open], :popover-open, :open) so
it matches <details>, <dialog>, and [popover] elements simultaneously.
The v3 open: compiles to &[open] only and does NOT match popovers.
NEVER assume open: matches modals you open via dialog.showModal() :
that adds the [open] attribute, which IS matched. Programmatically
showing a <dialog> via .show() also sets [open].
Empty Variant
<div class="empty:hidden">
<!-- if this div has zero children AND zero text, it is display:none -->
</div>
empty: matches both no-child and no-text states. Whitespace counts as
text, so a <div> </div> is NOT empty. Use empty:hidden to collapse
list placeholders that may receive no items.
Decision Tree : Which Variant Group?
Styling based on what?
├── Position among siblings → structural (first/last/nth-)
├── A pseudo-element (::before, ::placeholder, ::marker, etc.) → pseudo-element variants
├── A form input attribute state (required, disabled, checked) → form state
├── HTML5 validity (valid, invalid) → form-state, prefer user-valid/user-invalid in v4
├── User accessibility preference (reduced motion) → motion-safe/motion-reduce
├── URL fragment match → target:
├── details/dialog/popover open → open:
├── No children → empty:
└── User pointer interaction (hover, focus) → see tailwind-syntax-variants
Stacking With Other Variants
<!-- Position + hover -->
<li class="first:hover:bg-gray-100"></li>
<!-- Position + responsive -->
<li class="first:pt-0 md:first:pt-4"></li>
<!-- Form state + dark -->
<input class="invalid:border-red-500 dark:invalid:border-red-300" />
<!-- Pseudo-element + group -->
<div class="group">
<span class="before:opacity-0 group-hover:before:opacity-100">→</span>
</div>
v4 stacks chains left-to-right (per upgrade guide). The functional intent
is the same as v3 ; only the syntax order differs. See tailwind-syntax-variants
for the stacking-order migration.
Reference Files
references/methods.md: every variant with full CSS selector and browser-support notereferences/examples.md: full real-world patterns (validated form, drop-cap, custom bullet, modal backdrop, accessible spinner)references/anti-patterns.md: the empty-before-with-preflight-off trap,invalid:firing before user input,placeholder:on wrapper instead of input,first:counting wrong sibling type
Verified Sources
- https://tailwindcss.com/docs/hover-focus-and-other-states (v4)
- https://v3.tailwindcss.com/docs/hover-focus-and-other-states (v3)
- vooronderzoek-tailwind.md §6 Variant System