# API Reference (/docs/api)
## `tw` [#tw]
The main export from `@styled-cva/react`. It's a function that can be used to create styled components.
### Basic Usage [#basic-usage]
```tsx
import tw from "@styled-cva/react";
const Button = tw.button`bg-blue-500 text-white`;
```
### With Element Types [#with-element-types]
You can use `tw` with any HTML element:
```tsx
const Div = tw.div`container mx-auto`;
const Input = tw.input`border rounded`;
const A = tw.a`text-blue-500 hover:underline`;
```
### With Custom Components [#with-custom-components]
You can also wrap custom components:
```tsx
import MyComponent from "./MyComponent";
const StyledComponent = tw(MyComponent)`bg-gray-100 p-4`;
```
### Extending Styled Components [#extending-styled-components]
You can extend existing styled components to add additional styles:
```tsx
const BaseButton = tw.button`btn-base`;
// Extend with additional classes
const ExtendedButton = tw(BaseButton)`bg-green-500 hover:bg-green-600`;
// This works with CVA components too
const VariantButton = tw.button("btn-base", {
variants: {
$variant: {
primary: "btn-primary",
secondary: "btn-secondary",
},
},
});
const ExtendedVariantButton = tw(VariantButton)`rounded-lg`;
```
## `.cva()` [#cva]
**Deprecated on intrinsic elements** (`tw.button`, `tw.div`, …): prefer **`tw.element(base, config)`** — same types and runtime as **`tw.element.cva(base, config)`**. The `.cva()` member remains available but may be removed in a future major version.
Creates a styled component with variant support using class-variance-authority.
### Signature [#signature]
Preferred for intrinsics (see above):
```tsx
tw.element(baseClasses, config);
```
Equivalent legacy form:
```tsx
tw.element.cva(baseClasses, config);
```
### Parameters [#parameters]
- `baseClasses` (string): Base CSS classes that are always applied
- `config` (object): CVA configuration object
- `variants` (object): Variant definitions
- `defaultVariants` (object, optional): Default variant values
- `compoundVariants` (array, optional): Compound variant definitions
### Example [#example]
```tsx
const Button = tw.button("btn-base", {
variants: {
$variant: {
primary: "btn-primary",
secondary: "btn-secondary",
},
$size: {
sm: "btn-sm",
md: "btn-md",
lg: "btn-lg",
},
},
defaultVariants: {
$variant: "primary",
$size: "md",
},
compoundVariants: [
{
$variant: "primary",
$size: "lg",
className: "btn-primary-lg",
},
],
});
// Using default variants - they're applied automatically
; // Uses primary variant and md size
// Override defaults
;
```
### Boolean shorthand [#boolean-shorthand]
A string as a variant is a boolean variant. The prop type is `boolean`.
```tsx
const Frame = tw.div("rounded p-4", {
variants: {
$active: "ring-2 ring-blue-500", // same as { true: "ring-2 ring-blue-500", false: "" }
$tone: { info: "bg-blue-50", tip: "bg-green-50" },
},
compoundVariants: [{ $active: true, $tone: "info", className: "font-bold" }],
});
;
```
Use the object form (`{ true: "…", false: "…" }`) when the `false` value also needs classes.
`compoundVariants` and `defaultVariants` use `true` and `false` for the shorthand too.
### Transient Props [#transient-props]
Variant keys starting with `$` are transient props that won't be passed to the
DOM element. This prevents React warnings about unknown props.
## Intrinsic shorthand (`tw.element(base, config)`) [#intrinsic-shorthand-twelementbase-config]
For **intrinsic** factories (`tw.button`, `tw.a`, `tw.div`, …), omit `.cva` and pass the same arguments (**preferred**; `.cva()` on intrinsics is deprecated — see above):
```tsx
tw.element(baseClasses, cvaConfig);
// equivalent to:
tw.element.cva(baseClasses, cvaConfig);
```
Tagged templates (``tw.div`classes` ``) are unchanged. Dispatch treats the first argument as CVA config when it is not a tagged-template receiver. See **`isTaggedTemplateArg`** under [Utilities](#utilities).
## `.withProps()` [#withprops]
Sets default props for a component. User-provided props will override these defaults.
### Signature [#signature-1]
```tsx
Component.withProps(defaultProps);
```
### Parameters [#parameters-1]
- `defaultProps` (object): An object containing default props to apply
### Example [#example-1]
```tsx
const Button = tw.button("btn", {
variants: {
$variant: {
primary: "btn-primary",
secondary: "btn-secondary",
},
},
})
.withProps({
type: "button",
"data-testid": "button",
$variant: "primary",
});
```
Template components have `.withProps()` too. The result keeps `.withStyle()` and `.withProps()`, so you can chain them:
```tsx
// pin a wrapped component's own props
const BannerLink = tw(Button)`h-auto p-0 underline`.withProps({
variant: "link",
});
const SubmitButton = tw.button`px-4 py-2`
.withProps({ type: "submit" })
.withStyle({ minWidth: 120 });
```
In Vue, `withProps` uses Vue's `mergeProps`, so a default `class` and a call-site `class` are joined.
In React and Solid, a call-site prop replaces the default.
## `$as` Prop [#as-prop]
The `$as` prop allows you to render a component as a different element or component.
### Example [#example-2]
```tsx
const Button = tw.button`btn`;
// Render as an anchor tag
;
// Render as a custom component
;
// Works with CVA components too
const VariantButton = tw.button("btn", {
variants: {
$variant: {
primary: "btn-primary",
secondary: "btn-secondary",
},
},
});
Link Button
;
```
### `PolymorphicComponentProps` [#polymorphiccomponentprops]
Utility type for rendering CVA components as **custom React components** (TanStack Router `Link`, Next.js `Link`, etc.) while preserving every `$`-prefixed variant prop (`$variant`, `$size`, `$tone`, …) from the source component.
Use it when the runtime `$as={SomeComponent}` form needs explicit typing (e.g. when wrapping in your own component):
```tsx
import { Link, type LinkProps } from "@tanstack/react-router";
import tw, { type PolymorphicComponentProps } from "@styled-cva/react";
const Button = tw.button("btn", {
variants: {
$variant: { primary: "btn-primary", secondary: "btn-secondary" },
$size: { sm: "px-2", md: "px-3" },
},
});
type ButtonLinkProps = PolymorphicComponentProps &
LinkProps;
const ButtonLink = (props: ButtonLinkProps) => (
);
// Now type-safe end-to-end:
About
;
```
For intrinsic elements (`$as="a"`, `$as="button"`, …) the typing already works without this utility — it's only needed when `$as` is a custom component.
## Utilities [#utilities]
### `cn` [#cn]
A utility function for combining Tailwind CSS classnames. It uses `cn` for intelligent merging and conflict resolution optimized for Tailwind CSS v4.
```tsx
import { cn } from "@styled-cva/react";
const className = cn("text-red-500", "bg-blue-500");
// className = "text-red-500 bg-blue-500"
```
### `isTaggedTemplateArg` [#istaggedtemplatearg]
From `@styled-cva/core`, re-exported by framework packages: `true` when the value is a tagged-template `TemplateStringsArray` (has `raw`). Used internally to distinguish ``tw.div`...` `` from `tw.div(base, config)`.
```tsx
import { isTaggedTemplateArg } from "@styled-cva/react";
```
### `cva` [#cva-1]
The underlying class-variance-authority function. You can use it directly if needed:
```tsx
import { cva } from "@styled-cva/react";
const buttonVariants = cva("btn-base", {
variants: {
variant: {
primary: "btn-primary",
secondary: "btn-secondary",
},
},
});
// Use with className
;
```
When using `cva` directly (not through `tw.element.cva()`), variant keys don't
need the `$` prefix since they're not passed as props to components. The `$`
prefix is only needed when using `tw.element.cva()` to prevent props from
being passed to the DOM.
## TypeScript Types [#typescript-types]
### `TailwindComponent
` [#tailwindcomponentp-o]
The base type for all styled components.
- `P`: The base React props
- `O`: Additional props added by the template function
### `VariantProps` [#variantpropst]
Extracts variant props from a CVA configuration.
```tsx
import { cva, type VariantProps } from "@styled-cva/react";
const buttonVariants = cva("btn", {
variants: {
$variant: {
primary: "btn-primary",
secondary: "btn-secondary",
},
},
});
type ButtonVariants = VariantProps;
// { $variant?: 'primary' | 'secondary' }
```
`VariantProps` works on a `cva` function, not on a styled component. For a styled component, use `VariantsOf` and `VariantOf`.
### `VariantsOf` and `VariantOf` [#variantsofc-and-variantofc-k]
Read the variant types from a styled component. Then you do not need a separate `cva` only for the types.
```tsx
import tw, { type VariantOf, type VariantsOf } from "@styled-cva/react";
const Callout = tw.div("rounded p-4", {
variants: {
$tone: { info: "bg-blue-50", tip: "bg-green-50" },
$active: { true: "ring-2", false: "" },
},
});
type CalloutVariants = VariantsOf;
// { $tone?: "info" | "tip" | null; $active?: boolean | null }
type Tone = VariantOf; // "info" | "tip"
type Active = VariantOf; // boolean
const toneIcon: Record = { info: "ℹ️", tip: "💡" };
```
- `VariantsOf` gives all `$`-prefixed props of the component, without `$as`.
- `VariantOf` gives the values of one variant, without `null` and `undefined`.
- A boolean variant (`{ true, false }` or the string shorthand) gives `boolean`.
- They work on `tw.tag(base, config)`, `tw(Component, base, config)`, `.withProps()` results and template components. For a template component, they give the `$` props of its type parameter.
## Advanced: `.withStyle()` [#advanced-withstyle]
For additional flexibility, basic styled components (not CVA components) support `.withStyle()` to add inline styles:
```tsx
const Button = tw.button`btn`.withStyle({
transition: "all 0.2s",
});
```
This is only available on basic styled components created with template
literals. CVA components do not support this method.
# Biome Plugin (/docs/biome-plugin)
The `@styled-cva/biome-plugin` package ships Biome GritQL rules that lint `tw.tag\`…\`` / `tw(Component)\`…\`` tagged templates for abnormal whitespace and excessive single-line length, including safe auto-fixes for whitespace normalization.
Part of the styled-cva monorepo. Published to npm as `@styled-cva/biome-plugin`.
## Safe auto-fix for whitespace normalization [#safe-auto-fix-for-whitespace-normalization]
Biome 2.x supports GritQL rewrites with safe fixes. `@styled-cva/biome-plugin` provides:
- **`normalize-tw-classes`**: registers a **safe auto-fix** (`fix_kind = "safe"`). Running `biome lint --write` (or `biome check --write`) automatically normalizes irregular horizontal whitespace inside single-line `tw` templates.
- **`prefer-size-class`**: registers a **safe auto-fix** (`fix_kind = "safe"`). Automatically converts identical width and height utilities (e.g. `w-4 h-4`, `h-8 w-8`, or `md:w-12 md:h-12`) into shorthand `size-n` (`size-4`, `size-8`, `md:size-12`).
- **`multiline-long-tw`**: provides **diagnostics only**. Multi-line template formatting (indentation, line wrapping) is handled by [`@styled-cva/prettier-plugin`](/docs/prettier-plugin).
The two plugins complement each other:
- `@styled-cva/biome-plugin` — flags issues and auto-fixes whitespace and size shorthands in `biome check --write` / `biome lint --write`
- `@styled-cva/prettier-plugin` — formats and wraps long templates across multiple lines on save
## Rules [#rules]
### `normalize-tw-classes` [#normalize-tw-classes]
Flags inline `tw.tag` / `tw(Component)` tagged templates whose class string contains:
- a run of 2+ horizontal whitespace characters,
- leading horizontal whitespace,
- or trailing horizontal whitespace.
Multi-line templates (whose chunk contains a `\n`) are **exempt** so that prettier-formatted multi-line `tw.div` templates do not trigger the rule.
### `prefer-size-class` [#prefer-size-class]
Flags inline `tw.tag` / `tw(Component)` tagged templates containing identical width and height classes (e.g. `w-4 h-4`, `h-4 w-4`, `md:w-8 md:h-8`).
**Auto-fix:** Rewrites pairs of identical width and height classes to `size-n` (`md:size-8`, etc.) preserving any variant modifiers.
### `multiline-long-tw` [#multiline-long-tw]
Flags inline `tw.tag` / `tw(Component)` tagged templates whose quasi text exceeds 80 characters on a single line. Multi-line templates are exempt.
## Installation [#installation]
```bash
bun add -D @styled-cva/biome-plugin
```
**Peer dependency:** `@biomejs/biome` (^2).
## Configuration [#configuration]
In `biome.json`:
```json
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"plugins": ["@styled-cva/biome-plugin"]
}
```
Biome automatically resolves the plugin from `node_modules` and runs the rules during `biome lint` and `biome check`.
# ESLint Plugin (/docs/eslint-plugin)
The `@styled-cva/eslint-plugin` package provides an ESLint rule that suggests extracting JSX elements with many Tailwind classes into named styled components. The rule is **fixable**, so you can apply the suggestion automatically.
Part of the styled-cva monorepo. Published to npm as `@styled-cva/eslint-plugin`. Install with your package manager or link the workspace during development.
## Rule: `prefer-styled-cva` [#rule-prefer-styled-cva]
The rule reports when a **native HTML element** (e.g. `button`, `div`, `span`) has a `className` with more than a configured number of classes, and suggests extracting it to a styled component. Only **lowercase** element names are checked (custom components are ignored).
- **Default threshold:** 5 classes (configurable)
- **Fixable:** yes — ESLint can add the import, create the styled component, and replace the inline element
### Example [#example]
**Before** (reported when class count exceeds the threshold):
```tsx
```
**After** (auto-fix):
```tsx
import tw from "@styled-cva/react";
const StyledButton = tw.button`rounded bg-blue-500 px-4 py-2 text-white font-bold hover:bg-blue-600`;
// ...
Submit;
```
The plugin recognizes both `styled-cva` and `@styled-cva/react` as the styled import.
Run the ESLint fix (e.g. `eslint --fix` or your editor’s “Fix all”) to apply the extraction automatically.
## Installation [#installation]
In a project that uses `@styled-cva/react`:
```bash
npm install -D @styled-cva/eslint-plugin
# or
bun add -d @styled-cva/eslint-plugin
```
When developing in the monorepo, from the repo root:
```bash
bun install
bun run --filter @styled-cva/eslint-plugin build
```
**Peer dependencies:** `eslint` (^9 || ^10), `typescript` (^5 || ^6).
## Configuration [#configuration]
Use **ESLint flat config** (e.g. `eslint.config.js`). Import the plugin and enable the rule:
```js
import styledCvaPlugin from "@styled-cva/eslint-plugin";
export default [
{
plugins: {
"@styled-cva": styledCvaPlugin,
},
rules: {
"@styled-cva/prefer-styled-cva": ["warn", { threshold: 5 }],
},
},
];
```
### Options [#options]
| Option | Type | Default | Description |
| ----------- | ------ | ------- | ------------------------------------------------------ |
| `threshold` | number | `5` | Minimum number of classes before the rule is reported. |
Example: only suggest extraction when there are more than 8 classes:
```js
"@styled-cva/prefer-styled-cva": ["warn", { threshold: 8 }]
```
## When to use it [#when-to-use-it]
- Keep components readable by moving long `className` strings into named styled components.
- Encourage consistent use of `@styled-cva/react` (or styled-cva) in the codebase.
- Reduce duplication when the same set of classes is used in multiple places (extract once, reuse the component).
# Usage Examples (/docs/examples)
Common patterns and real-world examples using `styled-cva`.
## Intrinsic CVA shorthand [#intrinsic-cva-shorthand]
Preferred over **`tw.button.cva(base, config)`** (deprecated on intrinsics — same behavior):
```tsx
const Button = tw.button(
"inline-flex items-center justify-center rounded-md font-medium",
{
variants: {
$variant: {
primary: "bg-primary text-primary-foreground",
secondary: "bg-secondary text-secondary-foreground",
},
},
defaultVariants: { $variant: "primary" },
},
);
```
## Buttons [#buttons]
### Basic Button with Variants [#basic-button-with-variants]
A standard button component with `intent` and `size` variants.
```tsx
import tw from "@styled-cva/react";
const Button = tw.button(
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
{
variants: {
$variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
},
$size: {
default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
icon: "h-10 w-10",
},
},
defaultVariants: {
$variant: "default",
$size: "default",
},
},
);
export default function ButtonExample() {
return (
);
}
```
## Form Elements [#form-elements]
### Input with Validation States [#input-with-validation-states]
An input component that handles error, success, and disabled states.
```tsx
import tw from "@styled-cva/react";
const Input = tw.input(
"flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
$state: {
default: "border-input focus-visible:ring-ring",
error:
"border-red-500 focus-visible:ring-red-500 text-red-900 placeholder:text-red-300",
success: "border-green-500 focus-visible:ring-green-500 text-green-900",
},
},
defaultVariants: {
$state: "default",
},
},
);
export default function InputExample() {
return (
);
}
```
# Getting Started (/docs/getting-started)
This guide will help you get started with styled-cva.
### Installation [#installation]
Install styled-cva using your preferred package manager:
```bash
npm i --save @styled-cva/react
```
```bash
pnpm add @styled-cva/react
```
```bash
bun add @styled-cva/react
```
For other framework adapters:
- **Solid**: `@styled-cva/solid`
- **Vue**: `@styled-cva/vue`
- **ReScript**: `@styled-cva/rescript` (see [ReScript Guide](/docs/rescript))
### Basic Usage [#basic-usage]
Create a styled component using template literals:
```tsx
import tw from "@styled-cva/react";
const StaticButton = tw.button`
bg-primary rounded-xl cursor-pointer
`;
// Use it like a regular React component
Click Me;
```
### Variants [#variants]
Use **`tw.button(base, config)`** (intrinsic CVA shorthand) for variant styles:
```tsx
import tw from "@styled-cva/react";
const VariantButton = tw.button("btn-base-class", {
variants: {
// Variant keys starting with $ will not be sent to the DOM,
// this avoids extraneous props warning
$variant: {
primary: "btn-primary-class",
secondary: "btn-secondary-class",
},
},
});
// $variant is inferred to 'primary' | 'secondary'
Click Me;
// The actual element in the DOM will be:
// Click Me
```
**`tw.button.cva(base, config)` is deprecated** — same behavior as the shorthand above. Prefer calling the factory directly.
Variant keys starting with `$` are transient props that won't be passed to the
DOM element. This prevents React warnings about unknown props.
### Default Props [#default-props]
Use `.withProps()` to set default props:
```tsx
const StyledButton = tw.button("btn-base", {
variants: {
$variant: {
primary: "btn-primary",
secondary: "btn-secondary",
},
},
})
.withProps({
"data-testid": "my-button",
type: "button",
$variant: "primary", // Valid variant value
});
// The component now has default props applied
Click Me;
// User props override defaults
Submit
;
```
### Polymorphic Components [#polymorphic-components]
Use the `$as` prop to render as different elements or components:
```tsx
import Link from "next/link";
const Button = tw.button("btn", {
variants: {
$variant: {
primary: "btn-primary",
secondary: "btn-secondary",
},
},
});
// Works with known JSX elements
I'm a link that looks like a button
;
// Also with custom components
I'm a link that looks like a button
;
```
### Styling Custom Components [#styling-custom-components]
Create a styled component from a custom component that accepts a `className` prop:
```tsx
import tw from "@styled-cva/react";
const MyButton = ({ className }: { className: string }) => {
return Hello;
};
const StyledButton = tw(MyButton)`text-red-500`;
// Use it like a regular component
;
```
## VSCode IntelliSense [#vscode-intellisense]
Install the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension and add this to your workspace `.vscode/settings.json`:
```json
{
"tailwindCSS.emmetCompletions": true,
"tailwindCSS.includeLanguages": {
"typescript": "javascript",
"typescriptreact": "javascript"
},
"tailwindCSS.experimental.classRegex": [
"tw`([^`]*)",
"tw\\.\\w+`([^`]*)`",
"tw\\([^)]*\\)`([^`]*)`",
["tw\\.\\w+\\s*\\(([^;]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"],
["tw\\.\\w+\\.cva\\s*\\(([^;]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"],
["tw\\([^)]+\\)\\s*\\(([^;]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"],
["cva\\(([^;]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"],
["cn\\(([^;]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"]
],
"editor.quickSuggestions": {
"strings": true
}
}
```
Tuple patterns (the `["…", "…"]` entries) need a **capturing group** around the
full call arguments — `([^;]*)` — so IntelliSense sees the base string and every
variant value inside `tw.span("base", { variants })`. Matching only `tw.span(`
will not work. Reload the window after editing settings.
## Preact [#preact]
`@styled-cva/react` works under [Preact](https://preactjs.com/) via the standard `preact/compat` alias. Preact provides drop-in replacements for `forwardRef` and the React type stack that styled-cva relies on, so no styled-cva code changes are required — set up the alias in your bundler and use the package exactly as you would in React.
### Install [#install]
```bash
npm i --save preact @styled-cva/react
```
```bash
pnpm add preact @styled-cva/react
```
```bash
bun add preact @styled-cva/react
```
### Bundler alias [#bundler-alias]
```ts
// vite.config.ts
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";
export default defineConfig({
plugins: [preact()],
resolve: {
alias: {
react: "preact/compat",
"react-dom": "preact/compat",
"react/jsx-runtime": "preact/jsx-runtime",
},
},
});
```
`@preact/preset-vite` wires the alias for you; the explicit `resolve.alias` block above is shown for projects that don't use the preset.
```js
// webpack.config.js
module.exports = {
resolve: {
alias: {
react: "preact/compat",
"react-dom/test-utils": "preact/test-utils",
"react-dom": "preact/compat",
"react/jsx-runtime": "preact/jsx-runtime",
},
},
};
```
```js
// next.config.js
module.exports = {
webpack: (config) => {
Object.assign(config.resolve.alias, {
react: "preact/compat",
"react-dom": "preact/compat",
"react/jsx-runtime": "preact/jsx-runtime",
});
return config;
},
};
```
### Usage [#usage]
No changes from the React workflow:
```tsx
import tw from "@styled-cva/react";
const Button = tw.button("rounded bg-blue-500 px-4 py-2 text-white", {
variants: {
$variant: {
primary: "bg-blue-500",
ghost: "bg-transparent text-blue-500",
},
},
});
Click;
```
The lint/format plugins (`@styled-cva/eslint-plugin`, `@styled-cva/prettier-plugin`, `@styled-cva/biome-plugin`) key off the `tw` identifier in source code, not the runtime. They work identically under Preact.
### Caveats [#caveats]
- **No dedicated `@styled-cva/preact` package.** The alias is the supported path. If you need to avoid bundler configuration, file an issue with the use case.
- **`forwardRef` deprecation in React 19** does not affect Preact users — `preact/compat` still exports a working `forwardRef`.
- **JSX namespace** must resolve to React types under the alias. `preact/compat` re-exports them, so polymorphic `$as` props and intrinsic element typings keep working.
# Introduction (/docs)
Welcome to **styled-cva** documentation.
A typesafe, [class-variance-authority-based](https://github.com/joe-bell/cva), styled-components-like library for authoring UI components with Tailwind CSS.
styled-cva only supports React 19.x (or Solid, Vue, ReScript via their respective adapters). If you're on React 18, use the legacy 0.3.x releases.
## Quick Example [#quick-example]
```tsx
import tw from "@styled-cva/react";
// Basic styled component
const Button = tw.button`bg-blue-500 text-white px-4 py-2 rounded`;
// With variants (prefer shorthand — `.cva()` on intrinsics is deprecated)
const VariantButton = tw.button("px-4 py-2 rounded", {
variants: {
$variant: {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-500 text-white",
},
},
});
// Usage
Click mePrimary
```
For detailed installation instructions and comprehensive examples, check out
the [Getting Started guide](/docs/getting-started).
## Features [#features]
} title="Type-safe" href="/docs/api" description="Full TypeScript inference across variants, default variants, and compound variants." />
} title="CVA-based" href="/docs/api" description="Built directly on top of class-variance-authority with zero friction." />
} title="Styled Syntax" href="/docs/getting-started" description="Familiar styled-components template literal and factory API." />
} title="Tailwind CSS" href="/docs/getting-started" description="Designed ground-up to complement Tailwind CSS utility classes." />
} title="Intrinsic CVA Shorthand" href="/docs/getting-started" description="tw.button(base, config) shorthand on all 72+ HTML intrinsics." />
} title="Polymorphic Components" href="/docs/getting-started" description="Full $as prop support preserving variant typing on custom elements." />
## Next Steps [#next-steps]
Ready to get started? Check out the [Getting Started guide](/docs/getting-started)
to learn the basics, or dive into the [API Reference](/docs/api) for complete
documentation.
# MCP Docs Server (/docs/mcp)
This documentation site functions as an HTTP MCP (Model Context Protocol)
server, making styled-cva documentation accessible to AI assistants and other
MCP clients.
## Endpoint [#endpoint]
**Production URL**: `https://styled-cva.vercel.app/api/mcp`
## Integration [#integration]
Add to your Cursor MCP configuration:
```json
{
"mcpServers": {
"styled-cva-docs": {
"url": "https://styled-cva.vercel.app/api/mcp"
}
}
}
```
Add to your Claude Desktop MCP configuration file (typically `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
```json
{
"mcpServers": {
"styled-cva-docs": {
"url": "https://styled-cva.vercel.app/api/mcp"
}
}
}
```
Configure in Windsurf's MCP settings:
```
Server URL: https://styled-cva.vercel.app/api/mcp
```
Add to your Gemini MCP configuration:
```json
{
"mcpServers": {
"styled-cva-docs": {
"url": "https://styled-cva.vercel.app/api/mcp"
}
}
}
```
## Available Resources [#available-resources]
The server exposes all documentation pages as MCP resources:
- **[Introduction](/docs)** - `styled-cva://docs/introduction`
- **[Getting Started](/docs/getting-started)** - `styled-cva://docs/getting-started`
- **[API Reference](/docs/api)** - `styled-cva://docs/api`
- **[MCP Docs Server](/docs/mcp)** - `styled-cva://docs/mcp`
## Available Tools [#available-tools]
- **`list_docs`** - List all available documentation pages
- **`search_docs`** - Search documentation by keyword (requires `query` parameter)
## `llms.txt` [#llmstxt]
In addition to the MCP server, we also provide a standard [`llms.txt`](/llms.txt) and [`llms-full.txt`](/llms-full.txt). This is a markdown document that provides a concise summary of the project and its documentation, specifically designed for Large Language Models to consume.
Once configured, your AI assistant can query styled-cva documentation directly
through the MCP protocol.
# Prettier Plugin (/docs/prettier-plugin)
The `@styled-cva/prettier-plugin` package normalizes class-string whitespace inside `tw.tag` / `tw(Component)` tagged templates and inside `tw.tag.cva({ base, variants })` calls, and rewrites long inline `tw\`…\`` templates into multi-line form when they overflow `printWidth`.
Part of the styled-cva monorepo. Published to npm as `@styled-cva/prettier-plugin`.
## What it does [#what-it-does]
1. **Normalizes whitespace.** Collapses runs of whitespace inside recognized class strings to a single space and trims leading/trailing whitespace.
2. **Rewrites long inline tagged templates to multi-line.** When `tw.tag\`…\`` would overflow `printWidth`, the plugin breaks the class list across multiple indented lines.
For Tailwind-aware class ordering, pair this plugin with [`prettier-plugin-tailwindcss`](https://github.com/tailwindlabs/prettier-plugin-tailwindcss). Load it **after** `@styled-cva/prettier-plugin` in your Prettier config.
### Targets [#targets]
- `` tw.div`…` `` / `` tw.button`…` `` — any `tw.` tagged template
- `` tw(Component)`…` `` — `tw(Foo)` wrapper tagged template
- `tw.div.cva({ base: "…", variants: { … } })` — `base` string + each variant option string
`className="…"` attributes are intentionally **not** handled — that is `prettier-plugin-tailwindcss`'s job.
### Example [#example]
**Before:**
```ts
const Button = tw.button` rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-600 `;
const Card = tw.div.cva({
base: " rounded-lg bg-white shadow ",
variants: {
$tone: {
info: " bg-blue-50 text-blue-900 ",
danger: "bg-red-50 text-red-900",
},
},
});
```
**After (`printWidth: 80`):**
```ts
const Button = tw.button`rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-600`;
const Card = tw.div.cva({
base: "rounded-lg bg-white shadow",
variants: {
$tone: {
info: "bg-blue-50 text-blue-900",
danger: "bg-red-50 text-red-900",
},
},
});
```
**Multi-line rewrite** (when the single-line render exceeds `printWidth`):
```ts
const HeroCard = tw.div`
flex items-center justify-between gap-4 rounded-xl
border border-slate-200 bg-white p-6 shadow-md
hover:border-slate-300 hover:shadow-lg
`;
```
Inner lines are indented by `tabWidth` (or one tab when `useTabs` is true) relative to the column where the tag starts; the closing backtick aligns back to that column.
## Installation [#installation]
```bash
bun add -D @styled-cva/prettier-plugin
```
**Peer dependency:** `prettier` (^3). Optional companion: `prettier-plugin-tailwindcss`.
## Configuration [#configuration]
In `.prettierrc` / `prettier.config.js`:
```json
{
"plugins": [
"@styled-cva/prettier-plugin",
"prettier-plugin-tailwindcss"
]
}
```
Order matters: load `@styled-cva/prettier-plugin` **before** `prettier-plugin-tailwindcss` so class sorting runs on the normalized, multi-lined output.
This plugin reuses standard Prettier options (`printWidth`, `tabWidth`, `useTabs`). It introduces no new options.
## Composition with the Biome plugin [#composition-with-the-biome-plugin]
`@styled-cva/biome-plugin` reports diagnostics for the same constructs but cannot rewrite code (Biome GritQL plugins are diagnostics-only). The intended workflow is to run Prettier on save for auto-fix and use the Biome plugin in CI / `biome check` for verification.
## Caveats [#caveats]
- **Static templates only.** Tagged templates containing `${…}` interpolations are skipped.
- **`cva` string literals stay strings.** They are whitespace-normalized but not converted to multi-line template literals.
- **Recognition is conservative.** The `tw` identifier must be the root of the tag or `.cva()` member chain — renamed/aliased imports are not detected.
# ReScript Bindings (/docs/rescript)
`@styled-cva/rescript` provides zero-cost, type-safe [ReScript v12](https://rescript-lang.org) bindings for `@styled-cva/react`.
It includes code-generated element factories for all 72+ HTML elements (`Tw.div`, `Tw.button`, `Tw.a`, `Tw.span`, etc.), `cva()` variant generator, and `cn()` class merge utility.
## Installation [#installation]
```bash
bun add @styled-cva/rescript @styled-cva/react
```
```bash
npm i --save @styled-cva/rescript @styled-cva/react
```
```bash
pnpm add @styled-cva/rescript @styled-cva/react
```
Add `@styled-cva/rescript` to your `rescript.json` (or `bsconfig.json`):
```json
{
"dependencies": [
"@rescript/react",
"@styled-cva/rescript"
]
}
```
## Basic Usage [#basic-usage]
Open `StyledCva` to access element components and utilities:
```ocaml
open StyledCva
/* Basic Styled Element */
let card = Tw.div("bg-white p-4 rounded-xl shadow-md")
@react.component
let make = () => {
React.createElement(
card,
{
children:
{React.string("Hello ReScript!")}
,
},
)
}
```
## Variants with `cva` [#variants-with-cva]
Use `cva` to define multi-axis variants:
```ocaml
open StyledCva
type buttonVariantProps = {
"$state": string,
}
let buttonVariants: buttonVariantProps => string = cva(
"px-4 py-2 rounded-md font-bold transition-all active:scale-95",
{
"variants": {
"$state": {
"active": "bg-orange-500 text-white animate-pulse",
"idle": "bg-stone-600 text-stone-100 hover:bg-stone-700",
},
},
"defaultVariants": {
"$state": "idle",
},
},
)
@react.component
let make = (~isActive: bool) => {
{React.string("Click Me")}
}
```
## Class Name Merging with `cn` [#class-name-merging-with-cn]
`cn` uses the `cn` engine to combine arrays of class names and resolve conflicts:
```ocaml
open StyledCva
let baseClass = cn(["text-red-500", "p-4", "text-blue-500"])
/* Resolves to: "p-4 text-blue-500" */
```
# Testing (/docs/testing)
`styled-cva` components are standard React components that render HTML elements with specific class names. This makes them easy to test using standard testing tools like [React Testing Library](https://testing-library.com/docs/react-testing-library/intro), [Vitest](https://vitest.dev/), or [Bun Test](https://bun.sh/docs/test/test-runner).
The primary goal when testing `styled-cva` components is to verify that:
1. The component renders the correct HTML element.
2. The correct classes are applied based on the props (variants).
## Setup [#setup]
We recommend using **React Testing Library** for rendering components and asserting on the DOM.
If you are using **Vitest** or **Jest**, you should also install [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) to get useful matchers like `toHaveClass`.
```bash
# Install testing dependencies
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
# or
bun add -d vitest @testing-library/react @testing-library/jest-dom jsdom
```
If you are using **Bun Test**, you can use it directly, though `jest-dom` matchers might require additional configuration or you can check attributes directly.
## Basic Component Testing [#basic-component-testing]
Here is a simple example of testing a styled button to ensure it renders with the base classes.
```tsx
// Button.test.tsx
import { render, screen } from "@testing-library/react";
import tw from "@styled-cva/react";
import { expect, test } from "vitest";
export const Button = tw.button`px-4 py-2 bg-blue-500 text-white rounded`;
test("renders with default styles", () => {
render(Click me);
const button = screen.getByRole("button", { name: /click me/i });
expect(button).toBeInTheDocument();
expect(button).toHaveClass(
"px-4",
"py-2",
"bg-blue-500",
"text-white",
"rounded",
);
});
```
## Testing Variants [#testing-variants]
When using variant styles, you want to ensure that passing different variant props results in the correct class names being applied.
```tsx
// Alert.test.tsx
import { render, screen } from "@testing-library/react";
import tw from "@styled-cva/react";
import { expect, test } from "vitest";
export const Alert = tw.div(
"p-4 rounded border",
{
variants: {
$intent: {
info: "bg-blue-100 border-blue-500 text-blue-700",
success: "bg-green-100 border-green-500 text-green-700",
danger: "bg-red-100 border-red-500 text-red-700",
},
},
defaultVariants: {
$intent: "info",
},
},
);
test("renders info variant by default", () => {
render(Info message);
const alert = screen.getByText("Info message");
expect(alert).toHaveClass("bg-blue-100", "border-blue-500");
});
test("renders success variant", () => {
render(Success message);
const alert = screen.getByText("Success message");
expect(alert).toHaveClass("bg-green-100", "border-green-500");
});
```
## Snapshot Testing [#snapshot-testing]
Snapshot testing is particularly useful for styled components as it captures the entire structure and class list, allowing you to catch unintended style changes.
```tsx
// Card.test.tsx
import { render } from "@testing-library/react";
import { expect, test } from "vitest";
test("renders correctly", () => {
const { container } = render(Content);
expect(container.firstChild).toMatchSnapshot();
});
```
## Polymorphic Components [#polymorphic-components]
If you use the `$as` prop to change the rendered element, you can verify the tag name in your tests.
```tsx
test("renders as a link when $as prop is provided", () => {
render(
Login
,
);
const link = screen.getByRole("link", { name: /login/i });
expect(link).toBeInTheDocument();
expect(link.tagName).toBe("A");
expect(link).toHaveAttribute("href", "/login");
});
```