Twirlwind
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.
Install
npm install twirlwind
Usage
import { twirl } from 'twirlwind'
twirl({ display: 'flex', padding: '16px 8px', color: '#ef4444' })
Inputs
twirl() accepts any of these and returns a class string:
twirl({ backgroundColor: 'white', fontSize: '16px' })
twirl('display: flex; padding: 16px')
twirl(element.style)
twirl(getComputedStyle(element))
Detailed result
Use twirl.convert() when you need metadata:
const result = twirl.convert({ display: 'flex', width: '37px' })
result.className
result.classes
result.exact
result.arbitrary
result.unmatched
Features
Color matching
Matches across formats — OKLCH, hex, rgb(), keywords, opacity modifiers.
twirl({ color: '#ef4444' })
twirl({ color: 'rgb(59 130 246)' })
twirl({ color: 'oklch(62.3% 0.214 259.815 / 50%)' })
twirl({ color: 'currentColor' })
Shorthand expansion
CSS shorthands decompose into Tailwind longhands.
twirl({ border: '2px solid #ef4444' })
twirl({ font: 'bold 16px/1.5 sans-serif' })
twirl({ background: 'white center no-repeat' })
Multi-value parsing
Compound transform and filter declarations decompose into individual classes.
twirl({ transform: 'translateX(8px) rotate(45deg)' })
twirl({ filter: 'blur(8px) brightness(0.75)' })
twirl({ scrollSnapType: 'x mandatory' })
Compression
Expanded longhands compress to shorthand utilities.
twirl({ margin: '8px' })
twirl({ inset: '0' })
twirl({ padding: '8px 16px' })
twirl({ borderRadius: '8px' })
twirl({ gap: '12px 12px' })
Variants
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' }
})
Arbitrary fallback
Every CSS property produces valid output.
twirl({ scrollTimelineName: '--main' })
twirl({ width: '37px' })
Options
twirl(input, {
allowArbitraryValues: true,
allowArbitraryProperties: true,
compression: 'safe',
sort: 'grouped',
colorMatch: 'exact',
numericMultipliers: 'integer',
theme: {
colors: { brand: '#ff6600' },
spacing: { '18': '4.5rem' }
}
})
How it works
- 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
License
MIT