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.jsis gone. Configuration moves into CSS via@themeand@layerblocks. - 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 utilitiesdoing 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
contentarray.
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
darkon<html>from a small script that readslocalStorageandprefers-color-scheme. Style withdark: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-600and--color-primaryfor the same color. Audit@themequarterly. - Specificity surprises with
@layer. Utilities in@layer utilitiesare 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
@themegenerates 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.