css-transformer
Migrate Tailwind class strings to Tailwindest object styles.
tailwindest-css-transform is a migration tool. It rewrites supported
Tailwind class-string patterns into Tailwindest object-style calls, while
preserving source code when exact conversion is not proven.
Use it when you are moving an existing Tailwind codebase from className,
cn, clsx, classNames, or cva strings into Tailwindest styles.
Install
Run the transformer directly:
npx tailwindest-css-transform src/componentsOr install it for repeated local use:
pnpm add -D tailwindest-css-transformThe CLI needs only the file or directory to transform. If you omit the target, it opens an interactive prompt that asks only for that path, then prints the detected Tailwind CSS entry, Tailwindest import, mode, walkers, and dry-run setting.
What It Converts
Input:
import { cn } from "@/lib/utils"
export function Button() {
return (
<button
className={cn(
"flex items-center rounded-md bg-red-50 px-4 py-2",
"dark:hover:bg-red-950"
)}
/>
)
}Runtime-mode output:
import { tw } from "~/tw"
const buttonButton = tw.style({
display: "flex",
alignItems: "items-center",
borderRadius: "rounded-md",
backgroundColor: "bg-red-50",
paddingLeft: "px-4",
paddingTop: "py-2",
dark: {
hover: {
backgroundColor: "dark:hover:bg-red-950",
},
},
})
export function Button() {
return <button className={buttonButton.class()} />
}Runtime-mode output keeps prefixed nested leaves:
tw.style({
dark: {
hover: {
backgroundColor: "dark:hover:bg-red-950",
},
},
})Output Modes
The transformer is runtime-first:
| Mode | Target | Leaf value |
|---|---|---|
runtime | CreateTailwindest | original token, such as dark:hover:bg-red-950 |
auto | safe default | runtime output |
Recommended Mode Selection
# Safe default for mixed or unknown projects
npx tailwindest-css-transform src/components --mode auto
# Runtime Tailwindest migration
npx tailwindest-css-transform src/components --mode runtimeauto keeps runtime output for unknown or mixed projects.
Runtime Tailwindest Setup
Define your Tailwindest type with CreateTailwindest:
import { createTools, type CreateTailwindest } from "tailwindest"
import type { Tailwind, TailwindNestGroups } from "./tailwind"
import type { TailwindLiteral } from "./tailwind_literal"
export type Tailwindest = CreateTailwindest<{
tailwind: Tailwind
tailwindNestGroups: TailwindNestGroups
useArbitrary: true
useArbitraryNestGroups: true
}>
export const tw = createTools<{
tailwindest: Tailwindest
tailwindLiteral: TailwindLiteral
useArbitrary: true
useTypedClassLiteral: true
}>()Transform with runtime output:
npx tailwindest-css-transform src/components --mode runtimeNested variant leaves preserve the original prefixed class:
tw.style({
dark: {
hover: {
backgroundColor: "dark:hover:bg-red-950",
},
},
})Supported Patterns
The transformer currently supports static class strings in:
className="..."className={"..."}cn("...")clsx("...")classNames("...")cva("...")cva(..., { variants: { ... } })static string options
Dynamic arguments are preserved when the surrounding call can still be represented safely.
cn("flex px-4", isActive && "bg-red-500", props.className)The static part can become a Tailwindest style constant, while dynamic arguments
remain in the generated .class(...) call.
CVA Output
Static cva(..., { variants: ... }) declarations become tw.variants(...).
The generated call sites use .class(...), and VariantProps is replaced with
Tailwindest's GetVariants type.
import { type GetVariants } from "tailwindest"
import { tw } from "~/tw"
const buttonVariants = tw.variants({
base: {
display: "inline-flex",
alignItems: "items-center",
},
variants: {
variant: {
default: {
backgroundColor: "bg-primary",
},
outline: {
borderWidth: "border",
},
},
},
})
interface ButtonProps extends GetVariants<typeof buttonVariants> {
className?: string
}
function Button({ className, variant }: ButtonProps) {
return (
<button
className={tw.join(
buttonVariants.class({ variant }),
className
)}
/>
)
}If a cva(...) declaration has no variant map, the transformer emits
tw.style(...) and rewrites call sites to .class(...).
Conservative Fallback
The transformer does not rewrite code it cannot prove safe.
Common fallback cases:
- template literals with substitutions
- computed class strings
- unknown or ambiguous Tailwind utilities
- runtime-generated
cvavariant maps - unsupported
compoundVariantsconversion - helper imports still used elsewhere
Run dry-run first and inspect diagnostics before writing files.
CLI Options
npx tailwindest-css-transform src/components --mode auto --dry-run| Option | Alias | Default | Description |
|---|---|---|---|
--css <path> | -c | auto-detected | Tailwind CSS entry used to initialize Tailwind. |
--identifier <name> | -i | auto or tw | Tailwindest import identifier. |
--module <path> | -m | auto or ~/tw | Tailwindest module import path. |
--dry-run | -d | false | Preview without writing transformed files. |
--mode <mode> | none | auto | Output mode: auto or runtime. |
Auto discovery uses the same Tailwind CSS root and Tailwind package resolution
helpers as create-tailwind-type. If no Tailwindest createTools export can be
found, the CLI warns and falls back to tw from ~/tw. Explicit flags always
override discovered values:
npx tailwindest-css-transform src/components \
--css src/styles/tailwind.css \
--identifier tw \
--module @/styles/tailwind \
--mode runtimeProgrammatic Usage
import { transform } from "tailwindest-css-transform"
const result = await transform(source, {
resolver,
outputMode: "runtime",
projectRoot: process.cwd(),
sourcePath: "/repo/src/Button.tsx",
tailwindestIdentifier: "tw",
tailwindestModulePath: "~/tw",
walkers: ["cva", "cn", "classname"],
config: {
objectThreshold: 2,
},
})
console.log(result.code)
console.log(result.diagnostics)You normally do not need the programmatic API unless you are building custom migration tooling.
Safety Model
The transformer uses Collect -> Reverse Execute:
- Parse source with
ts-morph. - Collect supported transform targets.
- Analyze static class strings.
- Apply replacements from the end of the file to the beginning.
- Apply import edits once.
- Return transformed code and diagnostics.
This avoids stale AST ranges and keeps unrelated source code untouched.