Tailwind CSS Cheatsheet

States and Variants

Use this Tailwind CSS reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

How Variants Work

Prefix any utility with a variant followed by : to apply it conditionally:

<button class="bg-blue-600 hover:bg-blue-700 focus:ring-2 active:scale-95 disabled:opacity-50">
  Button
</button>

Variants can be stacked:

<button class="dark:hover:bg-blue-500">Stacked: dark mode + hover</button>
<a class="sm:hover:underline">Responsive + hover</a>
<input class="focus:invalid:border-red-500" placeholder="Focus + invalid">

User Interaction Variants

VariantWhen
hover:pointer hovering
focus:element has focus (keyboard or mouse)
focus-within:element or any descendant has focus
focus-visible:focus is visible (keyboard only, not mouse)
active:element is being pressed/clicked
visited:<a> has been visited
target:element is the URL fragment target
<!-- Keyboard-only focus ring (no ring on mouse click) -->
<button class="focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 outline-none">
  Accessible Button
</button>

<!-- Highlight enclosing card when any input inside is focused -->
<div class="focus-within:ring-2 focus-within:ring-blue-500 rounded-xl p-4 border">
  <input class="outline-none w-full" type="text">
</div>

Form State Variants

VariantCSS pseudo-class
disabled::disabled
enabled::enabled
checked::checked
indeterminate::indeterminate
default::default
required::required
valid::valid
invalid::invalid
in-range::in-range
out-of-range::out-of-range
placeholder-shown::placeholder-shown
autofill::autofill
read-only::read-only
optional::optional
<!-- Styled checkbox -->
<input type="checkbox" class="size-4 rounded border-gray-300 checked:bg-blue-600 checked:border-transparent focus:ring-2">

<!-- Floating label pattern -->
<div class="relative">
  <input class="peer border rounded px-3 pt-5 pb-1 placeholder-transparent w-full" placeholder="Email">
  <label class="absolute left-3 top-1 text-xs text-gray-500 transition-all peer-placeholder-shown:top-3 peer-placeholder-shown:text-base">
    Email
  </label>
</div>

<!-- Real-time validation styling -->
<input type="email" class="border rounded px-3 py-2 invalid:[&:not(:placeholder-shown)]:border-red-500 valid:[&:not(:placeholder-shown)]:border-green-500">

Structural / Position Variants

VariantCSS pseudo-class
first::first-child
last::last-child
only::only-child
odd::nth-child(odd)
even::nth-child(even)
nth-[3]::nth-child(3)
nth-last-[2]::nth-last-child(2)
first-of-type::first-of-type
last-of-type::last-of-type
only-of-type::only-of-type
nth-of-type-[2]::nth-of-type(2)
empty::empty
<!-- Remove border on last list item -->
<ul class="divide-y divide-slate-200">
  <li class="py-3 last:border-0">Item</li>
  <li class="py-3 last:border-0">Item</li>
  <li class="py-3 last:border-0">Last item — no border</li>
</ul>

<!-- Zebra striping -->
<tr class="even:bg-slate-50">...</tr>

<!-- Extra padding on first and last in list -->
<li class="px-4 first:pt-4 last:pb-4 py-2">...</li>

Parent State (group)

Style children based on parent's state.

<!-- Mark parent -->
<div class="group">
  <!-- Style child based on parent hover -->
  <h3 class="group-hover:text-blue-600 transition-colors">Title</h3>
  <p class="group-hover:opacity-100 opacity-0 transition-opacity">Revealed</p>
</div>

<!-- group-focus, group-active, group-checked also work -->
<label class="group">
  <input type="checkbox" class="sr-only peer">
  <span class="group-has-[:checked]:bg-blue-600">...</span>
</label>

Named Groups (nested groups)

<div class="group/card hover:shadow-lg">
  <div class="group/button">
    <span class="group-hover/card:text-blue-600">Changes on card hover</span>
    <span class="group-hover/button:underline">Changes on button hover</span>
  </div>
</div>

Sibling State (peer)

Style an element based on its preceding sibling's state.

<!-- Style label based on input state -->
<input class="peer border rounded px-3 py-2" type="checkbox">
<label class="peer-checked:text-blue-600 peer-checked:font-semibold">
  Checked label
</label>

<!-- Error message appears when input is invalid -->
<input class="peer border rounded px-3 py-2 invalid:border-red-500" type="email">
<p class="hidden peer-invalid:block text-red-500 text-sm mt-1">Invalid email</p>

Named Peers

<input class="peer/email" type="email" id="email">
<p class="hidden peer-invalid/email:block text-red-500">Invalid</p>

has() Variant

Style a parent based on whether it contains a matching child.

