Tailwind Variants & Responsive Design

In Tailwind, a utility like bg-white applies unconditionally. Variants are prefixes that apply it only under a condition: at a breakpoint (md:grid-cols-3), on hover (hover:bg-gray-50), when focused by keyboard (focus-visible:ring-2), when a parent is hovered (group-hover:opacity-100), when a sibling input is invalid (peer-invalid:visible), when an element has a data attribute (data-[state=open]:rotate-180), in dark mode, or inside a container of a certain size (@md:flex-row).

Variants replace most hand-written media queries, pseudo-classes, and state selectors. They stack (md:hover:dark:bg-gray-800), and they compose with component libraries that expose state through attributes, which is how headless UI libraries and Tailwind work so well together.

TL;DR

Quick Example

Core Concepts

Responsive Breakpoints

Default breakpoints (v4): sm 40rem, md 48rem, lg 64rem, xl 80rem, 2xl 96rem. They're min-width queries, so design for small screens first, then add md: and lg: overrides. Target ranges with max-* variants (md:max-lg:hidden means only between md and lg) and custom one-offs with min-[900px]:. Customize via --breakpoint-* in the theme.

State Variants

Prefer focus-visible: for focus rings, so keyboard users see them and mouse clicks don't trigger noisy outlines. Never remove focus indication without a replacement. See accessibility.

group and peer

Attribute Variants

Relational and Negation Variants

Container Queries

Add @container (optionally named: @container/sidebar) to a parent, and use @sm:, @md:, @lg: (and @max-md:) on descendants to respond to the container's width instead of the viewport's. It's built into v4. See container queries.

Media and Preference Variants

dark:, motion-reduce:/motion-safe:, contrast-more:, forced-colors:, print:, portrait:/landscape:, pointer-coarse:, and supports-[display:grid]:. Respect user preferences, for example by disabling non-essential animation under motion-reduce:.

Stacking and Arbitrary Variants

Variants chain left to right: md:hover:bg-gray-50, dark:group-hover:text-white. For anything not built in, use arbitrary variants: [&>li]:py-2 (style direct child lis), [&:nth-child(3)]:font-bold, [@supports(backdrop-filter:blur(0))]:bg-white/60. For reused ones, define a custom variant with @custom-variant.

Best Practices

Design Mobile-First

Write base utilities for the smallest screen, then layer larger breakpoints. It keeps class lists predictable, and avoids fighting max-width overrides.

Use Container Queries for Reusable Components

Components that appear in sidebars, grids, and main columns should respond to their container, not the viewport. Reserve viewport breakpoints for page-level layout.

Style State From Attributes

Drive visual states from aria-* and data-* attributes rather than toggling many classes in JavaScript. The DOM becomes the single source of truth, and accessibility attributes are guaranteed to match visuals.

Keep Interactions Accessible

Pair hover: with focus-visible: equivalents, ensure touch users can access hover-revealed content, and respect motion-reduce.

Common Mistakes

Thinking Breakpoints Are Max-Width

Use sm:max-md:block for a single range.

Using peer on a Later Sibling

peer-* only styles elements that come after the peer in the DOM (CSS sibling combinators look forward). Reorder markup, use has-* on a common parent, or use a group.

Removing Focus Styles

focus:outline-none without a focus-visible:ring-* replacement makes keyboard navigation invisible, which is a common accessibility failure.

FAQ

Are Tailwind breakpoints min-width or max-width?

Min-width. md:flex applies from the md breakpoint and up. Unprefixed utilities apply at all sizes. For upper bounds or ranges, use max-* variants like max-md: or combined md:max-lg:.

What's the difference between group and peer?

group styles descendants of an element based on the element's state (hovering a card changes its children). peer styles subsequent siblings based on a sibling's state (an invalid input shows the error message after it).

How do I use container queries in Tailwind?

Add the @container class to a parent, then use container variants like @md:grid-cols-2 on its children. They respond to the parent's inline size rather than the viewport. Name containers (@container/card, @md/card:) when nesting.

How do I style based on a data attribute?

Use data-* variants: data-[state=open]:bg-gray-100, or data-active: for boolean attributes. It pairs naturally with headless component libraries that expose state through data attributes.

Related Topics

References