mcpField
T
tailwind-to-stylex

tailwind-to-stylex

github.com/shadcn-labsDevelopment2867 installsUpdated 4w ago
Repository

About tailwind-to-stylex

A skill for migrating Tailwind utility classes to StyleX while preserving rendered styles. It guides developers through resolving CSS, handling conditional and structural selectors, applying framework-specific APIs, and verifying changes.

Key features

  1. 01Resolve Tailwind classes to CSS
  2. 02Map conditions to StyleX values
  3. 03Handle selectors requiring markup changes
  4. 04Apply props or attrs by framework
  5. 05Report gaps and verify migration

Use cases

  1. 01Migrate a component from Tailwind to StyleX
  2. 02Convert responsive and state variants
  3. 03Identify utilities that need restructuring
  4. 04Check migrated files with typecheck and lint

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/shadcn-labs/skills --skill tailwind-to-stylex

After installing, type/tailwind-to-stylexin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

SKILL.zh-CN.md
evals/
evals.json
evals/files/button/
package.json
evals/files/button/src/
Button.tsx
Spinner.tsx
evals/files/button/src/lib/
utils.ts
evals/files/navbar/
package.json
tailwind.config.ts
evals/files/navbar/src/
Navbar.tsx
evals/files/price-card/
package.json
vite.config.ts
evals/files/price-card/src/
PriceCard.tsx
references/
mapping.md
stylex-rules.md

Details

From the project README

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

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

Comments

Markdown supported