<!-- Card that highlights when it contains a checked checkbox -->
<div class="has-[:checked]:bg-blue-50 has-[:checked]:border-blue-300 border rounded-xl p-4">
  <input type="checkbox">
  <label>Option</label>
</div>

<!-- Form submission button disabled when form has invalid inputs -->
<button class="group-has-[:invalid]:opacity-50 group-has-[:invalid]:cursor-not-allowed">
  Submit
</button>

Data Attribute Variants

Apply styles based on data-* attribute values.

<!-- Generic data-* presence -->
<div data-active class="data-[active]:bg-blue-600 data-[active]:text-white px-4 py-2 rounded">
  Active tab
</div>

<!-- With specific value -->
<div data-state="open" class="data-[state=open]:block hidden">Dropdown</div>
<div data-state="closed" class="data-[state=closed]:animate-slide-out">...</div>

ARIA Variants

Style based on ARIA attribute values.

<button aria-expanded="true" class="aria-expanded:bg-slate-100">Toggle</button>
<div aria-hidden="true" class="aria-hidden:invisible">...</div>
<input aria-invalid="true" class="aria-invalid:border-red-500 aria-invalid:ring-red-500">

<!-- Custom aria value -->
<div aria-[sort=ascending]:text-blue-600>...</div>

Not Variant

Negate a selector:

<!-- Apply styles to all elements that are NOT the last child -->
<li class="not-last:border-b py-3">Item</li>

<!-- Not disabled -->
<button class="not-disabled:hover:bg-blue-700">...</button>

<!-- Not checked -->
<input type="checkbox" class="not-checked:border-gray-300">

In Variant (parent scope)

Apply styles when the element is inside a matching parent:

<!-- Style links differently when inside a nav -->
<a class="in-[nav]:font-medium in-[nav]:text-slate-700" href="#">Link</a>

<!-- Nested context styling -->
<p class="in-[.prose]:text-slate-700">...</p>

Pseudo-element Variants

VariantPseudo
before:::before
after:::after
placeholder:::placeholder
file:::file-selector-button
marker:::marker (list bullets)
selection:::selection
first-line:::first-line
first-letter:::first-letter
backdrop:::backdrop (for <dialog>, etc.)
<!-- Styled placeholder -->
<input class="placeholder:text-slate-400 placeholder:italic border rounded px-3 py-2" placeholder="Search...">

<!-- Custom list markers -->
<ul class="marker:text-blue-500 marker:font-bold list-disc pl-6">
  <li>Item</li>
</ul>

<!-- Custom text selection color -->
<p class="selection:bg-yellow-200 selection:text-yellow-900">
  Highlight this text
</p>

<!-- Decorative before/after -->
<span class="before:content-['✓'] before:text-green-500 before:mr-2">Done</span>

<!-- Custom file input button -->
<input type="file" class="file:mr-4 file:py-2 file:px-4 file:border-0 file:rounded-lg file:bg-blue-600 file:text-white file:font-medium hover:file:bg-blue-700">

Open / Closed Variants

For <details>, <dialog>, and <select> elements with open/closed states:

<details class="group border rounded-lg">
  <summary class="flex justify-between items-center p-4 cursor-pointer">
    Question
    <span class="group-open:rotate-180 transition-transform"></span>
  </summary>
  <div class="px-4 pb-4">Answer content</div>
</details>

<dialog class="backdrop:bg-black/50 open:flex flex-col">
  ...
</dialog>

Combining Multiple Variants

Variants stack left-to-right, all must match:

<!-- dark mode + hover + focus -->
<button class="dark:hover:bg-slate-700 dark:focus:ring-slate-500">...</button>

<!-- responsive + state -->
<input class="sm:focus:ring-2 lg:focus:ring-4">

<!-- group + responsive -->
<p class="sm:group-hover:underline">...</p>

Arbitrary Variant

One-off pseudo-class or selector using bracket notation:

<!-- :nth-child(3) -->
<li class="[&:nth-child(3)]:font-bold">...</li>

<!-- Complex selector -->
<div class="[&>p]:mb-4 [&>h2]:text-2xl">
  <p>Paragraph with bottom margin</p>
  <h2>Heading styled by parent</h2>
</div>

<!-- Multiple selectors -->
<ul class="[&_li]:py-2 [&_li:first-child]:pt-0">...</ul>

<!-- Combine with responsive -->
<div class="lg:[&>p]:text-lg">...</div>

Supports Variant

Apply styles only when the browser supports a CSS feature:

<div class="supports-[display:grid]:grid supports-[not(display:grid)]:flex">
  ...
</div>

<div class="supports-[backdrop-filter]:backdrop-blur-md bg-white/30">
  ...
</div>