Migrate from Tailwind
The csszyx migrate CLI command converts className= (JSX/TSX) or class=
(HTML) attributes to sz= props automatically. It handles static strings, clsx
calls, ternary expressions, and template literals.
The CLI ships as a separate package, @csszyx/cli. Run it one-off with
npx @csszyx/cli (or pnpm dlx @csszyx/cli), or install it as a dev dependency
(pnpm add -D @csszyx/cli) and then call csszyx directly.
Basic Usage
Section titled “Basic Usage”npx @csszyx/cli migrate src/ # migrate all JSX/TSX/HTML under src/npx @csszyx/cli migrate --dry-run # preview changes without writing filesnpx @csszyx/cli migrate --ignore "**/*.test.tsx,**/fixtures/**"npx @csszyx/cli migrate --pattern "src/components/**/*.tsx"Migration logs are written to .csszyx/logs/. Add .csszyx/ to .gitignore.
Understanding the Migration Report
Section titled “Understanding the Migration Report”After each run, migrate prints a summary (also written to .csszyx/logs/)
grouped by what happened to each class. The buckets tell you exactly what, if
anything, still needs manual attention:
| Report line | What it means | What to do |
|---|---|---|
classNames converted: N | Recognized utilities moved into sz={...} | Nothing |
classNames kept on components (no sz support): N | className on a custom component (<Card className="…" />), not a DOM element. sz only applies to host/DOM elements, so these are left untouched | Convert by hand if the component forwards styles; otherwise leave as-is |
classNames skipped (dynamic): N | A clsx / ternary / template-literal expression migrate could not safely rewrite | Review by hand — simple dynamic cases are converted automatically |
Unrecognized classes (N): … | Classes with no Tailwind/sz mapping (e.g. a project class like sport-neon). They stay in className and are listed here | Feed into --audit → .csszyx-todo.json and resolve |
Two behaviors to call out explicitly:
- Unknown static classes stay put. A class like
sport-neonis a regular static CSS class, not an atomic utility — migrate leaves it inclassNameand reports it as unrecognized. It is never folded into a dynamic_szMergecall alongside converted classes. - Unrecognized classes inside skipped dynamic patterns are still surfaced.
When a
clsx/ternary/template expression is skipped, any unmapped classes inside it still appear in theUnrecognized classeslist — nothing slips through silently, so the audit map stays complete.
Audit — Discover Unrecognized Classes
Section titled “Audit — Discover Unrecognized Classes”Before migrating, run an audit to see what csszyx cannot automatically convert:
npx @csszyx/cli migrate --auditThis scans your codebase and writes .csszyx-todo.json without touching any
source files. Each entry starts as "sz:todo":
{ "btn": "sz:todo", "custom-card": "sz:todo", "animate-spin-slow": "sz:todo"}Resolving the Todo Map
Section titled “Resolving the Todo Map”Edit .csszyx-todo.json to tell csszyx what to do with each class:
| Value | Meaning |
|---|---|
"sz:todo" | Not yet decided — skip, surface in reports |
"sz:keep" | Keep in className, acknowledged as intentional |
"sz:remove" | Drop from output entirely |
{ p: 4, bg: 'blue-500' } | Direct sz object — merged into sz prop |
"p-4 bg-blue-500" | Tailwind string — auto-converted to sz |
null / false | Same as "sz:todo" (backwards compat) |
--resolve-todos — Apply the Resolution Map
Section titled “--resolve-todos — Apply the Resolution Map”npx @csszyx/cli migrate --resolve-todos .csszyx-todo.jsonReads .csszyx-todo.json and applies it during migration. Classes mapped to
sz:keep stay in className; sz:remove entries are dropped; sz objects and
Tailwind strings are converted.
--resolve-todos is read-only — it never writes to the todo file. Still-unresolved
sz:todo entries appear in the console and log only.
Display Utilities
Section titled “Display Utilities”When migrating Tailwind display classes, csszyx emits the canonical display
property instead of boolean sugar:
| Tailwind | Migrated sz |
|---|---|
block | { display: 'block' } |
inline | { display: 'inline' } |
flex | { display: 'flex' } |
inline-flex | { display: 'inline-flex' } |
hidden | { display: 'none' } |
Manual sz authoring can still use sugar such as { flex: true }, but the
migration output is canonical so duplicate display classes share one object key
surface. If a class list contains conflicting display utilities in the same
variant scope, csszyx fails closed and leaves those classes in className /
todo output instead of guessing which one should win.
// Safe: display and flex shorthand are different CSS propertiesclassName="flex flex-1"// → sz={{ display: 'flex', flex: '1' }}
// Unsafe: two display values in the same scopeclassName="block flex"// → stays unresolved for manual review--inject-todos — Mark Unresolved Classes in Code
Section titled “--inject-todos — Mark Unresolved Classes in Code”npx @csszyx/cli migrate --inject-todosInserts {/* @sz-todo: classname1, classname2 */} comments above JSX elements
that still have unrecognized classes — a visual marker so you can grep or skim
the diff to find what needs attention.
When --resolve-todos is active, --inject-todos is automatically enabled for
any still-unresolved classes.
Full Workflow
Section titled “Full Workflow”# 1. Dry run — preview what will changenpx @csszyx/cli migrate --dry-run
# 2. Audit — find unrecognized classesnpx @csszyx/cli migrate --audit# → writes .csszyx-todo.json
# 3. Edit .csszyx-todo.json# → set "sz:keep", "sz:remove", or direct sz objects for each entry
# 4. Apply with resolution mapnpx @csszyx/cli migrate --resolve-todos .csszyx-todo.json
# 5. Re-audit if anything remains unresolvednpx @csszyx/cli migrate --auditHTML Files
Section titled “HTML Files”For plain HTML files (no JSX build step), csszyx converts class="..." to
sz="..." attributes. A runtime script is needed at page load to process them —
the migration command can inject it for you.
By default (csszyx migrate public/), the command:
- Converts
class="..."→sz="..."✅ - Injects FOUC prevention CSS into
<head>✅ - Does not inject a runtime script ❌
Injecting the runtime
Section titled “Injecting the runtime”# CDN (default URL: https://cdn.csszyx.com/runtime.js)npx @csszyx/cli migrate public/ --inject-runtime cdn
# Local file (default path: csszyx-runtime.js, relative to each HTML file)npx @csszyx/cli migrate public/ --inject-runtime local
# Custom CDN URLnpx @csszyx/cli migrate public/ --inject-runtime cdn --cdn-url https://my-cdn.com/csszyx.js
# Custom local pathnpx @csszyx/cli migrate public/ --inject-runtime local --local-path ./vendor/csszyx-runtime.jsThe runtime script tag is injected before </body>.
FOUC prevention
Section titled “FOUC prevention”Enabled by default. Injects this block before </head>:
<style> /* csszyx: hide [sz] elements until runtime processes them */ [sz] { visibility: hidden; } body.sz-ready [sz] { visibility: visible; }</style>Pass --no-fouc to skip this if you are managing the loading transition yourself.
--braces flag
Section titled “--braces flag”Controls the format of the generated sz attribute value.
Without --braces (default — bare object contents):
<div sz="p: 4, bg: 'blue-500'"></div>With --braces (full object syntax):
<div sz="{ p: 4, bg: 'blue-500' }"></div>Use --braces if your HTML is processed by a template engine that expects
full object literal syntax.
TypeScript Types
Section titled “TypeScript Types”The todo map types are exported for use in custom tooling:
import type { CsszyxTodoEntry, CsszyxTodoMap } from '@csszyx/cli';
// CsszyxTodoEntry = Record<string, unknown> | string | null | false// CsszyxTodoMap = Record<string, CsszyxTodoEntry>