Responsive & State Variants
Responsive prefixes like md: and lg:, state variants like hover: and focus:, and dark mode with dark:.
Responsive prefixes
Tailwind is mobile-first: an unprefixed utility applies at every screen size, and a breakpoint prefix makes a utility apply only from that breakpoint up. This mirrors the mobile-first min-width media query approach from plain CSS.
| Prefix | Applies from (default) |
|---|---|
| (none) | All screen sizes |
sm: |
640px and up |
md: |
768px and up |
lg: |
1024px and up |
xl: |
1280px and up |
2xl: |
1536px and up |
<div class="text-base md:text-lg lg:text-xl">
Font size grows as the viewport gets wider
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- 1 column on mobile, 2 from sm, 4 from lg -->
</div>
<nav class="hidden md:flex gap-6">
<!-- hidden on mobile, shown as a flex row from md up -->
</nav>
Read md:grid-cols-2 as "apply grid-cols-2 once the viewport is at least the md breakpoint" — exactly equivalent to writing @media (min-width: 768px) { grid-template-columns: repeat(2, 1fr); } by hand, just inline.
State variants: hover:, focus:, disabled:
State variants apply a utility only when the element is in a particular interactive state:
<button class="bg-blue-500 hover:bg-blue-600 focus:ring-2 focus:ring-blue-300 active:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-white px-4 py-2 rounded-md transition">
Submit
</button>
| Variant | Applies when |
|---|---|
hover: |
The pointer is over the element |
focus: |
The element has keyboard/click focus |
active: |
The element is being clicked/pressed |
disabled: |
The element has the disabled attribute |
focus-within: |
Any descendant has focus (useful on a form wrapper) |
group-hover: |
A parent marked group is hovered — lets a hover on a card affect a child element |
group is a small but powerful pattern for hovering a parent to style a child:
<div class="group p-4 border rounded-lg hover:border-blue-400">
<h3 class="text-gray-900">Card title</h3>
<p class="text-gray-500 group-hover:text-blue-500">
This text changes color when the *card* (not just this text) is hovered.
</p>
</div>
Combining responsive and state variants
Variants stack, and can be combined in any order the class name expresses left to right — a common pattern is a responsive prefix plus a state variant on the same utility:
<button class="bg-blue-500 md:hover:bg-blue-700">
Only applies the hover color from md breakpoint up
</button>
Dark mode: dark:
Tailwind ships a dark: variant that applies when dark mode is active. By default it follows the operating system's prefers-color-scheme, but it's commonly switched to a manual toggle using the class strategy:
// tailwind.config.js
module.exports = {
darkMode: 'class', // instead of the default 'media'
// ...
};
<body class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<div class="bg-gray-50 dark:bg-gray-800 p-6 rounded-lg">
<h2 class="text-gray-900 dark:text-white">Card title</h2>
<p class="text-gray-600 dark:text-gray-400">Card body text.</p>
</div>
</body>
With darkMode: 'class', dark styles only apply while an ancestor (usually <html>) has the class dark — toggled by a small script:
document.documentElement.classList.toggle('dark');
With the default darkMode: 'media', the same dark: classes apply automatically based on the visitor's OS-level preference, with no toggle needed — but also no way for the user to override it independently of their OS setting.
Common mistakes
- Using
lg:whenmd:(or vice versa) was intended — always check a design against actual breakpoint widths rather than guessing which prefix "sounds right." - Forgetting
dark:variants entirely on background/text color pairs, resulting in unreadable (e.g. dark text on a dark background) combinations for users with dark mode enabled. - Trying to use
group-hover:without adding thegroupclass to the actual ancestor — the child variant does nothing unless the correct parent carriesgroup. - Assuming responsive prefixes are max-width (desktop-first) — Tailwind's default breakpoints are all
min-width, somd:text-lgmeans "atmdand wider," not "atmdand narrower."