Skip to content

Commit 8ba566a

Browse files
FreekHeijtingclaude
andcommitted
feat(skill): tailwind-syntax-state-modifiers
Variants for CSS states and pseudo-elements beyond hover/focus: structural (first/last/nth-child), pseudo-elements (before/after/ placeholder/file/marker/selection/first-letter/first-line/backdrop), motion preference, full form-input state set, and target/open. Files: - SKILL.md (349 lines) : decision tree, variant categories, v4 user-valid/user-invalid coverage, content utility nuance - references/methods.md : every variant with CSS selector and browser-support baseline, v3 vs v4 divergence flagged - references/examples.md : 20 real-world patterns (striped table, drop-cap, modal backdrop, validated form v4 + v3 workaround, accessible spinner, preflight-off workaround) - references/anti-patterns.md : 15 traps (preflight-off before/after, invalid: firing on load, first: vs first-of-type:, placeholder: on wrapper, motion-safe without fallback, etc.) Correction vs raw masterplan: BOTH v3 and v4 auto-insert content: "" for before/after when Preflight is active. The divergence is preflight-on vs preflight-off, not v3 vs v4. Documented and used as primary anti-pattern. Verified against: - https://tailwindcss.com/docs/hover-focus-and-other-states - https://v3.tailwindcss.com/docs/hover-focus-and-other-states Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9b8c63f commit 8ba566a

4 files changed

Lines changed: 1267 additions & 0 deletions

