Tailwind v4-first CSS-to-utility-class serializer for JavaScript/TypeScript.
Converts style objects, CSS declaration strings, and CSSStyleDeclaration values into clean Tailwind utility classes. Prefers canonical utilities, falls back to arbitrary values, then arbitrary properties — every CSS property produces valid output.
npm install twirlwindimport { twirl } from 'twirlwind'
twirl({ display: 'flex', padding: '16px 8px', color: '#ef4444' })
// → "flex py-4 px-2 text-red-500"twirl() accepts any of these and returns a class string:
// Style object
twirl({ backgroundColor: 'white', fontSize: '16px' })
// CSS string
twirl('display: flex; padding: 16px')
// CSSStyleDeclaration (browser)
twirl(element.style)
// Computed styles
twirl(getComputedStyle(element))Use twirl.convert() when you need metadata:
const result = twirl.convert({ display: 'flex', width: '37px' })
result.className // "flex w-[37px]"
result.classes // ["flex", "w-[37px]"]
result.exact // exact utility matches
result.arbitrary // arbitrary value/property fallbacks
result.unmatched // declarations that couldn't be convertedMatches across formats — OKLCH, hex, rgb(), keywords, opacity modifiers.
twirl({ color: '#ef4444' }) // "text-red-500"
twirl({ color: 'rgb(59 130 246)' }) // "text-blue-500"
twirl({ color: 'oklch(62.3% 0.214 259.815 / 50%)' }) // "text-blue-500/50"
twirl({ color: 'currentColor' }) // "text-current"CSS shorthands decompose into Tailwind longhands.
twirl({ border: '2px solid #ef4444' }) // "border-2 border-red-500 border-solid"
twirl({ font: 'bold 16px/1.5 sans-serif' }) // "font-bold text-base leading-normal font-sans"
twirl({ background: 'white center no-repeat' }) // "bg-white bg-center bg-no-repeat"Compound transform and filter declarations decompose into individual classes.
twirl({ transform: 'translateX(8px) rotate(45deg)' }) // "rotate-45 translate-x-2"
twirl({ filter: 'blur(8px) brightness(0.75)' }) // "blur brightness-75"
twirl({ scrollSnapType: 'x mandatory' }) // "snap-x snap-mandatory"Expanded longhands compress to shorthand utilities.
twirl({ margin: '8px' }) // "m-2"
twirl({ inset: '0' }) // "inset-0"
twirl({ padding: '8px 16px' }) // "py-2 px-4"
twirl({ borderRadius: '8px' }) // "rounded-lg"
twirl({ gap: '12px 12px' }) // "gap-3"Nested objects map to Tailwind variants.
twirl({
color: 'white',
':hover': { color: '#3b82f6' },
'@media (min-width: 768px)': { display: 'grid' },
'@media (prefers-color-scheme: dark)': { backgroundColor: 'black' },
'@container (min-width: 512px)': { display: 'flex' }
})
// → "text-white hover:text-blue-500 md:grid dark:bg-black @lg:flex"Every CSS property produces valid output.
twirl({ scrollTimelineName: '--main' }) // "[scroll-timeline-name:--main]"
twirl({ width: '37px' }) // "w-[37px]"twirl(input, {
allowArbitraryValues: true, // default: true
allowArbitraryProperties: true, // default: true
compression: 'safe', // "none" | "safe" | "aggressive"
sort: 'grouped', // "input" | "tailwind" | "grouped"
colorMatch: 'exact', // "exact" | "nearest" | "none"
numericMultipliers: 'integer', // "all" | "integer" | "never"
theme: {
colors: { brand: '#ff6600' },
spacing: { '18': '4.5rem' }
}
})- Normalize — camelCase → kebab-case, numeric → px, vendor prefixes,
!important - Expand —
margin,border,font,background,transition,overflow,gap, etc. - Convert — exact utility → value alias → spacing token → color match → arbitrary value → arbitrary property
- Compress — merge longhands back to shorthand utilities
- Sort — deterministic output ordering
MIT