# Tailwind To Stylex

> Migrate TailwindCSS to StyleX. Use when converting Tailwind `className` utility strings into `stylex.create` with `stylex.props` or `stylex.attrs`. Use for one component or a whole codebase in React, Preact, Solid, Svelte, Vue, Qwik, react-strict-dom, and other JavaScript frameworks supported by StyleX. Trigger on tailwind-to-stylex, tw-to-stylex, or any request to move code from Tailwind to StyleX.

- Skill: `shadcn-labs/tailwind-to-stylex` (Agent Skill, multi-file: 14 files)
- Install (CLI): `npx skillmds@latest add shadcn-labs/tailwind-to-stylex`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shadcn-labs/tailwind-to-stylex/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: shadcn-labs (https://skillmd.com/u/shadcn-labs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/shadcn-labs/tailwind-to-stylex

---


# 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.

```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 },
```

### 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-radius` becomes `borderRadius`.
- Use longhands or single-value shorthands. StyleX warns on multi-value shorthands
  because they cause merge conflicts. Split `border: '1px solid red'` into
  `borderWidth: 1, borderStyle: 'solid', borderColor: 'red'`. Split a two-value `padding` into
  `paddingBlock` / `paddingInline`. A single-value shorthand such as `padding: 16` is valid.
- Numbers represent px for length properties. `width: 24` means 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 is `default`; breakpoints become `@media` keys with `min-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 plain `class` string and a string `style` value to bind onto the
  element. Pass styles in the same order as `stylex.props`.

Then, whichever applicator you use:

- Apply the result directly to lowercase host elements such as `div`, `span`, and `button`.
  Replace the old `className` or `class`.
- Components do not accept `stylex.props` or `stylex.attrs` output automatically.
  Pass the style tokens through 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 and the last
  value wins, matching `cn(...)`. `cn('base', isActive && 'active')` becomes
  `stylex.props(styles.base, isActive && styles.active)` or the same call with `stylex.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

```tsx
// 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>
  )
}
```

```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 (
    <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.md` contains the Tailwind modifier and variant table, plus ways
  to handle `space-x`, `divide`, `group`, `peer`, dark mode, arbitrary values, gradients,
  pseudo-elements, animations, and `sr-only`. Read it for
  anything beyond plain single-value utilities.
- `references/stylex-rules.md` covers StyleX authoring rules, common mistakes, project setup,
  framework support for `stylex.props` and `stylex.attrs`, and theming with
  `defineVars` and `createTheme`. Read it when setting up StyleX, migrating a non-React
  framework, or when the ESLint plugin flags something.

