---
name: tailwind-to-stylex
description: >-
Migrate TailwindCSS to StyleX. Use when converting Tailwind `className` utility strings
into StyleX `stylex.create` + `stylex.props`/`stylex.attrs` — a single component or a whole
codebase, in React/JSX/TSX, react-strict-dom, or any JS framework StyleX supports (Solid,
Svelte, Vue, Qwik, Preact, …) — when the user mentions tailwind-to-stylex or tw-to-stylex,
or when they have something "in Tailwind" and want it "in StyleX", even if they don't name
this skill.
compatibility: Any JavaScript-framework project (React, Preact, Solid, Svelte, Vue, Qwik, react-strict-dom, …) with StyleX (@stylexjs/stylex) and its compiler plugin configured. If StyleX isn't set up yet, see references/stylex-rules.md ("Project setup").
---
# 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(...)`.
This is a **behavior-preserving refactor**, not a redesign. The rendered pixels should
not change. Don't drop styles you can't cleanly convert — flag them (see Step 6) so the
user can decide, rather than silently changing the design.
## The core mechanic: resolve, then reshape
The reliable way to convert a class string is the same one the `tw-to-stylex` codemod
uses under the hood: **resolve each class to the CSS it actually produces, then reshape
that CSS into a StyleX object.** Don't pattern-match class names to guesses — think in
the computed CSS, because that's what has to stay identical.
```
"px-4 py-2 text-sm font-medium hover:bg-blue-600"
│
▼ resolve to CSS (mind the project's Tailwind version + config)
padding-inline: 1rem; padding-block: .5rem;
font-size: .875rem; line-height: 1.25rem; font-weight: 500;
:hover { background-color: #2563eb }
│
▼ reshape to StyleX (camelCase, conditions as values)
{
paddingInline: '1rem',
paddingBlock: '0.5rem',
fontSize: '0.875rem',
lineHeight: '1.25rem',
fontWeight: 500,
backgroundColor: { default: null, ':hover': '#2563eb' },
}
```
If a class uses arbitrary values (`w-[37px]`), the project's custom theme, or a plugin,
resolving-to-CSS still works — you read the value straight out of the class or the
config. When unsure what a class computes to (custom config, unfamiliar plugin), check
the project's `tailwind.config` / CSS `@theme`, or resolve it rather than guessing.
## Two things that trip up conversions
**1. StyleX conditions are values, not rules.** Tailwind spreads state and responsive
variants across separate rules (`hover:`, `md:`, `dark:`). StyleX instead nests the
*condition inside the property value*, and the `default` key is **required** whenever a
property has any condition (use `null` when there's no base value). This is the single
biggest shape difference — get it wrong and styles silently don't apply.
```tsx
// md:flex hover:opacity-80 -> conditions live on each property
display: { default: 'block', '@media (min-width: 768px)': 'flex' },
opacity: { default: 1, ':hover': 0.8 },
```
**2. Some Tailwind utilities compile to selectors StyleX can't express on one element.**
`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 don't map 1:1. Don't fake them — restructure (e.g. `space-x` →
`gap` on the flex parent; `group-hover` → lift state into React or a CSS variable) and
tell the user. See `references/mapping.md` for each case.
## Workflow
Work one component (file) at a time so each stays reviewable.
### Step 1: Confirm StyleX is set up
StyleX needs its compiler plugin wired into the bundler (Babel/Vite/Next/Webpack/rspack)
and `@stylexjs/stylex` installed — otherwise `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 `references/stylex-rules.md` → "Project setup" and either configure it or
tell the user what's missing before converting. Note whether the target is React DOM or
`react-strict-dom` (React Native) — it changes how styles are applied (see the
`react-strict-dom` note in `references/stylex-rules.md` → "Project setup").
### Step 2: Find the Tailwind usage
Locate every `className`/`class` in the file, including dynamic ones: `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/variant (`base`, `label`, `iconActive`…) —
descriptive names beat `$1`, `$2`. For each, resolve its classes to CSS and reshape
(the core mechanic). Apply the StyleX authoring rules that differ from raw CSS:
- **camelCase** every property (`border-radius` → `borderRadius`).
- **Longhand + single-value shorthands only.** StyleX warns on multi-value shorthands
because they cause merge conflicts. Split them: `border: '1px solid red'` →
`borderWidth: 1, borderStyle: 'solid', borderColor: 'red'`; a two-value `padding` →
`paddingBlock` / `paddingInline`. A single-value shorthand (`padding: 16`) is fine.
- **Numbers are px.** `width: 24` means 24px. Keep other units as strings (`'1.5rem'`,
`'50%'`, `'100vh'`).
- **Conditions as values with a required `default`** (see gotcha 1). Mobile-first: the
unprefixed class is `default`; `sm/md/lg/xl/2xl` become `@media (min-width: …)` keys.
- Full modifier → condition table (states, dark mode, `first`/`last`, `before`/`after`,
arbitrary values, gradients, animations) lives in `references/mapping.md` — read it
whenever you hit anything past plain utilities.
### Step 4: Apply the styles
Steps 1–3 are identical for every framework — StyleX is framework-agnostic, and only
*applying* the result differs. First pick the applicator for the target:
- **React and JSX-spread frameworks** (React DOM, Preact, `react-strict-dom`): spread
`stylex.props(...)`, which returns `{ className, style }`.
- **Solid, Svelte, Vue, Qwik, and other non-React frameworks**: use `stylex.attrs(...)`
instead — it returns a plain `class` string and a string `style` value to bind onto the
element. Which styles you pass and in what order is exactly the same as `props`.
Then, whichever applicator you use:
- **Host elements** (`div`, `span`, `button` — lowercase): apply the result directly,
replacing the old `className` / `class`.
- **Components** (capitalized): they don't accept `stylex.props`/`attrs` output directly.
Pass the style tokens down (e.g. a `style` prop) 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, last wins —
the same order semantics as `cn(...)`. So `cn('base', isActive && 'active')` becomes
`stylex.props(styles.base, isActive && styles.active)` (or `stylex.attrs(...)`), and a
passed-in override stays last so callers can still win.
### Step 5: Repeated design tokens (optional)
If the same theme value (a brand color, a spacing step) recurs across the file or the
project uses semantic tokens, consider `stylex.defineVars` in a `.stylex.ts` file instead
of hardcoding — this is also the clean answer to class-based `dark:` theming
(`createTheme`). See `references/stylex-rules.md` → "Theming with variables". Don't force
this for one-off values.
### Step 6: Flag what didn't convert
Call out, in your summary, anything you couldn't map 1:1: the selector-based utilities
from gotcha 2, any class you couldn't resolve, and any place you restructured markup.
Losing a hover/dark/responsive style silently 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, and remove Tailwind directives/config only once the whole codebase is migrated
(not per-file). If deps aren't installed in your environment, say so and mark the check
as structural rather than claiming it runs.
## Worked example
```tsx
// Before
import { cn } from '@/lib/utils'
export function Badge({ active, className }: Props) {
return (
Status
)
}
```
```tsx
// 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 (
Status
)
}
```
Note what changed: `text-xs` carried *two* declarations (`font-size` **and**
`line-height`) — both were kept. The caller override moved from `className` to `style`
(StyleX passes styles, not class strings). The `cn` ternary became a `stylex.props`
ternary with identical last-wins order. This example is React; in Solid/Svelte/Vue/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.md` — Tailwind modifier/variant → StyleX condition table, and how
to handle the tricky utilities (`space-x`, `divide`, `group`/`peer`, dark mode,
arbitrary values, gradients, `before`/`after`, animations, `sr-only`). Read it for
anything beyond plain single-value utilities.
- `references/stylex-rules.md` — StyleX authoring rules, common mistakes, project setup,
framework support (`stylex.props` vs `stylex.attrs`), and theming with
`defineVars`/`createTheme`. Read it when setting up StyleX, migrating a non-React
framework, or when the ESLint plugin flags something.