Atomic CSS and Tailwind: is this actually a scalable layout stack — or just a fast one?
Marcus Webb
September 18, 2026
I shipped Tailwind on a UI that outgrew the marketing site it started as. The first month was the fastest layout work I have done in CSS. The twelfth month was a pull request that touched 80 files to change a radius. Atomic CSS is a real idea: one class, one job, compose in the markup. Tailwind is the industrial version. The question is not whether it is fast to start. It is whether it stays a layout stack when the design system grows a spine — or whether it stays a sprint tool that you apologize for later.
I have used Tachyons, a home-grown utility sheet, CSS modules, vanilla-extract, and styled-components I will not add again. I still reach for Tailwind on a lot of product UI. I do not reach for it as a religion. Here is the split I actually use.
What “atomic” bought me
I stopped inventing class names for one-off spacing. flex items-center gap-3 px-4 is a sentence I can read in the JSX. I did not bounce to a CSS file to remember what .card-header--compact did. For a team that lives in components, that locality is speed. It is also how a file becomes a novel of classes.
The constraint of a scale — spacing, colors, radii from a config — is the actual design system. I have liked that more than I liked the utilities. A theme in tailwind.config that matches the Figma tokens is a stack. Random hex in a style tag is not. If I cannot name the tokens, Tailwind is just a faster way to be inconsistent.
Purge / content scanning kept the CSS small on a marketing site. On a large app with dynamic class names I had to stop concatenating strings (bg-${color}) because the scanner cannot see them. That is a real constraint. I keep a safelist for the few dynamics. I do not generate class names in a loop. That loop is how the bundle grows and the scanner lies.

Where it scaled
Component libraries. A Button that maps variants to a cva (class-variance-authority) or a tv recipe is a scalable atomic stack. The markup of the app uses <Button variant="danger" />, not a salad. The salad lives in one file. That is the pattern I would repeat. Tailwind without that layer is how you get twenty slightly different buttons.
Responsive layouts with the same utilities. md:grid-cols-3 is clearer to my teammates than a nest of media queries they will not open. I still write a custom breakpoint when the design is not a column count.
Dark mode as a class strategy I can test. I have shipped dark: variants and a simple toggle. I have also shipped a token file in CSS variables and used Tailwind as a thin map onto those variables. The variables are the scalable part. Tailwind is the keyboard.
Where it was just fast, and then expensive
A 200-line class string on a table cell. Nobody can review the visual intent. I extract a component or I extract a @apply in a CSS module for that island. I use @apply rarely. It is a smell that I wanted a semantic class. Sometimes I did.
Designers who do not read JSX. The Figma file and the markup drift. Tokens in config help. A weekly “does this radius match” does not happen. I have hired a designer who would not open the PR. We moved the visual source of truth back to a smaller set of components they could inspect in Storybook. Tailwind stayed inside those components. The app stopped inventing utilities at the call site.
A multi-brand white-label. Three themes in one Tailwind config became a fight. CSS variables plus a small set of semantic utilities scaled better. I would still use Tailwind for the chrome. I would not encode brand A and brand B as parallel color names in one config if I can avoid it.
Email HTML. I do not use Tailwind for email. I use a table and a few inline styles. That is not a joke. It is a different runtime.

CSS modules and “real CSS” I still want
For a distinctive marketing layout — a magazine, a docs home with a weird grid — I want CSS. Grid, subgrid, container queries, anchor positioning. Utilities will get there; they are often a week behind the property I want. I write the CSS. I do not wait for a plugin.
For a design system that is the product, I want semantic classes or variants with names the org already uses: surface.raised, text.muted. Tailwind can map to those via theme keys. If the team thinks in “slate-500,” we have trained them on Tailwind’s palette, not ours. I rename the tokens early. I did that too late once. The salad tasted like Tailwind, not like the brand.
Runtime CSS-in-JS I still will not add. I said so in the frontend 2026 bet. Atomic CSS is not that. Atomic CSS is compile-time or a static sheet. That is why I tolerate it. Emotion fighting RSC is a different tax.
Performance, specificity, and the cascade I still want
Utilities win specificity fights by being boring: most of them are single-class. I have been bitten less by “why is this blue” than I was in a 2,000-line sheet with nested BEM. I have been bitten more by “which of these twelve padding classes is the one we meant.” Readability moved from the stylesheet to the markup. That is a trade, not a free lunch. On a team that lives in the component, it is the right trade. On a team that hands HTML to a CMS, it is a mess. I do not put Tailwind in a WordPress theme a marketer will edit. I put it in a React or Svelte tree a programmer will edit.
Bundle size is fine if the scanner sees the classes. It is not fine if I build a page from a CMS field that injects arbitrary class strings. I treat user-supplied classes as hostile. I map a small allowlist. I do not let a rich-text field become a Tailwind injection point.
I still want the cascade for prose. @tailwindcss/typography (the prose plugin) is how I style markdown I do not want to wrap in utilities. A docs article should not be a pile of text-sm leading-7 on every <p>. I use prose. I customize the prose tokens. That is CSS. Tailwind just boots it.
Container queries are why I sometimes leave utilities. A card that changes layout based on its own width is not md:. md: is the viewport. I have written @container in a module and used a tiny bit of CSS because the utility story was behind. I will not wait for a plugin to ship a layout I can write in eight lines.
How I undo class soup without a rewrite
I do not migrate off Tailwind in a quarter. I pick the worst file — usually a settings page or a table — and I extract the repeated clusters. Three buttons become one. Two cards become one. The PR shrinks the string length and adds a Storybook story. I do this when I already have to touch the file. I do not schedule a “cleanup epic” that never ships.
If a whole surface is soup and the design just changed, I rewrite that surface in components and I leave the rest. Rewrites of CSS strategy are how you lose a month and keep both systems. I have kept both on purpose for a while. I will not keep both as a plan.
A decision rule I use on a new repo
If the UI is a product of many similar components and the team already thinks in utilities: Tailwind plus CVA plus tokens. If the UI is a few distinctive pages: CSS modules or vanilla-extract. If the team is backend-heavy and ships an admin: Tailwind, because they will not write a cascade they understand. If someone proposes a new utility framework to replace Tailwind for taste: I say no. The stack is the tokens and the components. The keyboard can stay.
Is it a scalable layout stack? It is a scalable composition stack if you put a component boundary around the salad. It is only a fast stack if every file is allowed to invent a layout. I have shipped both. I would only call the first one an architecture. The second is a sprint. Sprints end. The class names do not, unless you delete them on purpose.
I still start many apps in Tailwind. I still extract the third copy of a cluster into a component. I still write real CSS when the layout is the point. That mix is the stack. Tailwind alone is the accelerator. I will not confuse the two just because the first week felt like flying. Fast is a gift. Scale is a habit of extracting the third copy. I still want both. I will not pretend the gift is the habit.