Overview

Tailwind v4 is a meaningful break from v3. The config moved into CSS, the engine got faster, and several v3 escape hatches went away. This page covers the v4 conventions that matter day to day.

What changed from v3

Know these before you touch a v4 project.

  • The JavaScript tailwind.config.js is gone. Configuration moves into CSS via @theme and @layer blocks.
  • The Vite plugin (@tailwindcss/vite) is the recommended integration. The CLI still works for non-Vite stacks.
  • Native CSS cascade layers replace the old internal layering. You will see @layer base, @layer components, @layer utilities doing real CSS work, not Tailwind-internal bookkeeping.
  • The default color palette is wider and the default font stack is more conservative.
  • Content detection is automatic; no content array.

CSS-first config

Configure tokens and theme values in your main CSS file with @theme.

@import "tailwindcss";
 
@theme {
  --color-brand: oklch(0.72 0.18 264);
  --color-ink: oklch(0.2 0 0);
  --font-display: "Schibsted Grotesk", system-ui, sans-serif;
  --radius-card: 0.75rem;
}

The generated utility classes follow the token names: bg-brand, text-ink, font-display, rounded-card. Keep the token list short. Every new token is a maintenance cost.

Utility-first composition

Compose UI inline with utilities. Extract a component when, and only when, the same combination of utilities appears in three or more places and you want to change it as a unit.

  • One copy of a button: leave it inline.
  • A list of cards using the same shell: extract to a component.
  • A class string longer than ~12 utilities on one element: extract to a component or split into nested elements with clearer roles.

@apply is mostly an anti-pattern

Each @apply is a place where your CSS hides your component contract. Avoid it.

Cases where @apply is still the right answer:

  • Styling third-party markup you cannot annotate (a CMS-rendered article body, a vendor widget’s DOM).
  • Defining a single named utility group that is used by non-JSX templates and that you genuinely want to version separately.

Outside those cases, prefer a component. If a class list is too long to read, the problem is the class list, not the lack of @apply.

Custom properties via @theme

Every value in @theme becomes a CSS custom property at the document root. You can read it from non-Tailwind CSS, from JS, or from inline styles.

.callout {
  border-left: 4px solid var(--color-brand);
}

This means design tokens written once in @theme work for both Tailwind utilities and bespoke CSS. Use this when you need a value Tailwind cannot express cleanly, like a CSS gradient or a clip-path.

Dark mode strategy

Pick one strategy and stick with it.

  • Class-based (recommended). Toggle dark on <html> from a small script that reads localStorage and prefers-color-scheme. Style with dark: variants. Lets users override the system preference.
  • Media-query only. Style with @media (prefers-color-scheme: dark). Smaller, simpler, but no user override.

Define both light and dark token values inside @theme with @media blocks if you want the tokens themselves to flip:

@theme {
  --color-surface: white;
}
@media (prefers-color-scheme: dark) {
  @theme {
    --color-surface: #0b0b0c;
  }
}

Arbitrary values are a smell

bg-[#3a7], mt-[37px], w-[412px] are escape hatches. Each one is a token you decided not to add to @theme. A handful per project is fine. A page full of them means the design tokens are wrong, and the fix is to add the missing token, not to keep typing brackets.

Prettier plugin for class sorting

Install prettier-plugin-tailwindcss. It sorts utility classes in a canonical order. The order is not arbitrary; it groups by category (layout, spacing, typography, color) which makes diffs readable and review faster.

Add it once, commit the resulting churn, and never debate class order again.

Common pitfalls

  • Token drift. Two designers add --color-blue-600 and --color-primary for the same color. Audit @theme quarterly.
  • Specificity surprises with @layer. Utilities in @layer utilities are now real CSS layer utilities. A rule outside any layer wins over a layered rule. If a utility is not applying, check whether the overriding rule is unlayered.
  • Build size from forgotten tokens. Every value in @theme generates utilities. Removing a token shrinks the build; adding one bloats it.
  • Class string explosion on one element. When a single element has 30 utilities, the markup stops being readable. Split into wrapper + content, or extract a component.
  • Reaching for plugins too early. Most v3 plugins (forms, typography, container queries) became built-in or first-class in v4. Check the v4 docs before installing third-party plugins.