Your Design Tokens Should Outlive Your CSS Framework
Tailwind Labs is joining Shopify. The announcement landed on the Tailwind blog, went to the top of Hacker News with more than a thousand points and four hundred comments, and then something notable happened: nothing. No build broke. No site went down. Tailwind v4 is still MIT licensed, still installs the same way, still compiles the same output it did the week before. Your design tokens are exactly where they were on Monday, which is the problem worth looking at.
That is exactly why this is the right moment to ask the question, because the worst time to find out where your design tokens live is the week something actually does change. An acquisition is not a crisis. It is a free rehearsal for one.
And Tailwind is not the only layer moving. Look across the WordPress styling stack right now and almost every part of it is being re-founded at the same time. This article is about what that means for the layer you control, and about a specific arrangement that makes framework churn a build-step problem instead of a rewrite.
What actually changed, and what did not
Being precise here matters, because most of the commentary has been about vibes.
What changed: Tailwind Labs, the company, is now part of Shopify. That affects who funds the work, who sets the roadmap, and which use cases get prioritised when two good ideas compete for the same maintainer week. Over a long enough horizon, that is the thing that shapes a tool.
What did not change: your node_modules. The license is the same. The v4 engine is the same. Every utility class in your templates does the same thing today that it did before the announcement. If your build ran on Monday it runs now.
So the honest framing is not “Tailwind is in danger.” It is that a tool a lot of WordPress theme work now depends on has a new owner whose primary product is a competing commerce platform, and that this is worth exactly one afternoon of thinking rather than either a panic or a shrug.
The useful question is not “will Tailwind get worse.” Nobody knows. The useful question is “if it did, how much of my work would I have to redo,” and that one you can answer today, about your own codebase, without predicting anything.
Every layer is moving at once
Zoom out from the acquisition and the pattern is harder to ignore. In the space of a few months:
- Tailwind changed owner.
- Divi 5 shipped a component and slot architecture, the biggest structural change that builder has made in years, along with variable font handling that rethinks how type is configured.
- WP Rig 3.5 is being described by its own community as the largest shift in how the framework works since it launched.
- Gutenberg is still openly working out what a stable 1.0 even means for its own UI component library, with active discussions about release criteria and about how far a block’s Global Styles selector should be scoped.
- shadcn/ui and MUI continue to trend as the default answers in React land, on two completely opposite ownership models.
None of these is a crisis on its own. Together they say something simple: the styling layer of a WordPress project is the least stable layer you build on, and it has been for years. Any decision that assumes one framework will still be the right answer in three years is a decision you will revisit.
Which means the durable question is not which framework to pick. It is where your decisions live, so that picking again is cheap.
The portability test
Here is the test, and you can run it on a project you already have open. Ask three questions.
One: where is your brand blue defined? Not where it is used, where it is defined. If the answer is a hex value in a Tailwind config, or in a Sass variables file, or worse, in several places that have quietly drifted apart, that value belongs to your framework rather than to your project.
Two: what happens in the block editor? If your colours and spacing are defined only in your framework’s config, the WordPress editor does not know about them. Your client opens a post, clicks a colour control, and sees a palette that has nothing to do with your design. Most teams paper over this by defining the palette twice, once for the framework and once for WordPress, and from that day the two are drifting.
Three: if you removed the framework tomorrow, what survives? If the honest answer is “the markup, and I would rewrite the styles,” your tokens were never really yours. If it is “everything except the utility classes,” you are in good shape.
Almost every project fails question two. That failure is not about Tailwind, it predates it, and it is the actual source of the pain that an acquisition headline makes you suddenly notice.
The one layer with no vendor
In a WordPress site, theme.json is the only place design decisions can live that is not owned by a company. It ships with core, it is maintained by the project, and it is the only styling layer that both the front end and the block editor read from the same source.
That last part is the one people underrate. When you declare a palette in theme.json, WordPress does three things automatically. It renders CSS custom properties into the page. It populates the editor’s colour, typography and spacing controls with exactly those values. And it keeps the two in agreement forever, because there is only one declaration.
The custom properties follow a fixed naming scheme. A colour with the slug brand-primary becomes:
--wp--preset--color--brand-primary
Font sizes and spacing follow the same shape, as --wp--preset--font-size--{slug} and --wp--preset--spacing--{slug}. Those names are verbose, and that verbosity is the point: they are generated by core from your declarations, so they are stable in a way a hand-written variable name in a config file is not.
We have covered the mechanics of building that layer properly in how to build a design token system in theme.json and how to implement a consistent spacing system. What those articles do not address, because the question had not been asked yet, is ownership. That is what follows.
Canonical source, generated config
The arrangement is one sentence: declare tokens once in theme.json, and have your framework consume the custom properties that WordPress generates from it.
Not defined twice. Not synced by a script you have to remember to run. Consumed.
With Tailwind v4 this is unusually clean, because v4 moved token definition into CSS through the @theme directive, and CSS can reference custom properties. So the framework’s tokens point at WordPress’s tokens:
@import "tailwindcss";
@theme {
--color-brand-primary: var(--wp--preset--color--brand-primary);
--color-brand-ink: var(--wp--preset--color--brand-ink);
--color-surface: var(--wp--preset--color--surface);
--spacing-gutter: var(--wp--preset--spacing--40);
--text-display: var(--wp--preset--font-size--display);
}
Now bg-brand-primary in a template and the brand colour your client picks in the editor are the same value, because they resolve through the same custom property, which came from the same line of theme.json. Change the hex in one place and both move together. The attowp piece on bridging Tailwind v4 @theme and theme.json goes deeper into the wiring if you want the full implementation.
The part that matters for this article is what that arrangement does to the portability test. Your framework config is now a mapping file, not a source of truth. It is maybe forty lines. If you replace the framework, you rewrite forty lines of mapping and your entire design system stays exactly where it was, because it never lived in the framework in the first place.
That is the whole trick. It is not clever, and it does not cost you anything on day one, which is precisely why it is worth doing before you need it.
One more thing the mapping layer buys you, which only shows up later. Because the framework config is now forty lines of pointers rather than a design document, it is reviewable. A pull request that changes a brand colour touches theme.json, and the diff says so in plain terms. A pull request that changes the mapping is doing something structural and deserves a second pair of eyes. When both live in the same sprawling config file, every change looks the same in review, which is how a palette quietly grows to sixty entries without anyone approving it.
Name roles, not values
There is a second half to this, and skipping it undoes most of the benefit.
A token named for what it looks like is tied to a moment. A token named for what it does is tied to your design.
Compare --color-blue-500 with --color-brand-primary. The first tells you a hue and a position on a scale, which is a framework’s way of thinking. The second tells you a job. When the brand shifts to green, the first becomes a lie that is now everywhere in your templates, and every developer who touches the project afterwards has to hold that lie in their head.
Semantic roles are what make the mapping layer possible at all. You cannot point --color-blue-500 at a WordPress palette entry in any meaningful way, because the palette does not think in hue scales. You can point --color-brand-primary at it trivially, because both sides are describing the same job.
A workable starting set is smaller than people expect. A primary and its contrast text. A surface and a raised surface. A body text colour and a muted one. A border. One accent for state, one for danger. Nine roles will carry most sites, and nine is a set you can actually hold in your head, which matters more than completeness.
Resist adding a role until a design decision genuinely needs one. Token systems fail from bloat far more often than from gaps, and a palette with sixty entries is one nobody uses correctly.
Design tokens that change: dark mode and style variations
The strongest argument for this arrangement is not the acquisition. It is what happens when you need two versions of the same design.
Block themes support style variations: additional JSON files in the theme’s styles/ folder, each one able to override any part of the token set. The site owner picks one in the Site Editor and the whole site changes. No build step, no deploy, no developer.
If your tokens live in theme.json, a variation is a file that redeclares nine colour roles. Everything downstream follows automatically, including your framework utilities, because they resolve through the custom properties the variation just changed. A dark variant becomes a small JSON file rather than a parallel set of utility classes and a dark: prefix on every element that needs one.
If your tokens live in a framework config, you cannot do this at all. Config is compiled at build time and the site owner has no access to it, so the variation has to be expressed as a second set of classes shipped in the same stylesheet and toggled at runtime. That works, it is what most projects do, and it is roughly twice the CSS and permanently twice the maintenance.
There is a related detail worth knowing. Alongside the preset arrays, theme.json has a settings.custom object for values that are not colours, sizes or spacing. Anything you put there is emitted as a custom property too, under --wp--custom--, with camelCase keys converted to kebab-case. That is where things like a border radius scale, a shadow set, or a transition duration belong: still core-generated, still overridable by a style variation, still not owned by anyone’s framework.
We covered the mechanics of variations from the design side in custom block styles that extend the WordPress design system. The ownership argument is the same one as above, which is that a variation only works if the thing it overrides is the actual source.
The same test, applied to everything else
Once you have the test, run it across the rest of the stack. The answers vary more than you would think.
shadcn/ui passes for an unusual reason: you copy the components into your own repository. There is no dependency to be acquired, because you already own the code. The tradeoff is that you also own the maintenance, including the bugs.
MUI is the opposite. Deeply capable, and its theming system is genuinely good, but your design decisions live inside its theme object and its component API. Leaving means rewriting, and that is a known, accepted cost rather than a hidden one.
Divi 5 has made a real architectural improvement with components and slots, and that is worth taking seriously. But the design decisions still live in the builder’s own data. The portability answer is the same as it always was for a page builder: what you can export is markup, not a system.
Gutenberg’s own component library is the interesting case, because it is still deciding what it is. The active discussion about criteria for a stable 1.0 is a signal worth reading rather than ignoring. Core-maintained is not the same as stable-and-finished, and treating a pre-1.0 API as a foundation is its own kind of bet.
The point of running the test on each is not to produce a ranking. It is that “what happens if this goes away” should be a known number for every layer you depend on, rather than a question you discover the answer to under deadline.
What this costs you
Being honest about the tradeoffs, because there are some.
You lose a little of the framework’s ergonomics. Tailwind’s default palette is well designed, and opting out of it in favour of nine semantic roles means giving up the convenience of reaching for slate-700 without thinking. That convenience is real, and giving it up is a genuine cost, not an imaginary one.
You take on a naming problem. Semantic roles require agreement about what things are called, and that agreement is a design conversation, not a technical one. Teams that cannot have that conversation will produce a token set worse than the framework default.
You add one indirection. A developer reading bg-brand-primary has to know it resolves through a custom property to theme.json. That is one hop, it is documentable, and it is the thing you are buying the portability with.
What you do not pay is a performance cost. CSS custom properties resolve in the browser at effectively no cost at this scale, and the generated stylesheet is the same size it would otherwise be.
Migrating a project that already picked wrong
Most real projects are not starting fresh. They have a Tailwind or Sass config with forty colours in it, a theme.json that was filled in once and forgotten, and templates using both. Here is the order that causes the least disruption.
Do not start by deleting anything. Add the theme.json declarations alongside what already exists. Both systems coexist happily for a while, because they produce different CSS custom properties and nothing collides.
Map before you migrate. Write the nine semantic roles and, next to each, the existing value it corresponds to. This is where you discover that primary, brand and blue-600 are three names for two slightly different hex values, and that one of them is a typo somebody shipped eighteen months ago. Resolving that is most of the work, and it is design work rather than code.
Point the framework at the new source, one role at a time. Change --color-brand-primary to reference the WordPress custom property while every other token still points at its old literal. If something looks wrong, exactly one role is implicated. Migrating all nine at once turns a five-minute diagnosis into an afternoon.
Delete the old definitions last, and only once nothing references them. A quick search for the old variable names across templates, blocks and any editor stylesheet will tell you when that is true.
Two gotchas worth knowing before you hit them. First, if your theme registers a separate editor stylesheet, it needs the same custom properties available or the editor will render your blocks without them, which looks like the migration broke something when it only broke the editor’s copy. Second, fluid typography defined in theme.json generates clamp() values rather than fixed sizes, so a framework token pointed at a font-size preset inherits the fluid behaviour. That is usually what you want, and it is startling the first time if you did not expect it.
What to do this quarter
Concrete, in order.
- Find every place a brand colour is defined in one project. Not used, defined. Most teams find three. Some find six. The number itself is the finding.
- Write the nine roles down before touching any code. Argue about the names now, while it is cheap.
- Declare them once in
theme.jsonand confirm the editor controls show your palette. If your client opens a post and sees your colours in the picker, the hardest part is done. - Reduce your framework config to a mapping. If it still contains a hex value after this step, that value is still owned by the framework.
- Time the swap. Not as a plan to leave, as a measurement. How long would it take? If the answer is days rather than weeks, you are finished. If it is weeks, you have found the work.
If you want to see what the finished arrangement looks like in a shipped theme, our piece on WordPress having two design systems covers the part of this most teams hit next, which is that the admin and the front end do not share a token layer, and that gap is not one theme.json can close for you.
The actual lesson
Tailwind joining Shopify probably will not hurt you. Most acquisitions of this kind are uneventful for existing users, and the people who write the code are usually the same people the week after.
But the reason it does not hurt you should be that you arranged things so it could not, rather than that it happened to be fine. Those two states look identical right up until the moment they do not, and only one of them is a decision you made.
Your design tokens should outlive your CSS framework. Not because any particular framework is going away, but because over the life of a site, several of them will, and the ones that survive that are the projects where the design system was never inside the framework to begin with.