Skip to content

Build1 publisher3 min readPublished

Tailwind v4 moves your tokens into CSS. The token name is now the API.

Dropping tailwind.config.js for a CSS @theme block buys you tokens that any tool can read. It also means renaming every token in a shared design system to fit Tailwind's namespaces.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • Tailwind CSS v4 dropped tailwind.config.js in favour of a plain CSS file.
  • The author argues the CSS-file approach is better for marketing sites because design tokens live in one place that every tool - the browser, Figma tokens plugins, the CMS preview - can read.
  • Anything inside the @theme block becomes a Tailwind utility automatically (bg-brand-500, text-brand-600, rounded-card, shadow-card), with no config file and no plugin.
  • The same variables declared in @theme are available to arbitrary CSS via var(--color-brand-500) for edge cases where a utility class will not reach.
  • The author's starting file is app/globals.css containing @import "tailwindcss"; followed by an @theme block.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

Tailwind CSS v4 dropped `tailwind.config.js` in favour of a plain CSS file [1]. On a one-person marketing build that is an afternoon of tidying; on a shared design system it is a rename of everything you own, because in v4 a token's name is its public API.

The mechanism fits on one screen. You import Tailwind, open an `@theme` block, and anything declared inside it becomes a utility automatically, with no config file and no plugin [3]. The same declarations remain available to hand-written CSS through `var(--color-brand-500)` for cases a utility will not reach [4]. The starting file in the dev.to writeup by Nayan Kyada holds ten declarations: four colours in `oklch`, two font stacks, two spacing values, a radius and a shadow [5][6][1].

The mapping is mechanical and unforgiving. `--color-brand-500` yields `bg-brand-500`, `--radius-card` yields `rounded-card`, `--shadow-card` yields `shadow-card` [3][2]. The namespace prefix selects which family of utilities gets generated, so if your existing tokens are exported from a JS object or a Figma variables file under names like `brand.primary`, the migration is not a paste. Every name has to be restated in the form Tailwind will parse, and the design team has to accept that the CSS name and the utility name are the same decision made once.

The escape hatch is visible even in the author's own setup. Two of the ten declarations, `--spacing-container` and `--spacing-section`, are not consumed as generated utilities at all; the `Section` wrapper reaches them with `max-w-[var(--spacing-container)]` and `py-[var(--spacing-section)]` [7][3]. That is 80rem of container and 5rem of vertical rhythm, per the file's own comments [6]. So "no config file, no plugin" still leaves arbitrary-value brackets in the markup for tokens that do not land cleanly in a utility family.

Fonts show the other seam. `--font-sans` is declared in the `@theme` block as `"Inter", ui-sans-serif, system-ui, sans-serif` [6], and then declared again at runtime by `next/font/google`, which is configured with `variable: "--font-sans"` and `display: "swap"` specifically so Tailwind picks up the loaded face [8][4]. The handoff is deliberate, but it means the token file is not the final word on that value; the framework supplies it.

The payoff, as argued in the piece, is that tokens sit in one place that the browser, Figma token plugins and a CMS preview can all read [2]. Worth noting what the article actually shows: the token file and the Next.js code around it, not a working Figma or CMS pipeline [9]. The rest of the workflow is conventional and cheap to copy: `create-next-app` with `--typescript --tailwind --app --turbopack` [10], `globals.css` stripped back to the import line before the `@theme` block goes in [11], a `cn` helper built from `clsx` and `tailwind-merge` to stop override collisions [12], and a claimed sub-200ms Turbopack HMR on component edits [13].

What to watch on a real migration: the tokens with no obvious home among the prefixes shown here, such as motion durations or stacking layers. The count of `var(--...)` strings inside class attributes is the honest metric. If it climbs, the naming did not survive the move, and you have swapped a config file for a second, less searchable one.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories