712Tools
5 min read

Tailwind to plain CSS: migration, extraction, and component patterns

You inherited a Tailwind codebase and want to extract a component library. Or you're moving away from Tailwind entirely. Here's how the utility classes map to real CSS.

By
Software Engineer ยท M.Sc. Mechanical Engineering ยท Ontario, Canada

Two very different reasons to convert Tailwind to CSS

Understanding which one you have decides the workflow:

1. Extracting a component. You built a button in Tailwind, now you want to publish it as a design-system package that consumers import without Tailwind. The utility classes need to become a scoped CSS file.

2. Migrating away from Tailwind. The team is moving to CSS Modules, vanilla-extract, or plain CSS. Every component needs to lose its utility classes and gain a stylesheet.

Both start the same way: convert the class list to plain CSS declarations. But #1 needs only the used utilities; #2 needs the full extraction across every file.

The mental model: utility โ†’ declaration

Every Tailwind class is a shortcut for one or two CSS declarations. The mapping is deterministic. flex โ†’ display: flex. px-4 โ†’ padding-left: 1rem; padding-right: 1rem. text-blue-500 โ†’ color: #3b82f6.

Learn the mapping once and you can translate by eye. Or use Tailwind โ†’ CSS โ€” paste a class list, get the CSS.

The spacing scale

Tailwind's spacing scale is a multiple of 0.25rem (4px at default root):

  • p-0 = 0
  • p-1 = 0.25rem (4px)
  • p-2 = 0.5rem (8px)
  • p-3 = 0.75rem (12px)
  • p-4 = 1rem (16px)
  • p-6 = 1.5rem (24px)
  • p-8 = 2rem (32px)
  • p-12 = 3rem (48px)
  • p-16 = 4rem (64px)

Applies to padding (p-*), margin (m-*), gap (gap-*), width (w-*), height (h-*), and offsets (top-*, left-*).

Direction suffixes: px-4 = padding left+right, py-4 = top+bottom, pt-4 = top only, etc.

Colors: the shortest ramp is the widest

Tailwind's color palette is 22 named colors ร— 11 shades (50, 100, 200, ... 900, 950). That's 242 base colors, and each has variants (bg-*, text-*, border-*, ring-*).

The extraction path when migrating: dump the palette to CSS custom properties.

:root {
  --color-slate-50: #f8fafc;
  --color-slate-500: #64748b;
  --color-blue-500: #3b82f6;
  /* ...etc */
}

Then bg-blue-500 becomes background-color: var(--color-blue-500). This preserves the design language across the migration.

Responsive prefixes: media queries under the hood

md:flex means "flex at md breakpoint and up." The breakpoints:

  • sm: โ€” min-width: 640px
  • md: โ€” min-width: 768px
  • lg: โ€” min-width: 1024px
  • xl: โ€” min-width: 1280px
  • 2xl: โ€” min-width: 1536px

Extraction:

.card {
  display: block;
}
@media (min-width: 768px) {
  .card {
    display: flex;
  }
}

Nested prefixes stack: md:hover:bg-blue-500 becomes @media (min-width: 768px) { .x:hover { background: blue; } }.

State prefixes: pseudo-classes

  • hover: โ†’ :hover
  • focus: โ†’ :focus
  • focus-visible: โ†’ :focus-visible
  • active: โ†’ :active
  • disabled: โ†’ :disabled
  • dark: โ†’ @media (prefers-color-scheme: dark) or [data-theme="dark"]

Complex utilities that don't map 1:1

Box shadow. shadow-lg isn't one shadow โ€” it's a multi-layer shadow with specific values. Extract from Tailwind's source or use Box Shadow Generator to recreate.

Gradient. bg-gradient-to-r from-blue-500 to-purple-500 maps to background-image: linear-gradient(to right, #3b82f6, #a855f7). Use CSS Gradient Generator to preview and get the CSS.

Container queries. Tailwind 4 added @container support. Maps to CSS @container (min-width: X).

Arbitrary values. p-[13px] โ€” Tailwind's escape hatch for one-off values. Trivially maps to padding: 13px.

The migration workflow

For a real codebase, one component at a time:

  1. Pick a component with self-contained styles โ€” a Button, Badge, or Card. No context-dependent utilities.
  2. Copy the class list into Tailwind โ†’ CSS โ€” get the base declarations.
  3. Move to a CSS Module or scoped stylesheet โ€” Button.module.css in Next.js, <style scoped> in Vue, etc.
  4. Extract shared values โ€” colors and spacing into custom properties.
  5. Replace className="..." with the module class.
  6. Test in dark mode + all breakpoints โ€” where responsive/dark prefixes hide.

Budget 2-4 hours per moderately complex component the first few times; 30 minutes each after you've built muscle memory.

The extraction pattern (opposite direction)

Sometimes you want the extracted CSS without leaving Tailwind โ€” you're publishing a component that consumers use without Tailwind installed. Then:

  1. Build the component with Tailwind.
  2. Use @apply in a .css file to bundle the utilities:
.btn {
  @apply px-4 py-2 rounded-lg bg-blue-500 text-white font-semibold;
}
  1. Ship the .css file with the package. Consumers get plain CSS; the utilities were compiled away at build time.

This is how shadcn/ui, Radix Themes, and most modern component libraries publish CSS today.

Tailwind 4 changes to know

Tailwind 4 (released this year) changed several things:

  • CSS-first configuration โ€” themes defined in CSS custom properties, not tailwind.config.js.
  • Native cascade layers โ€” utilities go into @layer utilities automatically.
  • Container queries built in โ€” no plugin needed.
  • New color palette โ€” OKLCH-based, wider gamut on modern displays.

Extraction workflows still work โ€” the base utility set is largely the same. But if you're upgrading a codebase, check the Tailwind 4 migration guide for the config-file changes.

Related workflows

Tools mentioned in this post

Written by Shan

Shan builds 712 Tools. He holds a Master's degree in Mechanical Engineering and now works as a Software Engineer, shipping browser-based developer utilities out of Ontario, Canada. Learn more ยท 712studiogames@gmail.com