Transform styled-components to StyleX.
Try it in the online playground — experiment with the transform in your browser.
Before running the codemod, convert your theme object and shared style helpers into StyleX equivalents:
// tokens.stylex.ts — theme variables
import * as stylex from "@stylexjs/stylex";
// Before: { colors: { primary: "#0066cc" }, spacing: { sm: "8px" } }
export const colors = stylex.defineVars({ primary: "#0066cc" });
export const spacing = stylex.defineVars({ sm: "8px" });// helpers.stylex.ts — shared mixins
import * as stylex from "@stylexjs/stylex";
// Before: export const truncate = () => `white-space: nowrap; overflow: hidden; ...`
export const truncate = stylex.create({
base: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
});The adapter maps your project's props.theme.* access, CSS variables, and helper calls to the StyleX equivalents from step 1. See Basic usage for the full API.
When a component wraps another component that internally uses styled-components (e.g. styled(GroupHeader) where GroupHeader renders a StyledHeader), CSS cascade conflicts can arise after migration. Convert leaf files — the ones that don't wrap other styled-components — first, then work your way up. The codemod will bail with a warning if it detects this pattern.
Build and test your project. Review warnings — they tell you which files were skipped and why. Fix adapter gaps, re-run on remaining files, and repeat until done. Report issues with input/output examples if the codemod produces incorrect results.
Copy this into an agent working in the repository you want to migrate:
You are helping migrate this repository from styled-components to StyleX with
`styled-components-to-stylex-codemod`.
Work in small, reviewable steps:
1. Inspect the project before changing files.
- Identify the package manager and install command.
- Find styled-components usage, theme access patterns, CSS variables, helper
functions used inside template interpolations, shared mixins, and existing
StyleX setup.
- Identify a leaf component/file glob to migrate first. Prefer components
that do not wrap other styled-components.
2. Install the codemod and any missing StyleX runtime/build dependencies the
project needs.
- Use the repository's package manager.
- Keep dependency changes separate and explain why each package is needed.
3. Create a local codemod runner, for example
`scripts/run-styled-components-to-stylex.mts`, using this shape:
```ts
import { defineAdapter, runTransform } from "styled-components-to-stylex-codemod";
const adapter = defineAdapter({
resolveValue(ctx) {
// Map props.theme.*, CSS variables, and imported constants to StyleX
// variables or other static StyleX-compatible expressions.
return undefined;
},
resolveCall(ctx) {
// Map helper calls used in styled template interpolations to StyleX
// mixins/values, or return { preserveRuntimeCall: true } when safe.
return undefined;
},
resolveSelector(ctx) {
// Map imported selector helpers such as media query or pseudo aliases.
return undefined;
},
externalInterface(ctx) {
// Return { styles: true, as: true, ref: true } for exported components
// that must keep accepting className/style, polymorphic `as`, or refs.
return { styles: false, as: false, ref: false };
},
styleMerger: null,
useSxProp: false,
wrappedComponentInterface(ctx) {
return undefined;
},
themeHook: {
functionName: "useTheme",
importSource: { kind: "specifier", value: "styled-components" },
},
});
await runTransform({
files: "src/**/*.tsx",
consumerPaths: "src/**/*.tsx",
adapter,
dryRun: true,
parser: "tsx",
formatterCommands: ["pnpm prettier --write"],
});
```
4. Configure the adapter for this codebase.
- `resolveValue`: map theme paths (`props.theme.color.primary`), CSS
variables (`var(--token)`), and imported values to StyleX variables.
- `resolveCall`: map project style helpers to StyleX mixins or values.
- `resolveSelector`: map imported media-query or pseudo selector helpers.
- `externalInterface`: preserve `className`/`style`, `as`, and `ref` support
for public components. Use `externalInterface: "auto"` only when
`consumerPaths` covers the consumers and the prepass succeeds.
- `styleMerger`: provide the project's helper for combining StyleX styles
with external `className`/`style` when public components need it.
- `useSxProp` and `wrappedComponentInterface`: enable only if the project
uses StyleX `sx` props and the Babel plugin is configured for them.
- `themeHook`: point wrapper theme conditionals at the project's runtime
theme hook if it is not `useTheme` from styled-components.
- `resolveBaseComponent`: add this only for base UI primitives that can be
safely replaced with intrinsic elements and static StyleX styles.
5. Run a dry run first.
- Keep `dryRun: true`.
- Run the runner against the smallest useful file glob.
- Read every warning. Update the adapter instead of hand-editing output
when the warning describes a repeatable project pattern.
6. Run the real transform only after the dry run is clean enough to review.
- Set `dryRun: false`.
- Keep the migration scoped to the selected leaf files.
- Run the project's formatter, typecheck, lint, tests, and Storybook or
visual checks if available.
- Inspect the diff for dropped declarations, inline-style fallbacks, public
component API changes, and cross-file selector bridge/marker behavior.
7. Iterate bottom-up.
- Commit the runner/adapter and each migrated slice separately.
- Expand the `files` glob only after the previous slice is reviewed.
- Preserve warnings or TODOs for any file that needs manual follow-up.
npm install styled-components-to-stylex-codemod
# or
pnpm add styled-components-to-stylex-codemodUse runTransform to transform files matching a glob pattern:
import { runTransform, defineAdapter } from "styled-components-to-stylex-codemod";
const adapter = defineAdapter({
// Map theme paths and CSS variables to StyleX expressions
resolveValue(ctx) {
return undefined;
},
// Map helper function calls to StyleX expressions
resolveCall(ctx) {
return undefined;
},
// Map imported selector helpers such as media query or pseudo aliases
resolveSelector(ctx) {
return undefined;
},
// Control which components accept external className/style, polymorphic `as`, and refs
externalInterface(ctx) {
return { styles: false, as: false, ref: false };
},
// Optional: use a helper for merging StyleX styles with external className/style
styleMerger: null,
// Emit sx={} JSX attributes instead of {...stylex.props()} spreads (requires StyleX ≥0.18)
useSxProp: false,
// Optional override for sx-aware wrapped components. Auto-detection is on by
// default when `useSxProp: true` — the codemod scans the imported component's
// prop type for an `sx?:` member. Use this hook to override (e.g. for package
// imports that cannot be resolved to source on disk).
wrappedComponentInterface(ctx) {
return undefined;
},
// Optional: customize the runtime theme hook import/call used for theme conditionals
// Defaults to { functionName: "useTheme", importSource: { kind: "specifier", value: "styled-components" } }
themeHook: {
functionName: "useTheme",
importSource: { kind: "specifier", value: "styled-components" },
},
});
await runTransform({
files: "src/**/*.tsx",
consumerPaths: null, // set to a glob to enable cross-file selector support
adapter,
dryRun: false,
parser: "tsx",
formatterCommands: ["pnpm prettier --write"],
});Full adapter example
import { runTransform, defineAdapter } from "styled-components-to-stylex-codemod";
const adapter = defineAdapter({
/**
* Resolve dynamic values in styled template literals to StyleX expressions.
* Called for theme access (`props.theme.x`), CSS variables (`var(--x)`),
* and imported values. Return `{ expr, imports }` or `undefined` to skip.
*/
resolveValue(ctx) {
if (ctx.kind === "theme") {
const varName = ctx.path.replace(/\./g, "_");
return {
expr: `tokens.${varName}`,
imports: [
{
from: { kind: "specifier", value: "./design-system.stylex" },
names: [{ imported: "tokens" }],
},
],
};
}
if (ctx.kind === "cssVariable") {
const toCamelCase = (s: string) =>
s.replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
return {
expr: `vars.${toCamelCase(ctx.name)}`,
imports: [
{
from: { kind: "specifier", value: "./css-variables.stylex" },
names: [{ imported: "vars" }],
},
],
};
}
return undefined;
},
/**
* Resolve helper function calls in template interpolations.
* e.g. `${transitionSpeed("slow")}` → `transitionSpeedVars.slow`
* Return `{ expr, imports }` or `undefined` to bail the file with a warning.
*/
resolveCall(ctx) {
const arg0 = ctx.args[0];
const key = arg0?.kind === "literal" && typeof arg0.value === "string" ? arg0.value : null;
if (ctx.calleeImportedName !== "transitionSpeed" || !key) {
return undefined;
}
return {
expr: `transitionSpeedVars.${key}`,
imports: [
{
from: { kind: "specifier", value: "./lib/helpers.stylex" },
names: [{ imported: "transitionSpeed", local: "transitionSpeedVars" }],
},
],
};
},
/**
* Resolve imported values used in selector position, such as media query
* helpers or pseudo-class aliases. Return `undefined` to bail the file.
*/
resolveSelector(ctx) {
return undefined;
},
/**
* Optional: inline styled(ImportedComponent) into an intrinsic element.
* When the base component can be resolved statically, return the target
* element, consumed props, and base StyleX declarations. Return undefined
* to keep normal styled(Component) behavior.
*/
resolveBaseComponent(ctx) {
if (ctx.importSource !== "@company/ui" || ctx.importedName !== "Flex") {
return undefined;
}
const sx: Record<string, string> = { display: "flex" };
const consumedProps = ["column", "gap", "align"];
if (ctx.staticProps.column === true) {
sx.flexDirection = "column";
}
if (typeof ctx.staticProps.gap === "number") {
sx.gap = `${ctx.staticProps.gap}px`;
}
return { tagName: "div", consumedProps, sx };
},
/**
* Control which exported components accept external className/style,
* polymorphic `as`, and/or refs. Return `{ styles, as, ref }` flags.
*/
externalInterface(ctx) {
if (ctx.filePath.includes("/shared/components/")) {
return { styles: true, as: true, ref: true };
}
return { styles: false, as: false, ref: false };
},
/**
* When `externalInterface` enables styles, use a helper to merge
* StyleX styles with external className/style props.
* See test-cases/lib/mergedSx.ts for a reference implementation.
*/
styleMerger: {
functionName: "mergedSx",
importSource: { kind: "specifier", value: "./lib/mergedSx" },
},
/**
* Emit sx={} JSX attributes instead of {...stylex.props()} spreads.
* Requires @stylexjs/babel-plugin ≥0.18 with sxPropName enabled.
*/
useSxProp: false,
/**
* Optional override for sx-aware wrapped components.
*
* When `useSxProp: true`, the codemod auto-detects whether an imported
* component accepts an `sx` prop by walking its declared prop type
* (intersections, type aliases, and interfaces in the same file). When
* `styled(Component)` wraps an sx-aware component, the codemod emits
* `<Component sx={styles.x} />` instead of `<Component {...stylex.props(styles.x)} />`
* and lets the wrapped component merge className/style itself.
*
* Use this hook to override auto-detection for cases it can't see, such as
* unresolvable package imports or components whose sx support is added by a
* HOC at runtime. Returning `undefined` falls through to auto-detection.
*/
wrappedComponentInterface(ctx) {
if (ctx.importSource.startsWith("@company/ui/")) {
return { acceptsSx: true };
}
return undefined;
},
/**
* Optional: customize the runtime theme hook used when wrappers need theme booleans.
* Defaults to useTheme from styled-components.
*/
themeHook: {
functionName: "useDesignTheme",
importSource: { kind: "specifier", value: "@company/theme-hooks" },
},
});
await runTransform({
files: "src/**/*.tsx",
consumerPaths: null,
adapter,
dryRun: false,
parser: "tsx",
formatterCommands: ["pnpm prettier --write"],
});Adapters are the main extension point, see full example above. They let you control:
- how theme paths, CSS variables, and imported values are turned into StyleX-compatible JS values (
resolveValue) - what extra imports to inject into transformed files (returned from
resolveValue) - how helper calls are resolved (via
resolveCall({ ... })returning{ expr, imports }, or{ preserveRuntimeCall: true }to keep only the original helper runtime call;undefinedbails the file) - how imported media-query or pseudo selector helpers are resolved (
resolveSelector) - which exported components should support external className/style extension, polymorphic
as, and/or refs (externalInterface) - how className/style merging is handled for components accepting external styling (
styleMerger) - which imported components already accept a StyleX
sxprop (auto-detected from the imported component's prop type whenuseSxProp: true; can be overridden viawrappedComponentInterface). When detected, the codemod emitssx={styles.x}on the wrapped component instead of{...stylex.props(styles.x)}. - which runtime theme hook import/call to use for emitted wrapper theme conditionals (
themeHook) - how
styled(ImportedComponent)wrapping an external base component can be inlined into an intrinsic element with static StyleX styles (resolveBaseComponent)
consumerPaths is required. Pass null to opt out, or a glob pattern to enable cross-file selector scanning.
When transforming a subset of files, other files may reference your styled components as CSS selectors (e.g. ${Icon} { fill: red }). Pass consumerPaths to scan those files and wire up cross-file selectors automatically:
await runTransform({
files: "src/components/**/*.tsx", // files to transform
consumerPaths: "src/**/*.tsx", // additional files to scan for cross-file usage
adapter,
});- Files in both
filesandconsumerPathsuse the marker sidecar strategy (both consumer and target are transformed, usingstylex.defineMarker()). - Files in
consumerPathsbut not infilesuse the bridge strategy (a stableclassNameis added to the converted component so unconverted consumers' selectors still work).
Instead of manually specifying which components need styles, as, or ref support, set externalInterface: "auto" to auto-detect usage by scanning consumer code.
Note
Experimental. Requires consumerPaths and a successful prepass scan.
If prepass fails, runTransform() throws (fail-fast) when externalInterface: "auto" is used.
import { runTransform, defineAdapter } from "styled-components-to-stylex-codemod";
const adapter = defineAdapter({
// ...
externalInterface: "auto",
});
await runTransform({
files: "src/**/*.tsx",
consumerPaths: "src/**/*.tsx", // required for auto-detection
adapter,
});When externalInterface: "auto" is set, runTransform() scans files and consumerPaths for styled(Component) calls plus JSX usage such as <Component as={...}>, ref, className, and style, resolves imports back to the component definition files, and returns the appropriate { styles, as, ref } flags automatically.
If that prepass scan fails, runTransform() stops and throws an actionable error rather than silently falling back to non-auto behavior.
Troubleshooting prepass failures with "auto":
- verify
consumerPathsglobs match the files you expect - confirm the selected parser matches your source syntax (
parser: "tsx",parser: "ts", etc.) - check resolver inputs (import paths, tsconfig path aliases, and related module resolution config)
- if needed, switch to a manual
externalInterface(ctx)function to continue migration while you fix prepass inputs
Use this when you want to replace a base component entirely by inlining its styles. If your codebase has a layout primitive like <Flex> whose behavior is purely CSS, the codemod can eliminate the runtime import and render a plain <div> instead.
The resolver receives ctx.importSource, ctx.importedName, and ctx.staticProps (from .attrs() and JSX call sites). Return { tagName, consumedProps, sx } to inline, or undefined to skip.
// Input
const Container = styled(Flex).attrs({ column: true, gap: 16 })`
padding: 8px;
`;// Adapter
resolveBaseComponent(ctx) {
if (ctx.importedName !== "Flex") return undefined;
const sx: Record<string, string> = { display: "flex" };
if (ctx.staticProps.column === true) sx.flexDirection = "column";
if (typeof ctx.staticProps.gap === "number") sx.gap = `${ctx.staticProps.gap}px`;
return { tagName: "div", consumedProps: ["column", "gap", "align"], sx };
},// Output — Flex is gone, its styles are merged into stylex.create()
const styles = stylex.create({
container: { display: "flex", flexDirection: "column", gap: "16px", padding: "8px" },
});If the base component's styles already exist as a stylex.create() object, return mixins instead of (or alongside) sx. The codemod imports the mixin and includes it in stylex.props(...):
resolveBaseComponent(ctx) {
return {
tagName: "div",
consumedProps: ["column", "gap"],
mixins: [{ importSource: "./lib/mixins.stylex", importName: "mixins", styleKey: "flex" }],
};
},
// Output: <div {...stylex.props(mixins.flex, styles.container)} />When the codemod encounters an interpolation inside a styled template literal, it runs an internal dynamic resolution pipeline which covers common cases like:
- theme access (
props.theme...) viaresolveValue({ kind: "theme", path }) - indexed theme lookups (
props.theme.color[props.$bg]) — whenctx.indexedLookupis true, return{ usage: "props", dynamicArgUsage: "memberAccess" }to emit a prebuilt per-property mixin map (e.g.,$colorMixins.backgroundColor[bg]) instead of a dynamic style function - imported value access (
import { zIndex } ...; ${zIndex.popover}) viaresolveValue({ kind: "importedValue", importedName, source, path }) - prop access (
props.foo) and conditionals (props.foo ? "a" : "b",props.foo && "color: red;") - helper calls (
transitionSpeed("slowTransition")) viaresolveCall({ ... })— the codemod infers usage from context:- With
ctx.cssProperty(e.g.,color: ${helper()}) → result used as CSS value instylex.create() - Without
ctx.cssProperty(e.g.,${helper()}) → result used as StyleX styles instylex.props() - Use the optional
usage: "create" | "props"field to override the default inference - Use
preserveRuntimeCall: trueto keep the original helper call as a runtime style-function override (with or without a static fallback fromexpr)
- With
- if
resolveCallreturnsundefined, the transform bails the file and logs a warning - helper calls applied to prop values (e.g.
shadow(props.shadow)) by emitting a StyleX style function that calls the helper at runtime - conditional CSS blocks via ternary (e.g.
props.$dim ? "opacity: 0.5;" : "")
If the pipeline can't resolve an interpolation:
- for some dynamic value cases, the transform preserves the value as a wrapper inline style so output keeps visual parity (at the cost of using
style={...}for that prop) - otherwise, the declaration containing that interpolation is dropped and a warning is produced (manual follow-up required)
- Flow type generation is non-existing, works best with TypeScript or plain JS right now. Contributions more than welcome!
- createGlobalStyle: detected usage is reported as an unsupported-feature warning (StyleX does not support global styles in the same way).
- Theme prop overrides: passing a
themeprop directly to styled components (e.g.<Button theme={...} />) is not supported and will bail with a warning.
MIT