We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide12 min read

Tailwind CSS 4, or configuration in CSS

Tailwind 4 dropped the JavaScript config file for CSS. The new engine, browser requirements, migrating from version three, and the traps along the way.

Tailwind CSS 4, or configuration in CSS

Tailwind changed how styles are written in web applications, replacing named classes with a set of small utility classes composed directly in markup. Version four changed how you work with the tool itself, and more than the number suggests.

The largest change is the disappearance of the JavaScript configuration file. Colours, spacing, fonts, and everything else are now described directly in a stylesheet, as CSS variables. A JavaScript file still works if you point at it, but it stopped being the default route.

The second change concerns the engine, rewritten from scratch in Rust. Full builds are several times faster, and incremental rebuilds on file save are near instant.

Configuration in a stylesheet

Rather than exporting a settings object, you describe them in CSS, in a block reserved for the theme.

Code
CSS
@import "tailwindcss";

@theme {
  --color-brand: #0f766e;
  --color-brand-light: #14b8a6;
  --font-heading: "Inter Variable", sans-serif;
  --spacing-section: 6rem;
}

Utility classes arise from those values, so a colour defined this way is available everywhere a colour is expected.

The advantage is larger than it looks and reaches beyond convenience. The values are ordinary CSS variables, so they appear in developer tools, can be read from JavaScript, and can be overridden in a specific place without rebuilding the project.

That solves a problem the previous approach required working around. A dark theme, a theme for a particular client, or a colour variant for one section reduces to overriding variables within a chosen scope, rather than generating separate class sets.

Beyond the theme, three more elements are available: defining your own utility classes, defining your own variants, and naming plugins. All in the same file, alongside the rest of your styles.

Code
CSS
@utility card-shadow {
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.06), 0 8px 24px rgb(0 0 0 / 0.08);
}

@custom-variant dark (&:where(.dark, .dark *));

@plugin "@tailwindcss/typography";

A custom variant is the most commonly skipped of the three, and it solves a concrete problem: it lets you describe a state or a scope once and use it like any built in prefix, instead of repeating the same selector in a dozen places.

The directive name misleads here and deserves memorising exactly. A variant is defined with @custom-variant, while the @variant directive itself does something else: it applies an existing variant to rules inside an ordinary CSS block. The shorter name comes from the version four prereleases, so tutorials written back then give a form that no longer works.

Browser requirements

This is the most important caveat with this version and something to check before starting a migration rather than after.

Version four requires newer browsers: Safari from version sixteen point four, Chrome from version one hundred and eleven, and Firefox from version one hundred and twenty eight. Older ones simply will not render the styles correctly.

The reason is technical and worth understanding, since it settles the matter. The tool relies on CSS mechanisms that cannot be substituted: registering custom properties and mixing colours. Those are not things for which a fallback for older browsers exists, so there is no workaround.

The practical conclusion is simple and inconvenient. If your application must work on older browsers, version four is out and you stay on version three. That is not a matter of convenience but of technical impossibility.

Check that against real traffic data rather than assumptions. If your project holds a supported browsers list, one command answers the question directly.

Code
Bash
npx browserslist "> 0.5% in PL, last 2 versions, not dead"

On an application for external customers, particularly in the public sector or in countries with slower device replacement, the share of older browsers is often higher than intuition suggests. If the output names Safari below sixteen or Chrome below one hundred and eleven, the decision is made and there is no point arguing with it through workarounds.

Migrating from version three

The vendor supplies a tool automating the transition and starting there pays off, since it handles most of the mechanical work.

Code
Bash
git switch -c tailwind-migration
npx @tailwindcss/upgrade
git diff --stat

A separate branch before running it is not excessive caution. The tool edits template files across the whole repository, and reviewing a thousand changed lines is considerably easier when you can compare them against a starting point and undo them with one command.

It swaps dependencies, moves configuration from the JavaScript file into the stylesheet, and fixes class names in templates. It requires a newer runtime version, worth checking before you run it.

A few things remain to do by hand and they are what takes time.

The first is renamed classes. Separate opacity classes disappeared in favour of a notation combining a colour with opacity. Some flexbox related class names changed too. The tool fixes those in templates while not reaching names built dynamically in code.

The second is custom utility classes defined in the layer reserved for utilities. In the new version they stop responding to variants, so a class works until you try using it with a hover prefix. The answer is moving them into the directive intended for that.

The third is style ordering. The new version uses cascade layers, and styles outside layers take precedence over those inside them. That means a stylesheet included alongside, from a component library for instance, can start overriding things Tailwind previously overrode.

Practical advice: migrate on a separate branch and review the appearance visually rather than only checking that the project builds. Changes in style ordering report no errors; they simply shift things by a few pixels in places nobody planned.

Installation and build tools

How you attach it changed with the version, and knowing the right route for your project pays off, since older tutorials describe an outdated one.

With newer generation build tools a dedicated plugin is available, noticeably faster than going through a style processing layer. That is the recommended route for projects on Vite and everything built on it.

With Next.js and other frameworks using a style processing layer, attachment happens through a separate plugin package. Note one thing: in version four the plugin was split into its own package, so configuration copied from an older tutorial simply will not work.

The need to state where classes live disappeared too. The previous version required listing paths to template files; the current one detects them itself, skipping what is excluded from version control. That removes a whole category of problems with classes that were never generated because somebody forgot to add a directory.