File tree

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
---
2+
name: tailwind-syntax-state-modifiers
3+
description: >
4+
Use when styling element states (form input validity, checked, disabled,
5+
required, placeholder visibility), structural positions (first, last,
6+
even, odd, nth-child), pseudo-elements (before, after, placeholder, file,
7+
marker, selection, first-letter), or motion-preference (motion-safe,
8+
motion-reduce), or when wiring a Tailwind class to a CSS pseudo-element
9+
with the content utility. Prevents the empty-before-pseudo trap (the
10+
::before disappears when Preflight is disabled), the structural-variant
11+
scope mistake (first: applied to grandchildren when it should be on
12+
direct children), the placeholder-styling mistake (placeholder: on the
13+
wrapper instead of the input), the form-state target mistake (invalid:
14+
triggers before user input via :user-invalid in v4), and the dialog/details
15+
open-state confusion (open: works for both via the :is() compound).
16+
Covers structural pseudo-class variants, all pseudo-element variants, the
17+
content-[...] utility for ::before and ::after, motion-preference media
18+
query variants, the full form-state variant set, and target/open variants.
19+
Keywords: tailwind state, first-child, last-child, nth-child, odd, even,
20+
before, after, placeholder, content-[], content utility, motion-safe,
21+
motion-reduce, prefers-reduced-motion, required, valid, invalid,
22+
user-valid, user-invalid, disabled, checked, indeterminate, autofill,
23+
read-only, placeholder-shown, in-range, out-of-range, target, open,
24+
details open, dialog open, empty, first-letter, first-line, marker,
25+
selection, backdrop, file-selector-button, my ::before is not showing,
26+
content empty string, preflight disabled, why is first: not working, how
27+
do I style placeholder text, how do I style disabled input, how to style
28+
checked checkbox, pseudo element variant, structural variant.
29+
license: MIT
30+
compatibility: "Designed for Claude Code. Requires Tailwind CSS v3.4 or v4.0+."
31+
metadata:
32+
author: OpenAEC-Foundation
33+
version: "1.0"
34+
---
35+
36+
# Tailwind CSS State Modifiers
37+
38+
This skill covers variants that target CSS states and pseudo-elements other
39+
than interaction states (`hover`, `focus`, `active`). Use it together with :
40+
41+
- `tailwind-syntax-variants` : full variant grammar including hover/focus,
42+
group/peer, attribute, and arbitrary variants
43+
- `tailwind-syntax-responsive` : breakpoint and container-query variants
44+
- `tailwind-syntax-dark-mode` : the `dark:` variant configuration
45+
46+
## Quick Reference : Variant Categories
47+
48+
| Category | Examples |
49+
|----------|----------|
50+
| Structural position | `first:`, `last:`, `only:`, `odd:`, `even:`, `first-of-type:`, `last-of-type:`, `only-of-type:`, `nth-[...]:`, `empty:` |
51+
| Pseudo-elements | `before:`, `after:`, `placeholder:`, `file:`, `marker:`, `selection:`, `first-letter:`, `first-line:`, `backdrop:` |
52+
| Motion preference | `motion-safe:`, `motion-reduce:` |
53+
| 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:` |
54+
| Target / Open | `target:`, `open:` |
55+
56+
All variants in this skill are SHARED between v3 and v4 unless flagged
57+
otherwise. The one divergence : `user-valid:` and `user-invalid:` are
58+
v4-only (CSS-native, gated on browser support in v3).
59+
60+
## Structural Variants : Position in Parent
61+
62+
```html
63+
<ul>
64+
<li class="first:pt-0">A</li> <!-- &:first-child -->
65+
<li>B</li>
66+
<li>C</li>
67+
<li class="last:pb-0">D</li> <!-- &:last-child -->
68+
</ul>
69+
70+
<ul>
71+
<li class="odd:bg-gray-50 even:bg-white">A</li> <!-- &:nth-child(odd/even) -->
72+
<li class="odd:bg-gray-50 even:bg-white">B</li>
73+
</ul>
74+
75+
<ul>
76+
<li class="only:font-bold">Alone</li> <!-- &:only-child -->
77+
</ul>
78+
79+
<table>
80+
<tr class="first-of-type:font-bold last-of-type:border-b-0">
81+
<td>Row data</td>
82+
</tr>
83+
</table>
84+
85+
<div class="empty:hidden"></div> <!-- &:empty (no children, no text) -->
86+
```
87+
88+
### Arbitrary `nth-` Selectors
89+
90+
| Variant | Selector |
91+
|---------|----------|
92+
| `nth-[3n+1]:` | `&:nth-child(3n+1)` |
93+
| `nth-last-[3]:` | `&:nth-last-child(3)` |
94+
| `nth-of-type-[2n]:` | `&:nth-of-type(2n)` |
95+
| `nth-last-of-type-[odd]:` | `&:nth-last-of-type(odd)` |
96+
| `nth-[2n+1_of_li]:` | `&:nth-child(2n+1 of li)` |
97+
98+
NEVER apply structural variants to elements that share a parent with
99+
DIFFERENT element types unless you want the variant to count ALL siblings.
100+
`first-of-type:` counts only same-tag siblings ; `first:` counts every kind.
101+
102+
```html
103+
<!-- "first:" picks the heading even though it's not a li -->
104+
<div>
105+
<h2 class="first:text-xl">Heading</h2>
106+
<ul>
107+
<li class="first:pt-0">First li (correct)</li>
108+
<li>Second</li>
109+
</ul>
110+
</div>
111+
```
112+
113+
## Pseudo-Element Variants
114+
115+
| Variant | Selector | Use case |
116+
|---------|----------|----------|
117+
| `before:` | `&::before` | Inject content before element |
118+
| `after:` | `&::after` | Inject content after element |
119+
| `placeholder:` | `&::placeholder` | Style `<input>` placeholder text |
120+
| `file:` | `&::file-selector-button` | Style `<input type="file">` button |
121+
| `marker:` | `&::marker, & *::marker` | Style `<li>` bullets and ordered numbers |
122+
| `selection:` | `&::selection` | Style user-selected text |
123+
| `first-letter:` | `&::first-letter` | Drop-cap |
124+
| `first-line:` | `&::first-line` | Style first line of text |
125+
| `backdrop:` | `&::backdrop` | Style native `<dialog>` overlay |
126+
127+
## The Content Utility (before / after)
128+
129+
Both v3 and v4 automatically insert `content: ''` for `before:` and `after:`
130+
WHEN PREFLIGHT IS ACTIVE. If Preflight is disabled, both versions fail
131+
silently : the pseudo-element does not render. This is the single most
132+
common before/after bug.
133+
134+
```html
135+
<!-- Works with preflight on (both v3 and v4) -->
136+
<span class="before:ml-1 before:inline-block before:size-2 before:bg-red-500"></span>
137+
138+
<!-- Override the default with a string -->
139+
<span class="before:content-['→'] before:mr-1">Next</span>
140+
141+
<!-- Read from data attribute -->
142+
<span data-label="New" class="after:content-[attr(data-label)]"></span>
143+
144+
<!-- Empty content (also explicit) -->
145+
<span class="before:content-['']"></span>
146+
```
147+
148+
Source : https://tailwindcss.com/docs/hover-focus-and-other-states
149+
("Tailwind will automatically add content: '' by default so you don't have
150+
to specify it unless you want a different value").
151+
152+
NEVER use a literal space between brackets in `content-[...]`. Whitespace
153+
is parsed as an underscore. Use `content-['Hello_World']` (Tailwind
154+
converts `_` to space) or `content-["Hello World"]` with double quotes.
155+
156+
### Preflight-Disabled Workaround
157+
158+
When `preflight: false` (v3) or `@import "tailwindcss/utilities";` only
159+
(v4 without the preflight import), you MUST add `content-['']` explicitly
160+
to every `before:` / `after:` usage :
161+
162+
```html
163+
<!-- Preflight off : explicit content required -->
164+
<span class="before:content-[''] before:ml-1 before:inline-block before:size-2 before:bg-red-500"></span>
165+
```
166+
167+
## Form State Variants
168+
169+
```html
170+
<input class="required:border-red-500" required />
171+
<input class="optional:border-gray-300" />
172+
173+
<!-- HTML5 validation : :valid / :invalid match BEFORE first interaction -->
174+
<input type="email" required
175+
class="invalid:border-red-500 invalid:text-red-700" />
176+
177+
<!-- v4 only : user-* variants match only AFTER user has interacted -->
178+
<input type="email" required
179+
class="user-invalid:border-red-500 user-invalid:text-red-700" />
180+
181+
<input type="checkbox" class="checked:bg-blue-500" />
182+
<input type="checkbox" class="indeterminate:bg-gray-400" />
183+
184+
<input type="radio" class="default:ring-2 default:ring-blue-500" />
185+
186+
<input type="text" disabled class="disabled:opacity-50 disabled:cursor-not-allowed" />
187+
<input type="text" readonly class="read-only:bg-gray-50" />
188+
189+
<input class="placeholder-shown:border-gray-300" placeholder="Type here" />
190+
191+
<input type="text" class="autofill:bg-yellow-50" />
192+
193+
<input type="number" min="0" max="100"
194+
class="in-range:border-green-500 out-of-range:border-red-500" />
195+
```
196+
197+
### Variant Selectors
198+
199+
| Variant | Selector |
200+
|---------|----------|
201+
| `required` | `&:required` |
202+
| `optional` | `&:optional` |
203+
| `valid` | `&:valid` (matches before first interaction) |
204+
| `invalid` | `&:invalid` (matches before first interaction) |
205+
| `user-valid` | `&:user-valid` (v4 only ; matches after user input) |
206+
| `user-invalid` | `&:user-invalid` (v4 only) |
207+
| `disabled` | `&:disabled` |
208+
| `enabled` | `&:enabled` |
209+
| `checked` | `&:checked` |
210+
| `indeterminate` | `&:indeterminate` |
211+
| `default` | `&:default` (form input pre-selected by browser) |
212+
| `in-range` | `&:in-range` (number/range inputs) |
213+
| `out-of-range` | `&:out-of-range` |
214+
| `placeholder-shown` | `&:placeholder-shown` |
215+
| `autofill` | `&:autofill` |
216+
| `read-only` | `&:read-only` |
217+
218+
ALWAYS prefer `user-invalid:` over `invalid:` in v4 when styling field
219+
errors : `invalid:` fires on every required field as soon as the page
220+
loads, before the user has typed anything. `user-invalid:` waits for
221+
interaction. v3 has no `user-*` variants : work around with the
222+
`:placeholder-shown` trick or wire validation via JS.
223+
224+
```html
225+
<!-- v3 trick : invalid only matters after user typed something -->
226+
<input type="email" required
227+
class="invalid:not-placeholder-shown:border-red-500"
228+
placeholder=" " />
229+
```
230+
231+
## Motion Preference Variants
232+
233+
```html
234+
<!-- Animation only when user has NOT set reduce-motion preference -->
235+
<div class="motion-safe:animate-spin">spinner</div>
236+
237+
<!-- Static fallback when user prefers reduced motion -->
238+
<div class="motion-safe:animate-bounce motion-reduce:opacity-50"></div>
239+
```
240+
241+
| Variant | Media query |
242+
|---------|-------------|
243+
| `motion-safe` | `@media (prefers-reduced-motion: no-preference)` |
244+
| `motion-reduce` | `@media (prefers-reduced-motion: reduce)` |
245+
246+
ALWAYS gate animations with `motion-safe:` OR provide a `motion-reduce:`
247+
override. NEVER ship `animate-spin` unconditionally on critical UI : users
248+
with vestibular conditions can be physically hurt by it.
249+
250+
## Target Variant : URL Fragment
251+
252+
```html
253+
<a href="#section-2">Jump</a>
254+
255+
<section id="section-2" class="target:bg-yellow-100">
256+
Section 2 (highlights when URL fragment is #section-2).
257+
</section>
258+
```
259+
260+
`target:` matches when the element's id equals the current URL fragment.
261+
Useful for in-page anchor highlighting without JS.
262+
263+
## Open Variant : details / dialog / popover
264+
265+
```html
266+
<details class="open:bg-gray-50">
267+
<summary>Click to expand</summary>
268+
<p>Hidden until summary is clicked.</p>
269+
</details>
270+
271+
<dialog id="d" class="backdrop:bg-black/30 open:p-4">
272+
<p>Modal content</p>
273+
</dialog>
274+
275+
<div popover class="open:shadow-lg">popover content</div>
276+
```
277+
278+
The v4 `open:` variant compiles to `&:is([open], :popover-open, :open)` so
279+
it matches `<details>`, `<dialog>`, and `[popover]` elements simultaneously.
280+
The v3 `open:` compiles to `&[open]` only and does NOT match popovers.
281+
282+
NEVER assume `open:` matches modals you open via `dialog.showModal()` :
283+
that adds the `[open]` attribute, which IS matched. Programmatically
284+
showing a `<dialog>` via `.show()` also sets `[open]`.
285+
286+
## Empty Variant
287+
288+
```html
289+
<div class="empty:hidden">
290+
<!-- if this div has zero children AND zero text, it is display:none -->
291+
</div>
292+
```
293+
294+
`empty:` matches both no-child and no-text states. Whitespace counts as
295+
text, so a `<div> </div>` is NOT empty. Use `empty:hidden` to collapse
296+
list placeholders that may receive no items.
297+
298+
## Decision Tree : Which Variant Group?
299+
300+
```
301+
Styling based on what?
302+
├── Position among siblings → structural (first/last/nth-)
303+
├── A pseudo-element (::before, ::placeholder, ::marker, etc.) → pseudo-element variants
304+
├── A form input attribute state (required, disabled, checked) → form state
305+
├── HTML5 validity (valid, invalid) → form-state, prefer user-valid/user-invalid in v4
306+
├── User accessibility preference (reduced motion) → motion-safe/motion-reduce
307+
├── URL fragment match → target:
308+
├── details/dialog/popover open → open:
309+
├── No children → empty:
310+
└── User pointer interaction (hover, focus) → see tailwind-syntax-variants
311+
```
312+
313+
## Stacking With Other Variants
314+
315+
```html
316+
<!-- Position + hover -->
317+
<li class="first:hover:bg-gray-100"></li>
318+
319+
<!-- Position + responsive -->
320+
<li class="first:pt-0 md:first:pt-4"></li>
321+
322+
<!-- Form state + dark -->
323+
<input class="invalid:border-red-500 dark:invalid:border-red-300" />
324+
325+
<!-- Pseudo-element + group -->
326+
<div class="group">
327+
<span class="before:opacity-0 group-hover:before:opacity-100">→</span>
328+
</div>
329+
```
330+
331+
v4 stacks chains left-to-right (per upgrade guide). The functional intent
332+
is the same as v3 ; only the syntax order differs. See `tailwind-syntax-variants`
333+
for the stacking-order migration.
334+
335+
## Reference Files
336+
337+
- `references/methods.md` : every variant with full CSS selector and
338+
browser-support note
339+
- `references/examples.md` : full real-world patterns (validated form,
340+
drop-cap, custom bullet, modal backdrop, accessible spinner)
341+
- `references/anti-patterns.md` : the empty-before-with-preflight-off
342+
trap, `invalid:` firing before user input, `placeholder:` on wrapper
343+
instead of input, `first:` counting wrong sibling type
344+
345+
## Verified Sources
346+
347+
- https://tailwindcss.com/docs/hover-focus-and-other-states (v4)
348+
- https://v3.tailwindcss.com/docs/hover-focus-and-other-states (v3)
349+
- vooronderzoek-tailwind.md §6 Variant System

0 commit comments

Comments
 (0)