Web Design and Engineering

Why we removed Tailwind from production: a CSS-only design system

After years of running Tailwind alongside our own design system, we removed it from production. Here is the architecture that replaced it and what changed.

May 1, 20268 min read
Why we removed Tailwind from production: a CSS-only design system

A CSS-only design system is a component library distributed as native CSS, where every visual decision lives in a token, a class, or a cascade layer, and nothing depends on a JavaScript framework or an HTML utility-class language. It is what we ship in production today across every Studio project, after years of running Tailwind in parallel with our own components.

This article is the story of why we stopped reaching for Tailwind, and why "remove Tailwind" turned out to be cheaper than "scale Tailwind" once the design system passed a hundred consumers. It is opinionated. It is also reversible: if your team is two people and your product ships in three months, Tailwind is probably the right answer for you. The reasoning below applies to teams that own a product line or sell to multi-tenant customers.

The problem: utility-class debt at scale

Tailwind sells a clean trade. You write classes, the compiler tree-shakes the unused ones, the production bundle stays small. The Tailwind documentation cites typical apps shipping between 5 and 15 KB of gzipped CSS. That part is accurate.

The hidden cost lives elsewhere. CSS shrinks; HTML grows. In a real-world account from the community, one project saw CSS go from 45 KB to 8 KB after migrating to Tailwind, but HTML grew from 120 KB to 340 KB, a net increase of 183 KB. The classes have to live somewhere. They live in the markup, repeated across every component, every page, every locale variant.

The second cost is reading time. A button with eight states in Tailwind looks like sixty utility classes interrupted by template logic. To answer a basic question (what does this button look like in dark mode?) the reader scans every class, mentally parses prefixes, and reconstructs the cascade in their head. We measured this in code review. Reviews of utility-class components took longer per line than reviews of equivalent BEM-class components, and the divergence grew with component complexity.

The third cost is portability. Once the codebase has 30,000 utility-class strings, leaving Tailwind is not a Friday refactor. It is a quarter of work. Lock-in has a price even if you intend to stay.

Why the obvious fix did not work

The obvious fix is "Tailwind plus components": use @apply to extract repeated class strings into named classes, keep utilities for one-offs. We tried it. It collapses for two reasons.

First, @apply reintroduces the cascade problem Tailwind set out to remove. You now have a custom class that wraps utility classes, which means specificity, override order, and source-of-truth confusion are back. You traded one set of cascade decisions for two layers of them.

Second, the team splits in half. Half the developers write utility classes inline, half extract them, and the design system stops being a single language. Code review turns into a style debate. The DS Ops audit work we run weekly across our consumer projects shows that mixed Tailwind plus component-class projects accumulate override drift faster than either pure approach.

What actually works: native CSS in three layers

Our production architecture has three layers, all native CSS, all distributed through a single npm package.

Layer 1: design tokens

Around 140 custom properties cover the entire visual surface: color, spacing, radius, typography, shadow, motion. Tokens are flat strings, namespaced with --ds-*, defined once at :root and overridden in a [data-theme="dark"] block. Every component consumes tokens; nothing hardcodes a value. Migrating from one brand to the next is a token swap, not a CSS rewrite. We covered the trade-offs of this approach in design tokens vs CSS variables vs Tailwind.

Layer 2: components

About 60 components ship as plain CSS classes prefixed ds-*: .ds-btn, .ds-card, .ds-input. Each component owns its own block in the stylesheet, uses tokens for every value, and exposes modifiers (.ds-btn--ghost, .ds-card--bordered) instead of utility composition. A reader who knows the token names and reads ds-btn--ghost can predict the visual outcome without opening the file.

Layer 3: utilities

A short list of layout and state utilities (ds-flex, ds-grid, ds-hover-row, ds-focus-ring) covers cases where a one-off rule would be wasteful as a full component. The list is closed and audited. We do not ship ds-text-2xl, ds-px-4, or any sizing utility; those values come from component modifiers. The utility surface stays small enough that a developer can hold it in their head.

The three layers are wired together using CSS cascade layers, which now have over 96% global browser support. We declare the order once at the top of the package: @layer reset, tokens, base, components, utilities;. Specificity wars stop existing because the layer order, not the selector, decides who wins.

Browser support is the part that changed

This architecture would have been painful in 2022. It is straightforward in 2026. Native CSS nesting reached Baseline Widely Available in 2026, after becoming Newly Available in August 2023. Cascade layers crossed 96% support. color-mix(), oklch(), container queries, and :has() all stabilized within the same window. Native CSS in 2026 covers about 90% of what teams reached for Sass, PostCSS, or Tailwind to do in 2020. We mapped the full surface in modern CSS in 2026.

