Migrate Tailwind to StyleX
Convert Tailwind utility classes into StyleX styles. Replace className="..." strings
with entries in a stylex.create({...}) call, and apply them with stylex.props(...).
Preserve the rendered output. Do not redesign the component during migration. If a style cannot be converted exactly, flag it in Step 6 instead of silently changing the design.
The core mechanic: resolve, then reshape
Convert class strings the same way as the tw-to-stylex codemod. Resolve each class to
the CSS it produces, then reshape that CSS into a StyleX object. Do not guess from class
names. The computed CSS is the behavior that must remain identical.
"px-4 py-2 text-sm font-medium hover:bg-blue-600"
│
▼ resolve to CSS using the project's Tailwind version and config
padding-inline: 1rem; padding-block: .5rem;
font-size: .875rem; line-height: 1.25rem; font-weight: 500;
:hover { background-color: #2563eb }
│
▼ reshape to StyleX with camelCase properties and conditional values
{
paddingInline: '1rem',
paddingBlock: '0.5rem',
fontSize: '0.875rem',
lineHeight: '1.25rem',
fontWeight: 500,
backgroundColor: { default: null, ':hover': '#2563eb' },
}
If a class uses an arbitrary value such as w-[37px], the project's custom theme, or a plugin,
resolving to CSS still works because the value comes from the class or project config.
When a custom config or unfamiliar plugin makes the output unclear, check
the project's tailwind.config / CSS @theme, or resolve it rather than guessing.
Two things that trip up conversions
Conditions live in property values
Tailwind spreads state and responsive variants such as hover:, md:, and dark:
across separate rules. StyleX instead nests the
condition inside the property value. The default key is required whenever a property
has any condition. Use null when there is no base value. Without default, StyleX may
ignore the condition.
// md:flex hover:opacity-80 -> conditions live on each property
display: { default: 'block', '@media (min-width: 768px)': 'flex' },
opacity: { default: 1, ':hover': 0.8 },
Some selectors need a structural change
space-x-*, divide-*, group/group-hover:*, peer/peer-*:*, and typography's
prose all rely on descendant, sibling, or ancestor-state selectors. StyleX styles a
single element, so these do not map directly. Restructure them and tell the user. For
example, replace space-x with gap on a flex parent, or lift group-hover state into
React or a CSS variable. See references/mapping.md for each case.
Workflow
Work through one component file at a time so each change stays reviewable.
Step 1: confirm StyleX is set up
StyleX needs @stylexjs/stylex and a compiler plugin for Babel, Vite, Next.js, Webpack,
or rspack. Without the plugin, stylex.props returns nothing at runtime
and the migration looks broken. Check package.json and the bundler config. If it isn't
set up, read the project setup section in references/stylex-rules.md. Configure it when
the user requested setup, or explain what is missing before converting. Also identify
React DOM and react-strict-dom, since they apply styles differently.
Step 2: find the Tailwind usage
Locate every className and class in the file, including dynamic calls to cn(...),
clsx(...), twMerge(...), template literals, and ternaries. You need the complete
set of classes that can apply to each element, and under which conditions, to convert
faithfully.
Step 3: build the stylex.create object
Create one named entry per distinct element or variant, such as base, label, or
iconActive. Use descriptive names instead of $1 and $2. Resolve the classes for
each entry to CSS, then apply these StyleX rules:
- Use camelCase for every property. For example,
border-radiusbecomesborderRadius. - Use longhands or single-value shorthands. StyleX warns on multi-value shorthands
because they cause merge conflicts. Split
border: '1px solid red'intoborderWidth: 1, borderStyle: 'solid', borderColor: 'red'. Split a two-valuepaddingintopaddingBlock/paddingInline. A single-value shorthand such aspadding: 16is valid. - Numbers represent px for length properties.
width: 24means 24px. Keep other units as strings, such as'1.5rem','50%', and'100vh'. - Conditions belong in property values and require
default. In mobile-first order, the unprefixed class isdefault; breakpoints become@mediakeys withmin-width. - The modifier-to-condition table for states, dark mode, structural selectors, arbitrary
values, gradients, and animations lives in
references/mapping.md. Read it whenever you hit anything past plain utilities.
Step 4: apply the styles
The first three steps are the same for every framework. Only the applicator changes:
- In React DOM, Preact, react-strict-dom, and other JSX-spread frameworks, spread
stylex.props(...), which returns{ className, style }. - In Solid, Svelte, Vue, Qwik, and other non-React frameworks, use
stylex.attrs(...). It returns a plainclassstring and a stringstylevalue to bind onto the element. Pass styles in the same order asstylex.props.
Then, whichever applicator you use:
- Apply the result directly to lowercase host elements such as
div,span, andbutton. Replace the oldclassNameorclass. - Components do not accept
stylex.propsorstylex.attrsoutput automatically. Pass the style tokens through astyleprop and let the component apply them on its own host element. Don't just dump the applicator output onto a component and assume it works. - Preserve conditional composition. The applicator merges left to right and the last
value wins, matching
cn(...).cn('base', isActive && 'active')becomesstylex.props(styles.base, isActive && styles.active)or the same call withstylex.attrs. A passed-in override stays last so callers can still win.
Step 5: extract repeated design tokens when useful
If a brand color, spacing step, or other theme value recurs across the file, or the
project uses semantic tokens, consider stylex.defineVars in a .stylex.ts file instead
of hardcoding it. Variables with createTheme also replace class-based dark: theming.
See the theming section in references/stylex-rules.md. Don't force
this for one-off values.
Step 6: report what did not convert
In the summary, list selector-based utilities, unresolved classes, and markup changes that did not map directly. Losing a hover, dark mode, or responsive style without warning is the failure that matters most here.
Step 7: verify
Typecheck and run the linter on the changed files. If the project has the StyleX ESLint
plugin, it catches invalid shorthands, missing default, and unknown properties. Fix
what it reports. Confirm no stray className / Tailwind imports remain on migrated
elements. Remove Tailwind directives and config only after the whole codebase is migrated,
not after one file. If dependencies aren't installed, say so and mark the check as
structural rather than claiming it runs.
Worked example
// Before
import { cn } from '@/lib/utils'
export function Badge({ active, className }: Props) {
return (
<span
className={cn(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600',
className,
)}
>
Status
</span>
)
}
// After
import * as stylex from '@stylexjs/stylex'
const styles = stylex.create({
base: {
display: 'inline-flex',
alignItems: 'center',
borderRadius: '9999px',
paddingInline: '0.625rem',
paddingBlock: '0.125rem',
fontSize: '0.75rem',
lineHeight: '1rem',
fontWeight: 600,
},
active: { backgroundColor: '#dcfce7', color: '#166534' },
inactive: { backgroundColor: '#f3f4f6', color: '#4b5563' },
})
export function Badge({ active, style }: Props) {
return (
<span
{...stylex.props(styles.base, active ? styles.active : styles.inactive, style)}
>
Status
</span>
)
}
text-xs produces both font-size and line-height, so the conversion keeps both.
The caller override moved from className to style because StyleX passes styles instead
of class strings. The cn ternary became a stylex.props ternary with the same last-wins
order. This example uses React. In Solid, Svelte, Vue, and Qwik, the styles object is
byte-for-byte the same. Only the stylex.props call becomes
stylex.attrs and you bind the returned class/style the way that framework binds attributes.
Reference files
references/mapping.mdcontains the Tailwind modifier and variant table, plus ways to handlespace-x,divide,group,peer, dark mode, arbitrary values, gradients, pseudo-elements, animations, andsr-only. Read it for anything beyond plain single-value utilities.references/stylex-rules.mdcovers StyleX authoring rules, common mistakes, project setup, framework support forstylex.propsandstylex.attrs, and theming withdefineVarsandcreateTheme. Read it when setting up StyleX, migrating a non-React framework, or when the ESLint plugin flags something.