A namespace for build-time Tailwind CSS expansion plugins. Transform CSS component aliases into utility classes in your JSX.
| Package | Details |
|---|---|
| @tailwind-expand/vite | |
| @tailwind-expand/postcss | |
| @tailwind-expand/babel | |
| @tailwind-expand/swc | |
| @tailwind-expand/core |
Tailwind's @apply creates CSS classes with utility rules baked in:
/* Using @apply */
.Button {
@apply text-sm inline-flex items-center;
}This approach has limitations:
- DevTools opacity: Browser shows
.Buttoninstead of actual utilities - Bundle bloat: CSS contains duplicate rules across components
- No variant composition: Can't use
lg:Buttonorhover:Button
tailwind-expand lets you define component aliases in CSS and expands them to utility classes in your JSX at build time:
/* globals.css */
@expand Button {
@apply text-sm inline-flex items-center;
&Md {
@apply h-10 px-4;
}
}// Your component
<button className="Button ButtonMd lg:Button" />
// After build (debug: true)
<button data-expand="Button ButtonMd lg:Button" className="text-sm inline-flex items-center h-10 px-4 lg:text-sm lg:inline-flex lg:items-center" />With
debug: false(default),data-expandis omitted.
- Atomic CSS in production: Inlined utilities = smaller bundles, deduplicated classes
- Semantic classes in development:
.Button,.HomeHeroTitlein DevTools for easy debugging - Zero runtime: All expansion happens at build time
- Variant support: Use
lg:,hover:,!prefixes with any alias - Familiar syntax: Define aliases using
@applyyou already know
pnpm add -D @tailwind-expand/vite @tailwind-expand/babel// vite.config.ts
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import tailwindExpandVite from '@tailwind-expand/vite'
import tailwindExpandBabel from '@tailwind-expand/babel'
export default defineConfig({
plugins: [
tailwindExpandVite(), // Must come before tailwindcss
tailwindcss(),
react({
babel: {
plugins: [tailwindExpandBabel({
cssPath: './src/globals.css',
debug: process.env.NODE_ENV !== 'production',
})],
},
}),
],
})pnpm add -D @tailwind-expand/postcss @tailwind-expand/swc// next.config.ts
import tailwindExpandSWC from '@tailwind-expand/swc'
const nextConfig = {
experimental: {
swcPlugins: [tailwindExpandSWC({ cssPath: './app/globals.css' })],
},
}
export default nextConfig// postcss.config.mjs
export default {
plugins: {
'@tailwind-expand/postcss': {},
'@tailwindcss/postcss': {},
},
}Note: CSS alias changes require a server restart. See @tailwind-expand/swc for a workaround.
For frameworks using PostCSS (see Tailwind PostCSS installation):
pnpm add -D @tailwind-expand/postcss @tailwind-expand/babel// postcss.config.js
module.exports = {
plugins: {
'@tailwind-expand/postcss': {},
tailwindcss: {},
autoprefixer: {},
},
}// babel.config.js
import tailwindExpandBabel from '@tailwind-expand/babel'
module.exports = {
plugins: [tailwindExpandBabel({ cssPath: './src/globals.css' })],
}Create component aliases using the @expand at-rule with nested modifiers. Aliases can reference other aliases for composition:
/* globals.css */
@import "tailwindcss";
@theme inline {
--color-primary: #3b82f6;
--color-danger: #ef4444;
}
@expand Typography {
&Caption {
@apply text-xs font-bold uppercase;
}
&Heading {
@apply text-2xl font-bold;
}
}
@expand Button {
/* Compose with other aliases */
@apply TypographyCaption inline-flex items-center;
&Sm {
@apply h-8 px-3;
}
&Md {
@apply h-10 px-4;
}
&Primary {
@apply bg-primary text-white hover:bg-primary/90;
}
}
/* Container pattern: define entire page structures */
@expand Home {
@apply min-h-screen bg-gray-50 p-8;
&Hero {
@apply mx-auto max-w-2xl space-y-8;
&Title {
@apply TypographyHeading text-gray-900;
}
}
&Section {
@apply space-y-4;
&Actions {
@apply flex items-center gap-2;
&Submit {
@apply Button ButtonMd ButtonPrimary flex-1;
}
}
}
}// Input
<div className="Home">
<div className="HomeHero">
<h1 className="HomeHeroTitle">Welcome</h1>
</div>
<section className="HomeSection">
<div className="HomeSectionActions">
<button className="HomeSectionActionsSubmit">Submit</button>
<button className="Button ButtonMd ButtonPrimary">Click me</button>
</div>
</section>
</div>
// Output (after build)
<div className="min-h-screen bg-gray-50 p-8">
<div className="mx-auto max-w-2xl space-y-8">
<h1 className="text-2xl font-bold text-gray-900">Welcome</h1>
</div>
<section className="space-y-4">
<div className="flex items-center gap-2">
<button className="text-xs font-bold uppercase inline-flex items-center h-10 px-4 bg-primary text-white hover:bg-primary/90 flex-1">Submit</button>
<button className="text-xs font-bold uppercase inline-flex items-center h-10 px-4 bg-primary text-white hover:bg-primary/90">Click me</button>
</div>
</section>
</div>Use Tailwind states with any alias. Each utility in the alias gets the state prefix:
// Input
<button className="Button ButtonSm lg:ButtonMd hover:ButtonPrimary !ButtonMd" />
// Output (debug: true)
<button data-expand="Button ButtonSm lg:ButtonMd hover:ButtonPrimary !ButtonMd" className="text-xs font-bold uppercase inline-flex items-center h-8 px-3 lg:h-10 lg:px-4 hover:bg-primary hover:text-white hover:bg-primary/90 !h-10 !px-4" />When composing aliases, you may end up with conflicting utilities (e.g., text-xs from one alias and text-sm added later). Use mergerFn to resolve these conflicts with tailwind-merge:
pnpm add tailwind-mergeVite + React:
// vite.config.ts
import { twMerge } from 'tailwind-merge'
export default defineConfig({
plugins: [
tailwindExpandVite({ mergerFn: twMerge }),
tailwindcss(),
react({
babel: {
plugins: [tailwindExpandBabel({ cssPath: './src/globals.css', mergerFn: twMerge })],
},
}),
],
})Next.js:
// next.config.ts
import { twMerge } from 'tailwind-merge'
swcPlugins: [tailwindExpandSWC({ cssPath: './app/globals.css', mergerFn: twMerge })]// postcss.config.mjs
import { twMerge } from 'tailwind-merge'
export default {
plugins: {
'@tailwind-expand/postcss': { mergerFn: twMerge },
'@tailwindcss/postcss': {},
},
}Example:
@expand Button {
@apply TypographyCaption; /* includes text-xs */
}
@expand Form {
&Submit {
@apply Button text-sm; /* text-xs + text-sm conflict */
}
}// Without mergerFn
<button className="text-xs font-bold uppercase inline-flex items-center text-sm" />
// With mergerFn: twMerge
<button className="font-bold uppercase inline-flex items-center text-sm" />All plugins accept these options:
| Option | Type | Default | Description |
|---|---|---|---|
cssPath |
string |
— | Path to CSS file containing @expand definitions |
mergerFn |
(classes: string) => string |
— | Function to resolve conflicting utilities (e.g., twMerge) |
debug |
boolean |
false |
Add data-expand attribute with expanded alias names |
- CamelCase required: Alias names must be CamelCase (
Button,TypographyHeading) - No inheritance: Modifiers don't inherit parent styles—compose explicitly
- Single entry file: Define all aliases in one CSS file
- Unknown classes preserved: Classes not matching aliases are left untouched
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
MIT © Victor Nunes