If your support matrix excludes browsers older than 2024, native CSS is a complete language. Most B2B SaaS targets meet that floor.

The Tailwind ecosystem signal we did not ignore

In January 2026, Tailwind Labs cut its engineering team from four to one, citing roughly 40% traffic decline since 2023 and an 80% revenue collapse. The framework itself is widely used and v4 with the Oxide engine is genuinely fast. The business model behind it (paid components, paid tooling) eroded as AI assistants started serving Tailwind documentation directly to developers. We do not enjoy team layoffs. We do read them as a signal about where stewardship and roadmap stability are heading.

A design system that lives in a vendor's release cadence is exposed to the vendor's commercial reality. A design system that lives in native CSS is exposed only to the browser standards process, which moves slowly and predictably.

What this looks like in practice

Here is how a button definition reads in our system, condensed.

@layer components {
  .ds-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-2);
    padding: var(--ds-space-2) var(--ds-space-4);
    border-radius: var(--ds-radius-md);
    background: var(--ds-color-bg-action);
    color: var(--ds-color-fg-on-action);
    font: var(--ds-font-body-sm);
    transition: background var(--ds-motion-fast);

    &:hover { background: var(--ds-color-bg-action-hover); }
    &[disabled] { opacity: 0.6; pointer-events: none; }
  }

  .ds-btn--ghost {
    background: transparent;
    color: var(--ds-color-fg-default);
    box-shadow: inset 0 0 0 1px var(--ds-color-border-subtle);
  }
}

Reading that block tells you everything: which variables drive it, which interactive states exist, which dark-mode override will happen automatically through the token swap. There is no class soup in the markup, no JavaScript runtime, no PostCSS plugin chain to debug.

The trade-off is visible too. You write more CSS. The package surface area grows. You need governance, an audit cadence, and a maintainer who cares. We run a weekly DS audit across every consumer project and it catches drift before it compounds. If your team cannot commit to that cadence, Tailwind's "it just works" is genuinely safer.

How to evaluate this for your team

Three questions decide whether the migration is worth your quarter.

Are you shipping more than one product surface? One site, one app, one marketing page can stay on Tailwind forever and never feel pain. Two products with shared components start paying interest on every utility class that lives in two markup trees.

Do you have a designer who thinks in tokens? The whole architecture rests on a token list a designer maintains alongside the engineering team. Without that, the CSS-only approach drifts back into ad hoc values and you lose the consistency benefit.

Is your support matrix flexible? If you must support Safari 15 or Firefox ESR pinned to 2022, cascade layers and native nesting will fight you. Most modern B2B and consumer products have moved past that line by 2026, but check before committing.

If two of three answers are yes, the migration pays back inside two quarters. If only one is yes, stay on Tailwind and revisit in a year.

What we kept from Tailwind

Three ideas survived the migration and live in our system today. The constraint mindset (a small fixed set of spacing and color values rather than arbitrary numbers). The dark-mode-as-data-attribute pattern, simpler than a class toggle and easier to test. The convention of utility classes for layout primitives, kept short and closed. The names changed; the discipline stayed.

Removing Tailwind is not a victory lap over a framework that did its job for a decade. It is a recognition that for a studio that sells design system work, owning the CSS down to the cascade is the product, not an implementation detail.

Sources

Photo by Tim Schmidbauer on Unsplash

Frequently asked questions

How long does it take to remove Tailwind from a production codebase?
Plan one engineering quarter for a codebase of 30,000 to 50,000 utility-class instances, working alongside feature delivery. The migration is mechanical for layout and typography, slower for interactive states and dark mode, and slowest for places where Tailwind variants encoded business logic (responsive copy, conditional spacing). Run it incrementally per route or per feature flag, never as a single big bang. The cost is real; it pays back when the team stops paying interest on every new component.
Do CSS cascade layers work with Server Components in Next.js?
Yes. Cascade layers are pure CSS, processed by the browser, with zero coupling to the rendering model. We use them in production with Next.js 16 App Router and React 19 Server Components. The only nuance is that you import the design system stylesheet once in the root layout, declared at the top so the layer order is established before any route-level CSS loads.
Can you mix Tailwind and a CSS-only design system during migration?
Technically yes, practically not for long. Put Tailwind in its own cascade layer below your design system layer, so component classes always win. This buys you a migration window where new code uses the design system and existing code keeps working. Set a deadline (one quarter is realistic) and remove Tailwind entirely at the end. Long-term coexistence reintroduces the hybrid drift problem we described above.

Studio

Start a project.

One partner for companies, public sector, startups and SaaS. Faster delivery, modern tech, lower costs. One team, one invoice.