Tailwindest

css-transformer

Migrate Tailwind class strings to Tailwindest object styles.

tailwindest css transformer banner

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.

For supported static class sources, the transformer is lossless: every input token must be represented in the generated Tailwindest output. Tokens that do not resolve to direct CSS properties are preserved as class literals through tw.def(...) or raw tw.join(...).

Install

Run the transformer directly:

npx tailwindest-css-transform src/components

Or install it for repeated local use:

pnpm add -D tailwindest-css-transform

The 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:

ModeTargetLeaf value
runtimeCreateTailwindestoriginal token, such as dark:hover:bg-red-950
autosafe defaultruntime output
# 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 runtime

auto 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 runtime

Nested 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.

Preserved Tokens

Some Tailwind tokens are not direct CSS properties. They may anchor selectors, configure CSS variables, represent plugin animation utilities, or use variant syntax that should stay literal.

For mixed static sources, the transformer emits structured style tokens through Tailwindest and preserved tokens through tw.def(...):

cn("group/card flex text-sm [--card-spacing:--spacing(5)]", className)
tw.join(
    tw.def(["group/card", "[--card-spacing:--spacing(5)]"], cardStyle.style()),
    className
)

Preserved token families include:

  • named group, peer, and container anchors such as group/card, peer/menu-button, and @container/card-header
  • arbitrary declarations such as [--cell-size:2rem]
  • variant arbitrary declarations such as data-[size=sm]:[--card-spacing:--spacing(4)]
  • placement animation utilities such as data-[side=bottom]:slide-in-from-top-2
  • descendant variant chains such as **:data-[slot=kbd]:z-50
  • parenthesized arbitrary values such as xs:w-(--popup-width)

All-unresolved static sources are preserved as raw class streams:

className={tw.join("toaster group")}

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(...).

When CVA strings contain preserved tokens, call sites switch from .class(...) to tw.def(..., helper.style(...)). Base preserved tokens are unconditional; variant-option preserved tokens are conditional on the selected option:

tw.join(
    tw.def(
        [
            "peer/menu-button",
            variant === "outline" && "hover:bg-sidebar-accent",
        ],
        sidebarMenuButtonVariants.style({ variant, size })
    ),
    className
)

If the call site does not expose a safe selected option value, the transformer does not emit the variant-specific preserved token unconditionally. It reports a diagnostic so the case can be reviewed manually.

Conservative Fallback

The transformer does not rewrite code it cannot prove safe.

Common fallback cases:

  • template literals with substitutions
  • computed class strings
  • unsafe conditional CVA preserved-token serialization
  • runtime-generated cva variant maps
  • unsupported compoundVariants conversion
  • helper imports still used elsewhere

Run dry-run first and inspect diagnostics before writing files.

Unknown or ambiguous static utilities are not deleted. They are kept as preserved class literals when the surrounding source shape is supported.

CLI Options

npx tailwindest-css-transform src/components --mode auto --dry-run
OptionAliasDefaultDescription
--css <path>-cauto-detectedTailwind CSS entry used to initialize Tailwind.
--identifier <name>-iauto or twTailwindest import identifier.
--module <path>-mauto or ~/twTailwindest module import path.
--dry-run-dfalsePreview without writing transformed files.
--mode <mode>noneautoOutput 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 runtime

Programmatic 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:

  1. Parse source with ts-morph.
  2. Collect supported transform targets.
  3. Analyze static class strings into a lossless token plan.
  4. Apply replacements from the end of the file to the beginning.
  5. Apply import edits once.
  6. Return transformed code and diagnostics.

This avoids stale AST ranges and keeps unrelated source code untouched.

Registry Verification

The shadcn registry suite is the regression gate for token preservation:

  • shadcn_class_stability.test.ts checks that every supported static input source is stable under tailwind-merge.
  • shadcn_class_preservation.test.ts transforms every registry fixture and asserts that output class-producing expressions contain every input token as a multiset.
  • shadcn_registry.test.ts validates the final transformed snapshots.

On this page