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.
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= 0p-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: 640pxmd:โ min-width: 768pxlg:โ min-width: 1024pxxl:โ min-width: 1280px2xl:โ 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:โ:hoverfocus:โ:focusfocus-visible:โ:focus-visibleactive:โ:activedisabled:โ:disableddark:โ@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:
- Pick a component with self-contained styles โ a Button, Badge, or Card. No context-dependent utilities.
- Copy the class list into Tailwind โ CSS โ get the base declarations.
- Move to a CSS Module or scoped stylesheet โ
Button.module.cssin Next.js,<style scoped>in Vue, etc. - Extract shared values โ colors and spacing into custom properties.
- Replace
className="..."with the module class. - 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:
- Build the component with Tailwind.
- Use
@applyin a.cssfile to bundle the utilities:
.btn {
@apply px-4 py-2 rounded-lg bg-blue-500 text-white font-semibold;
}
- Ship the
.cssfile 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 utilitiesautomatically. - 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
- Box Shadow Generator โ for recreating Tailwind's shadow scale.
- CSS Gradient Generator โ for gradient extractions.
- Color Converter โ when translating between hex, RGB, HSL for palette tokens.
- PX to REM โ for the spacing scale in different root font sizes.
Tools mentioned in this post
Related reading
CSS box-shadow patterns that look professional (not cheap)
A single 5px shadow makes any UI look like a bootstrapped 2015 landing page. Here are the layered-shadow patterns that modern design systems use โ Material, Tailwind, Apple.
CSS gradient patterns that actually look good (and the two that don't)
Two colors and a 45ยฐ angle is not a gradient โ it's a shortcut to a design that looks like every SaaS site from 2018. Here's how to build ones that don't.
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