The import is now a single line rather than three directives. A detail, though easy to miss during migration, and the old directives stopped being recognised.

Cascade layers and coexisting with other CSS

This part causes trouble on projects where Tailwind is not the only source of styles.

The new version places its styles in cascade layers, which orders things inside the tool and introduces one consequence worth remembering. Styles outside any layer take precedence over those inside layers, regardless of document order and regardless of selector specificity.

In practice that means a stylesheet from a component library, included alongside, starts winning against utility classes. The symptom misleads: the class is in the markup, visible in developer tools, and nonetheless has no effect.

There are two answers. The first is wrapping the third party stylesheet in a layer of your own at import, bringing it into the same ordering mechanism. The second is stating layer order explicitly at the top of your stylesheet, so it does not depend on import order.

When working with ready component kits, the one covered in the piece on Mantine for instance, settle this at the project's start. Later it means hunting through styles for places where something overrides something else for no visible reason.

Tailwind against the alternatives

OptionStrengthWeaknessPick it when
TailwindFast writing, consistency, no dead stylesCluttered markup, browser requirementsA project built from components
CSS modulesOrdinary CSS, full control, no dependencyNaming classes, more filesA team preferring the classic approach
Styles in the componentStyles beside logic, dynamic valuesRuntime cost, lower popularityHeavily dynamic styling
A ready component kitAppearance included, fewer decisionsHarder to depart from the imposed styleAn admin panel, an internal application

The first row wins on a project assembled from components, since cluttered markup stops mattering there: the classes sit in one place, in a component's definition, rather than scattered across the project.

It loses on pages written directly in markup, without a component layer. Thirty classes on one element is then a real problem rather than an aesthetic detail.

Remember that the first row often accompanies the last. Kits such as shadcn/ui and daisyUI are built on this layer, so choosing one does not exclude the other.

Daily practice

A few things that over longer work separate a readable project from a tangle of classes.

Extract repeated class sets into components rather than into your own shorthand classes. A component carries structure and behaviour along with it, while a shorthand class carries only appearance, so the first scales better and creates no parallel naming system of its own.

When a project does not sit on React, attribute driven kits save a lot of work. Preline UI offers Tailwind components that run without any framework, so it drops equally into Astro, a server side template, or a plain HTML file. Just check the licence, since some kits split a free and a commercial edition.

Compose conditional classes with a tool built for it rather than by joining strings. Manual concatenation leads to two contradictory classes landing on the same element, with whichever appears later in the generated stylesheet winning.

Do not build class names dynamically from fragments. The tool scans code for complete names, so a name assembled from a prefix and a variable will not be detected and the class will not be generated. That is the most common cause of a style that works locally and vanishes after a build.

Use values outside the scale sparingly. A few such places are fine; several dozen mean the scale is badly chosen and the theme deserves fixing rather than working around each time.

Standardise class order automatically, with a plugin for your formatter. That removes a whole category of code review discussion and makes repository diffs show actual changes rather than reordered classes.

The editor plugin suggesting classes is worth using too. Beyond completing names it shows which style sits behind a given class, which while learning shortens the path to understanding the spacing scale more than reading documentation does. On a team of mixed experience that is one of the few things which genuinely speeds up onboarding.

The last thing concerns dark mode. Decide at the start whether it follows the system setting or a user choice stored in the application. Changing that decision later means reviewing every place using the dark variant, since the two mechanisms require different markup.

Common mistakes

The first is migrating to version four without checking browser requirements. Older browsers will not render the styles, and no fallback exists.

The second is class names built dynamically from fragments. The tool will not detect them, so the class is never generated, and the problem surfaces only after a build.

The third is custom utility classes left in the former layer after migration. They stop responding to variants, which shows up as a working class with a broken hover state.

The fourth is checking only that the project builds after migration. Changes in style ordering report no errors; they shift things visually.

The fifth is creating your own shorthand classes instead of components. That produces a parallel naming system, meaning exactly what this tool was meant to free you from.

The sixth is overusing values outside the scale. Several dozen such places mean the theme is badly chosen and deserves fixing.

The seventh is pulling in somebody else's stylesheet alongside without assigning it a layer. Styles outside the layers beat utility classes, and the symptom is a class visible in the page source that does nothing for no apparent reason.

FAQ

What did version four change?

Above all where configuration lives: colours, spacing, and fonts are now described in a stylesheet as CSS variables rather than in a JavaScript file. The engine was rewritten, making builds noticeably faster and rebuilds on file save near instant.

Does the JavaScript configuration file still work?

Yes, provided you point at it explicitly from the stylesheet. It stopped being the default route, though, and new capabilities are described in CSS, so on a new project start with the new approach directly.

Can I migrate with users on older browsers?

No. Version four requires newer browser versions and relies on mechanisms no fallback can substitute. Under a requirement to support older devices you stay on version three.

Why does my class not work after a build?

Most often because its name is assembled dynamically from fragments. The tool scans code for complete names, so a name built from a variable will not be detected. The answer is writing complete names in code and choosing between them.

Does Tailwind suit large projects?

Yes, provided you work with components. Classes gathered in component definitions stay readable; scattered directly through markup they stop being so. That is the main criterion rather than project size itself.

Documentation sits on the project site, and moving from the previous version is described in the upgrade guide.