# 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) => ( ; ``` 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 (
); } ``` ## Interactive Components [#interactive-components] ### Toggle Switch [#toggle-switch] A switch component using `aria-pressed` or a custom prop for state. ```tsx import { useState } from "react"; import tw from "@styled-cva/react"; const SwitchRoot = tw.button( "peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50", { variants: { $checked: { true: "bg-primary", false: "bg-input", }, }, }, ); const SwitchThumb = tw.span( "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform", { variants: { $checked: { true: "translate-x-5", false: "translate-x-0", }, }, }, ); export default function SwitchExample() { const [checked, setChecked] = useState(false); return ( setChecked(!checked)} role="switch" aria-checked={checked} > ); } ``` ## Layouts [#layouts] ### Card [#card] A composed card component. ```tsx import tw from "@styled-cva/react"; const Card = tw.div`rounded-lg border bg-card text-card-foreground shadow-sm`; const CardHeader = tw.div`flex flex-col space-y-1.5 p-6`; const CardTitle = tw.h3`text-2xl font-semibold leading-none tracking-tight`; const CardDescription = tw.p`text-sm text-muted-foreground`; const CardContent = tw.div`p-6 pt-0`; const CardFooter = tw.div`flex items-center p-6 pt-0`; export default function CardExample() { return ( Create project Deploy your new project in one-click.

Project details form goes here...

); } ``` ## Polymorphic Components [#polymorphic-components] Using the `$as` prop to render a button as a link. ```tsx import Link from "next/link"; import tw from "@styled-cva/react"; const Button = tw.button( "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2", ); export default function PolymorphicExample() { return (
{/* Renders as {/* Renders as */} {/* Renders as Next.js Link */}
); } ``` ### Type-safe wrapper for custom components [#type-safe-wrapper-for-custom-components] When you want explicit typing for a custom-component `$as` (e.g. so consumers see autocomplete for both the link's props *and* your CVA variants), use `PolymorphicComponentProps` to compose the prop type: ```tsx import Link, { type LinkProps } from "next/link"; import tw, { type PolymorphicComponentProps } from "@styled-cva/react"; const Button = tw.button("btn", { variants: { $variant: { primary: "btn-primary", secondary: "btn-secondary" }, }, }); type ButtonLinkProps = PolymorphicComponentProps & LinkProps; export const ButtonLink = (props: ButtonLinkProps) => ( ``` **`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 ; // Also with custom components ; ``` ### 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 ; }; 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", }, }, }); ; ``` 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 Primary ``` 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) => { } ``` ## 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(); 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( , ); const link = screen.getByRole("link", { name: /login/i }); expect(link).toBeInTheDocument(); expect(link.tagName).toBe("A"); expect(link).toHaveAttribute("href", "/login"); }); ```