npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

styled-components-to-stylex-codemod

v0.0.32

Published

Codemod to transform styled-components to StyleX

Downloads

13,116

Readme

styled-components-to-stylex-codemod

Transform styled-components to StyleX.

Try it in the online playground — experiment with the transform in your browser.

Installation

npm install styled-components-to-stylex-codemod
# or
pnpm add styled-components-to-stylex-codemod

Usage

Use 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 null;
  },
  // Map helper function calls to StyleX expressions
  resolveCall(ctx) {
    return null;
  },
  // Control which components accept external className/style and polymorphic `as`
  externalInterface(ctx) {
    return { style: false, as: 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: 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"],
});
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 `null` 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 null;
  },

  /**
   * Resolve helper function calls in template interpolations.
   * e.g. `${transitionSpeed("slow")}` → `transitionSpeedVars.slow`
   * Return `{ expr, imports }` or `null` 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 null;
    }

    return {
      expr: `transitionSpeedVars.${key}`,
      imports: [
        {
          from: { kind: "specifier", value: "./lib/helpers.stylex" },
          names: [{ imported: "transitionSpeed", local: "transitionSpeedVars" }],
        },
      ],
    };
  },

  /**
   * 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
   * and/or polymorphic `as` prop. Return `{ styles, as }` flags.
   */
  externalInterface(ctx) {
    if (ctx.filePath.includes("/shared/components/")) {
      return { styles: true, as: true };
    }
    return { styles: false, as: 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: 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"],
});

Adapter

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; null/undefined bails the file)
  • which exported components should support external className/style extension and/or polymorphic as prop (externalInterface)
  • how className/style merging is handled for components accepting external styling (styleMerger)
  • 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)

Cross-file selectors (consumerPaths)

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 files and consumerPaths use the marker sidecar strategy (both consumer and target are transformed, using stylex.defineMarker()).
  • Files in consumerPaths but not in files use the bridge strategy (a stable className is added to the converted component so unconverted consumers' selectors still work).

Auto-detecting external interface usage (experimental)

Instead of manually specifying which components need styles or as 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 and <Component as={...}> JSX usage, resolves imports back to the component definition files, and returns the appropriate { styles, as } 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 consumerPaths globs 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

Base component resolution (resolveBaseComponent)

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

Dynamic interpolations

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...) via resolveValue({ kind: "theme", path })
  • imported value access (import { zIndex } ...; ${zIndex.popover}) via resolveValue({ kind: "importedValue", importedName, source, path })
  • prop access (props.foo) and conditionals (props.foo ? "a" : "b", props.foo && "color: red;")
  • helper calls (transitionSpeed("slowTransition")) via resolveCall({ ... }) — the codemod infers usage from context:
    • With ctx.cssProperty (e.g., color: ${helper()}) → result used as CSS value in stylex.create()
    • Without ctx.cssProperty (e.g., ${helper()}) → result used as StyleX styles in stylex.props()
    • Use the optional usage: "create" | "props" field to override the default inference
    • Use preserveRuntimeCall: true to keep the original helper call as a runtime style-function override (with or without a static fallback from expr)
  • if resolveCall returns null or undefined, 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)

Limitations

  • 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 theme prop directly to styled components (e.g. <Button theme={...} />) is not supported and will bail with a warning.

Migration game plan

1. Define your theme and mixins as StyleX

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" },
});

2. Write an adapter and run the codemod

The adapter maps your project's props.theme.* access, CSS variables, and helper calls to the StyleX equivalents from step 1. See Usage for the full API.

3. Convert bottom-up (leaf components first)

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.

4. Verify, iterate, clean up

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.

License

MIT