Patterns

Patterns — Tailwind CSS

Theming & customization

Extend, don't replace, unless you mean to. Add new tokens with @theme { --color-brand: ...; } alongside the defaults. Only reach for --namespace-*: initial (drop one namespace) or --*: initial (drop everything) when building a from-scratch design system — most projects want the defaults plus a few additions. Trade-offs: a fully custom theme (--*: initial) means zero accidental use of default colors/spacing, but every utility must be re-justified against your own token set — slower to start, harder to accidentally drift from a design system once set up.

Reference theme tokens as CSS variables, not the theme() function. var(--color-blue-500) works everywhere; theme() is deprecated except inside media queries (which can't take a CSS variable) — there, use the variable name, not v3's dot notation (theme(--breakpoint-xl), not theme(screens.xl)). Trade-offs: CSS variables are live (respond to @theme inline overrides, cascade); theme() was resolved once at build time — prefer variables unless you specifically need JS interop via getComputedStyle.

Share a theme across projects as a plain CSS file. @theme { --*: initial; ...tokens... } in its own file, imported via @import "../brand/theme.css"; — works as a monorepo package or an npm-published stylesheet, no build tooling required.

Responsive & adaptive design

Mobile-first, always. Write the unprefixed (mobile) styles first, then layer sm:/md:/lg: overrides for larger sizes — never use a breakpoint prefix to mean "only on this device class," since prefixes mean "this breakpoint and up." Trade-offs: mobile-first requires thinking about the smallest layout first even when designing desktop-first mockups — worth the friction because it matches how the cascade actually resolves.

Use container queries (@container/@sm:) for components that appear in variable-width contexts (sidebar widget, reusable card, dashboard panel) — viewport breakpoints (sm:/md:) are for page-level layout, container queries are for component-level layout independent of where it's placed.

Compose sm: (or @sm:) with max-sm: (or @max-sm:) to scope a style to a range instead of "this size and up" — the default direction is almost always wrong for "only between these two breakpoints" cases.

State & variants

Reach for a variant before reaching for JS state. hover:/focus:/aria-*:/data-*:/has-*:/group-*:/peer-*: cover the vast majority of "style differently when X" needs without any JavaScript or class-toggling logic — headless UI libraries that expose state via data-*/aria-* attributes are designed to be styled this way.

peer must come before the styled element in the DOM; group has no such constraint. If a "style based on sibling state" pattern isn't working, check element order first — CSS sibling combinators only look forward.

Prefer -safe alignment variants for centered/end-aligned content of unpredictable size. justify-center-safe falls back to start-alignment instead of letting content clip off-screen with no scroll access — a small addition with real accessibility payoff for user-generated or variable-length content.

Composition & duplication

Climb the duplication ladder in order: (1) if markup repeats because it's rendered in a loop, there's no real duplication — the class list is authored once; (2) multi-cursor editing for duplication localized to one file; (3) a component/template partial when styles repeat across files; (4) a hand-written @utility/@layer components class only when a partial feels like overkill for one trivial element. Don't jump straight to step 4.

Never apply two utilities targeting the same CSS property to one element — cascade order (not class-attribute order) decides the winner, which is rarely what you expect. Branch the class string in your component logic instead (gridLayout ? "grid" : "flex").

Compose CSS-variable-based utility families freelyblur-sm grayscale, from-red-500 via-orange-400 to-yellow-400, rotate-x-45 rotate-y-30 scale-110 all combine because each utility sets its own variable and the shared property references all of them. This is different from utilities on the same literal property (see previous pattern) — those conflict; these compose.

Migration & compatibility

Run npx @tailwindcss/upgrade on a branch, then read the diff — don't trust it blind on a complex project. Search specifically for the shadow/blur/radius/backdrop-blur -sm-suffix renames and ringring-3 even after running it; these are semantic shifts that won't always show up as an obvious visual regression until deployed.

Don't fight Tailwind with a preprocessor. Sass/Less/Stylus, CSS Modules, and framework <style> blocks each add friction (build cost, missing @theme access) that Tailwind v4's native nesting/variables/imports already solve — when one of these is unavoidable (existing codebase), add @reference wherever @apply/@variant is used, or switch to var(--token) entirely to skip Tailwind processing that file.