Hover and focus cover interaction, and group/peer cover relationships. A third family of variants targets an element by its position among siblings, by attributes that JavaScript sets, or by pseudo-elements. After this lesson you will be able to style first and last items, alternate rows, react to aria-* and data-* attributes, and add ::before/::after content without extra CSS.
Structural variants map onto CSS pseudo-classes:
| Variant | Selector | Typical use |
|---|---|---|
| first: / last: | :first-child / :last-child | Remove the trailing border, round outer corners |
| odd: / even: | :nth-child(odd/even) | Zebra-striped tables |
| only: | :only-child | Different layout for a single item |
| nth-3: / nth-last-2: | :nth-child(3) | Highlight a specific position |
| nth-[2n+1_of_.item]: | Arbitrary nth-child formula | Skip non-matching siblings |
| empty: | :empty | Hide containers with no content |
<table class="w-full">
<tr class="border-b last:border-b-0 even:bg-gray-50 hover:bg-blue-50">
<td class="p-2 first:ps-4 last:pe-4">Cell</td>
</tr>
</table>first-of-type: and last-of-type: are the alternatives when siblings are mixed elements.
Sometimes the children are generated by a component you do not control. The *: variant styles direct children and **: styles all descendants, so the classes live on the parent:
<ul class="*:rounded-full *:border *:px-3 *:py-1 flex gap-2">
<li>Sales</li>
<li>Marketing</li>
</ul>
<div class="**:data-avatar:size-8 **:data-avatar:rounded-full">
<!-- any nested element with data-avatar becomes a small round image -->
</div>*:first:pt-0 reads left to right: "the direct child that is the first child". Because *: uses :where(), a class on the child itself still wins when you need an exception.
JavaScript libraries express state through attributes rather than classes: data-state="open", aria-expanded="true", aria-selected. Tailwind reads them directly:
<div data-state="open" class="data-[state=open]:block data-[state=closed]:hidden">Panel</div>
<button aria-pressed="true" class="aria-pressed:bg-blue-600 aria-pressed:text-white">Bold</button>
<th aria-sort="ascending" class="aria-[sort=ascending]:bg-gray-100">Name</th>
<li data-active class="data-active:font-bold">Boolean attribute</li>The common boolean ARIA states are built in — aria-checked:, aria-disabled:, aria-expanded:, aria-hidden:, aria-pressed:, aria-readonly:, aria-required:, aria-selected:, aria-busy: — and match ="true". Any other attribute or value uses the arbitrary form aria-[sort=ascending]: or data-[size=large]:. All of them combine with group- and peer- prefixes, so group-aria-expanded:rotate-180 rotates an icon when its parent button is expanded. Because the attribute a screen reader announces is the same one driving the visuals, the two can never drift apart.
before: and after: generate ::before and ::after. Tailwind adds an empty content automatically, so you only set it when it should contain text:
<label class="after:ms-0.5 after:text-red-500 after:content-['*']">Name</label>
<a class="relative after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 after:bg-current after:transition-all hover:after:w-full">
Animated underline
</a>Other pseudo-element variants include placeholder:, marker: (list bullets), selection: (highlighted text), file: (file input button), first-line:, first-letter: and backdrop: (dialog backdrops).
not-* negates any variant: not-first:mt-4 adds top margin to every item except the first, not-hover:opacity-75 dims everything that is not hovered, and not-[.active]:text-gray-500 uses an arbitrary selector.
first: to work on a wrapper: it applies to the element carrying the class, which must be the first child of its parent.data-[state="open"] with quotes; write data-[state=open].after: for essential text; generated content is unreliable for screen readers.Which variant removes the bottom border from the last item of a list?
first:, last:, odd:, even:, only: and nth-* style elements by their position among siblings.*: and **: push styles down to children and descendants from the parent.aria-* and data-* variants, plus arbitrary data-[key=value]:, react to attributes set by JavaScript.before: and after: create pseudo-elements with automatic empty content; add content-['…'] for text.not-* negates any variant for "everything except" rules.Next lesson: Dark Mode — build a color scheme that responds to system preference or a manual toggle with the dark: variant.