Tools
This content is for Beta. Switch to the latest version for up-to-date documentation.
The cva/tools entry point holds the helpers that read a component’s variants back out at runtime. It’s a separate entry point, so an ESM bundler drops it from a build that never imports it.
getSchema
Section titled “getSchema”Re-declaring variants for a Storybook story or prop table creates a second copy to keep in sync by hand. getSchema reads them from the component instead, so you declare each variant once:
import { cva } from "cva";import { getSchema } from "cva/tools";
const button = cva({ base: "button", variants: { intent: { primary: "button--primary", secondary: "button--secondary" }, size: { small: "button--small", large: "button--large" }, }, defaultVariants: { intent: "primary", size: "small" },});
getSchema(button);// => {// intent: { values: ["primary", "secondary"], defaultValue: "primary" },// size: { values: ["small", "large"], defaultValue: "small" },// }The schema is fully typed: values narrows to the variant’s literal values, and defaultValue only appears when the component declares one. Hover the result in your editor and you see the same shape the runtime returns:
import { cva } from "cva";import { getSchema } from "cva/tools";
const badge = cva({ base: "badge", variants: { tone: { info: "badge--info", warning: "badge--warning" }, round: { true: "badge--round", false: "badge--square" }, weight: { 400: "badge--regular", 700: "badge--bold" }, }, defaultVariants: { tone: "info" },});
const schema = getSchema(badge);// ^ {// tone: {// values: readonly ("info" | "warning")[];// defaultValue: "info";// };// round: { values: readonly boolean[] };// weight: { values: readonly (400 | 700)[] };// }Boolean and numeric variant keys come back as booleans and numbers, matching the props the component accepts rather than the object keys they were written as. Internal variants are omitted.
For the full signature, see the API reference.
Generate a React variant gallery
Section titled “Generate a React variant gallery”The runnable React with Tailwind CSS example exports its button class function separately from its React wrapper. The wrapper supplies intent, size, and disabled defaults so styling and native attributes use the same values:
import React from "react";import { cva, type VariantProps } from "cva";
export const button = cva({ base: "font-semibold border rounded", variants: { intent: { primary: "bg-blue-500 text-white border-transparent", secondary: "bg-white text-gray-800 border-gray-400", }, size: { small: "py-1 px-2 text-sm", medium: "py-2 px-4 text-base", }, disabled: { false: null, true: "opacity-50 cursor-not-allowed", }, }, compoundVariants: [ { intent: "primary", disabled: false, class: "hover:bg-blue-600", }, { intent: "secondary", disabled: false, class: "hover:bg-gray-100", }, { intent: "primary", size: "medium", class: "uppercase" }, ],});
export interface ButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "disabled">, VariantProps<typeof button> {}
export const Button: React.FC<ButtonProps> = ({ className, intent = "primary", size = "medium", disabled = false, ...props}) => ( <button className={button({ intent, size, disabled, className })} disabled={disabled} {...props} />);Pass button, the cva class function, to getSchema. The React Button renders markup and is not a valid input. Read the schema outside the render function, then map its typed values into buttons:
import React from "react";import { cx } from "cva";import { getSchema } from "cva/tools";import { Button, button } from "./components";
const schema = getSchema(button);const intents = [undefined, ...schema.intent.values];const sizes = [undefined, ...schema.size.values];const isDisabled = schema.disabled.values;
export function ButtonGallery() { return ( <table className={cx( "relative h-max w-max self-center justify-self-center", "[&_:where(th,td)]:p-2", )} > <caption>Button variants</caption> <thead> <tr> <td></td> <td></td> {intents.map((intent) => ( <th key={intent || "default"} scope="col"> {intent || "default"} </th> ))} </tr> </thead> {isDisabled.map((disabled) => ( <tbody key={String(disabled)}> {sizes.map((size, index) => ( <tr key={`${disabled}-${size || "default"}`}> {index === 0 && ( <th scope="rowgroup" rowSpan={sizes.length}> {disabled ? "disabled" : "enabled"} </th> )} <th scope="row">{size || "default"}</th> {intents.map((intent) => ( <td key={intent || "default"}> <Button intent={intent} size={size} disabled={disabled}> {intent || "default"} button </Button> </td> ))} </tr> ))} </tbody> ))} </table> );}Adding an intent or size to button now adds it to the gallery without another list to update. The default cells leave those props unset, exercising the React wrapper’s prop defaults, while each row passes disabled explicitly. getSchema cannot discover wrapper defaults, so use defaultVariants when the class function itself should expose a defaultValue.
The React examples use this approach with default, size, intent, and disabled states.