Skip to content

Migrating from Tailwind CSS

Move a Tailwind CSS v4 project to Stillwind, from the stylesheet and build to components and configuration.

You want to move an existing Tailwind CSS v4 project to Stillwind. Your utilities, variants, theme, and plugins keep working. What changes is where you write them: in style() calls instead of className strings. Tailwind CSS compatibility lists the tested plugins and the differences.

Tailwind scans your files for anything that looks like a class name. Stillwind generates CSS only for the utilities in style() calls. A class in a plain className string, or in HTML from another source, gets no CSS after the migration. Move every utility that you want to keep into style().

Replace the stylesheet and the build plugin

In your stylesheet, replace @import "tailwindcss" with @import "stillwind":

@import "stillwind";

@theme {
  --color-brand: oklch(0.6 0.2 250);
}

Keep your @theme, @utility, @custom-variant, @variant, @apply, and @plugin rules.

Replace the Tailwind build plugin, such as @tailwindcss/vite, with the Stillwind integration. Then create stillwind.config.ts, as described in Getting started, and set up your framework, as described in Framework integrations.

Move classes into style()

Replace each className string with a style() call that you spread onto the element:

import { style } from "stillwind/react";

export function Button() {
  return <button {...style("rounded-md bg-brand px-4 py-2 text-white")}>Continue</button>;
}

Import style from the entrypoint of the framework that renders the element. For a class that another stylesheet defines, pass it in a class field, such as style("p-4", { class: "external-card" }).

A large codebase may have hundreds of className strings. Your coding agent can write a one-off codemod for your repository that converts them in one pass.

Replace class helpers

style() and style.cva() replace the libraries that Tailwind projects use to build class strings:

Library Stillwind
clsx, classnames style() with conditions
tailwind-merge Compose a base with new utilities
cva from class-variance-authority style.cva() with the same configuration

clsx and classnames

Pass conditions as arguments:

import { style } from "stillwind/react";

export function SaveButton({ loading }: { loading: boolean }) {
  return (
    <button {...style("rounded-md bg-brand px-4 py-2 text-white", loading && "opacity-60")}>
      Save
    </button>
  );
}

Do not convert the object form of clsx. In clsx({ "opacity-60": loading }), each key is a class name. In style(), each key of an object is a variant, such as "hover:". Rewrite each entry as loading && "opacity-60".

tailwind-merge

Remove twMerge() calls. Store the base style and pass it to another style() call. The compiler emits the new utilities after the base’s CSS. They override competing declarations when the other cascade criteria, such as specificity and importance, are equal. See Override a style and CSS ordering.

cva

Replace cva( with style.cva(, and import VariantProps from your framework’s entrypoint:

import { style, type VariantProps } from "stillwind/react";

const button = style.cva("rounded-md px-4 py-2", {
  variants: {
    tone: {
      neutral: "bg-gray-100",
      primary: "bg-brand text-white",
    },
  },
  defaultVariants: {
    tone: "neutral",
  },
});

export function Button({ tone }: VariantProps<typeof button>) {
  return <button {...button({ tone })}>Continue</button>;
}

variants, defaultVariants, and compoundVariants work as in class-variance-authority. Write each option value as a plain utility string, without && or a ternary. The props that the factory returns cannot be passed to style() as a base. The style.cva() reference lists the other differences.

For new components, consider state variants and data-* attributes instead of options. See Reusable styles.

Move configuration

Some Tailwind settings move from the stylesheet to stillwind.config.ts:

Tailwind Stillwind
@config "./tailwind.config.js" The tailwind field of the project
@import "tailwindcss" important The important field of the project
@source and source(...) Not supported; see below
blocklist The no-restricted-utilities lint rule
prefix(tw) Write utilities without tw:; see below
theme(...) on the import Not supported; define the tokens in @theme

The tailwind field accepts a Tailwind configuration object. Its theme, presets, plugins, darkMode, and important keys work as in Tailwind. Other keys have no effect. This field requires the tailwindcss package.

Set important in one place only. Setting both important and tailwind.important is an error.

Stillwind does not need the tw: class prefix, because it generates unique class names. Theme variables can still collide with other CSS in the page. To keep the --tw- names of prefix(tw), set themePrefix: "--tw-" on the project. The prefix applies to the generated CSS, including var(--color-brand) in your stylesheet and in arbitrary values, so you can keep var(--tw-color-brand) or use the shorter name. JavaScript that reads or sets a theme variable by name must use the prefixed name.

Stillwind does not support @source, including @source inline(), or a safelist, because only style() calls generate utilities. Tailwind classes inside a dependency also get no CSS. Write each utility that you need in a style() call in your own code.

Stillwind ignores @config, @source, @reference, and import options in the stylesheet, and the CLI reports them with ignored-tailwind-feature. Remove them after you move their settings. The same rule reports a v3 path in theme(), such as theme(colors.red.500). Use --theme(--color-red-500) instead.

Keep your @plugin rules and the @import rules of CSS packages. Tailwind CSS compatibility lists the packages that the test suite covers.

Check the migrated code

Run the CLI checks over the migrated files. They report utilities that Stillwind cannot resolve and Tailwind features that it ignores. Then test the production build and your pages in the browser.