@basic-ui/styled-system-to-tailwind-codemod
v0.0.5
Published
Codemod to migrate @emotion/react + styled-system Box props to Tailwind classes
Readme
@basic-ui/styled-system-to-tailwind-codemod
A codemod that migrates @emotion/react + styled-system style props (used by
Box, Text, and the rest of @basic-ui/material) into plain Tailwind utility
classes plus the project's semantic color/typography utilities.
It rewrites JSX like:
<Box display="flex" alignItems="center" px={3} bg="surface-container" />into:
<Box className="flex items-center px-4 bg-surface-container" />Anything that can't be expressed statically (responsive arrays, nested sx
selectors, dynamic colors, alpha() calls, …) is left untouched and tagged
with:
// TODO(tailwind-migration): too complex to migrate automaticallyFor a step-by-step guide to resolving those markers by hand (dynamic values,
unknown colors, complex sx objects, responsive arrays, tricky borders), see the
bundled SKILL.md.
Usage
# from the repo root
yarn workspace @basic-ui/styled-system-to-tailwind-codemod build
# dry run (prints what would change, writes nothing)
node packages/styled-system-to-tailwind-codemod/bin/cli.js --dry-run ./large-codebase
# migrate for real (also runs oxfmt on the changed files)
node packages/styled-system-to-tailwind-codemod/bin/cli.js ./large-codebaseThe transform parses with recast so unrelated formatting is preserved. Because
recast reflows the small number of elements that have direct text children, the
CLI runs oxfmt on changed files afterwards (run yarn format yourself if the
automatic step is skipped).
Options
| Flag | Description |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --dry-run, -d | Report changes without writing files. |
| --no-format | Skip the post-run oxfmt pass. |
| --no-detect-wrappers | Disable the project-wide local Box-wrapper detection pre-pass. |
| --responsive-arrays | Convert responsive array values (display={['none','flex']} → hidden sm:flex). Off by default — only correct when styled-system breakpoints are aligned with Tailwind's. |
| --components A,B | Also migrate these component names regardless of import source. Rarely needed thanks to auto-detection; use it for wrappers the heuristic can't see. |
| --modules m1,m2 | Override the module specifiers whose imports are migration targets (default @basic-ui/material). |
| --color-unsafe A,B | Leave the color prop untouched on these components (when color selects a variant rather than text color). |
Targeting
Every component imported from @basic-ui/material is a target, because they all
forward styled-system props through Box.
In addition, the CLI runs a project-wide pre-pass that finds local Box
wrapper components and migrates their props too. A component counts as a
wrapper when its props type references a *Props type from @basic-ui/material
(e.g. BoxProps, ButtonProps) — directly, through a local type alias /
interface … extends …, or an intersection like BoxProps & { … }. This covers
patterns such as:
// Icons/NorthEastIcon.tsx — detected because props is BoxProps
export const NorthEastIcon = (props: BoxProps<SVGSVGElement>) => (
<Box as="svg" {...props} />
);
// components/Link/ButtonLink.tsx — detected via ButtonLinkProps -> ButtonProps
export type ButtonLinkProps = Omit<ButtonProps, 'href'> & NextLinkProps;
export const ButtonLink: FC<ButtonLinkProps> = forwardRef(/* … */);Detected wrapper names are matched globally by name. Disable with
--no-detect-wrappers, or add names the heuristic misses with --components.
What it migrates
- Spacing —
m/pfamily using the styled-system space scale (p={3}→p-4),auto, negative margins, and explicit pixels. - Sizing —
width/height/min*/max*as raw pixels (width={24}→w-6), percentages (width="100%"→w-full), and keywords. - Flexbox —
display,flexDirection,alignItems,justifyContent,flex,flexGrow/flexShrink,flexWrap,gap. - Position —
position,top/right/bottom/left,zIndex. - Color —
bg/backgroundColor/color/borderColorto the project'sbg-*/text-*/border-*semantic color utilities, hex/rgb to arbitrary values. - Border radius / shadow — theme
radiiandshadowsscales. - Typography —
textAlign,fontWeight,fontSize,textTransform,whiteSpace,overflow,opacity(incl. string / fractional values), and thevariant: 'text.body-small'shorthand →typography-body-small. - Borders —
border/borderTop…shorthand ('1px solid'→border border-solid),borderWidth,borderStyle,borderColor. - SVG / misc —
fill(incl.none/currentColor),cursor,pointerEvents,textOverflow,textDecoration. - Helper calls —
rem(24)/em(…)are resolved to pixels, andalpha('on.surface', 0.7)becomes the opacity modifiertext-on-surface/70. Imports left unused by these rewrites (rem,em,alpha) are pruned. sx/__cssobjects — converted only when every property maps cleanly; otherwise the object is preserved and flagged.
Responsive arrays
Responsive array values (display={['none', 'flex']}) are left as TODOs by
default: styled-system's default 52em breakpoint doesn't line up with
Tailwind's md (48em), so an automatic rewrite would be wrong.
If your app overrides styled-system's breakpoints to match Tailwind's
(['640px', '768px', '1024px', '1280px', '1536px']), pass --responsive-arrays
to convert them by array index:
| index | breakpoint | prefix |
| ----- | ---------- | ------ |
| 0 | (base) | — |
| 1 | 640px | sm: |
| 2 | 768px | md: |
| 3 | 1024px | lg: |
| 4 | 1280px | xl: |
| 5 | 1536px | 2xl: |
<Box display={['none', 'none', 'block']} px={[2, 3]} />
// -> className="hidden md:block px-2 sm:px-4"Array holes / undefined entries inherit the previous breakpoint, and a
breakpoint that resolves to the same value as the one before it is dropped. An
array with any non-static element still bails to a TODO.
className is merged intelligently: string literals are concatenated, dynamic
values are wrapped with clsx(...) (auto-imported), and a generated className
keeps its position relative to any {...spread} so precedence is preserved.
Development
# run the test suite (jest, from the repo root)
yarn jest packages/styled-system-to-tailwind-codemodThe mapping logic lives in src/mappings.ts (pure, easily unit tested) and the
AST rewriting in src/transform.ts.
