CVA (Class Variance Authority)

repository·main·Indexed 24 days ago

https://github.com/joe-bell/cva

A library for managing CSS class variations in a structured and type-safe manner. It provides the `cva` function for building component variants with base classes, variant schemas, compound variants, and default values, as well as the `cx` function (an alias for `clsx`) for concatenating class names. Features include component composition via the `composes` property, schema extraction with `getSchema`, and custom configuration through `defineConfig`.

Tokens
11.7K
Snippets
34
Records
59
Agent score
90%

What's inside cva

  1. Overview of Class Variance Authority (cva)

    main
    cva is a utility for building type-safe, variant-driven class names. It is designed to work with any styling approach, including Tailwind CSS, standard CSS, or CSS Modules. It solves the problem of manually matching CSS classes to component props and managing types for UI variants, providing a pattern similar to CSS-in-TS libraries (like Stitches or Vanilla Extract) but without requiring a specific CSS-in-TS runtime.
  2. Build Compound Components with CVA

    main

    For complex UI elements, you can use cva to build sets of composable components (Compound Components) that work together. Instead of a single monolithic component, you create a parent component and several subcomponents (e.g., <Accordion.Root>, <Accordion.Item>, <Accordion.Header>).

    cva is designed to work effectively with this pattern by encouraging the use of CSS features like the cascade, custom properties, and :has() selectors to manage the relationships and styles between the parent and its subcomponents.

    import * as Accordion from "./Accordion";
    
    function Example() {
      return (
        <Accordion.Root>
          <Accordion.Item>
            <Accordion.Header>Section 1</Accordion.Header>
            <Accordion.Content>Content 1</Accordion.Content>
          </Accordion.Item>
        </Accordion.Root>
      );
    }
  3. Make specific variants required using TypeScript Utility Types

    main

    Since cva does not provide a built-in way to mark variants as required, you should use TypeScript utility types like Omit, Pick, and Required to enforce specific variant props in your component's interface.

    To make a variant required:

    1. Extract all variant props using VariantProps.
    2. Use Omit to remove the variant from the base props.
    3. Use Pick and Required to re-add the variant as a mandatory field.
    4. Extend your component's interface with these combined types.
    import { cva, type VariantProps } from "class-variance-authority";
    
    export const buttonVariants = cva("…", {
      variants: {
        optional: { a: "…", b: "…" },
        required: { a: "…", b: "…" },
      },
    });
    
    export type ButtonVariantProps = VariantProps<typeof buttonVariants>;
    
    /**
     * Button
     */
    export interface ButtonProps
      extends
        Omit<ButtonVariantProps, "required">,
        Required<Pick<ButtonVariantProps, "required">> {}
    
    export const button = (props: ButtonProps) => buttonVariants(props);
    
    // ✅ Correct usage:
    button({ required: "a" });
    
    // ❌ TypeScript Error:
    // Property "required" is missing in type "{}" but required in type "ButtonProps".
    button({});
  4. Compose multiple CVA components using the `composes` property

    main

    You can merge one or more cva components into a single component using the composes property. This allows you to build complex components by shallowly merging base styles and variants from existing cva definitions. You can pass a single component directly or an array of multiple components.

    Important: Pass components to composes as an inline array literal or one marked as const. Using a pre-declared, mutable array variable (e.g., const list = [a, b]) will cause the loss of tuple inference, which can lead to incorrect or missing variant types in the resulting component.

    import { cva, type VariantProps } from "cva";
    
    const box = cva({
      base: "box box-border",
      variants: {
        margin: { 0: "m-0", 2: "m-2", 4: "m-4", 8: "m-8" },
        padding: { 0: "p-0", 2: "p-2", 4: "p-4", 8: "p-8" },
      },
      defaultVariants: {
        margin: 0,
        padding: 0,
      },
    });
    
    const root = cva({
      base: "card rounded border-solid border-slate-300",
      variants: {
        shadow: {
          md: "drop-shadow-md",
          lg: "drop-shadow-lg",
          xl: "drop-shadow-xl",
        },
      },
    });
    
    // Compose the components
    export const card = cva({ composes: [box, root] });
    export interface CardProps extends VariantProps<typeof card> {}
    
    // Usage
    card({ margin: 2, shadow: "md" });
    // => "box box-border m-2 p-0 card rounded border-solid border-slate-300 drop-shadow-md"
    
    card({ margin: 2, shadow: "md", class: "adhoc-class" });
    // => "box box-border m-2 p-0 card rounded border-solid border-slate-300 drop-shadow-md adhoc-class"
  5. Compose CVA components using cx

    main

    Since cva does not have a built-in method for composing components, you can extend and concatenate multiple cva instances using the cx utility.

    To do this:

    1. Define your base components using cva.
    2. Extract their variant types using VariantProps.
    3. Create a new component function that accepts the combined props and returns the result of cx() applied to all base components.

    Note: If you are using cva@beta, first-class composition is available via the composes property.

    import type { VariantProps } from "class-variance-authority";
    import { cva, cx } from "class-variance-authority";
    
    /**
     * Base component 1
     */
    export type BoxProps = VariantProps<typeof box>;
    export const box = cva(["box", "box-border"], {
      variants: {
        margin: { 0: "m-0", 2: "m-2", 4: "m-4", 8: "m-8" },
        padding: { 0: "p-0", 2: "p-2", 4: "p-4", 8: "p-8" },
      },
      defaultVariants: {
        margin: 0,
        padding: 0,
      },
    });
    
    /**
     * Base component 2
     */
    type CardBaseProps = VariantProps<typeof cardBase>;
    const cardBase = cva(["card", "border-solid", "border-slate-300", "rounded"], {
      variants: {
        shadow: {
          md: "drop-shadow-md",
          lg: "drop-shadow-lg",
          xl: "drop-shadow-xl",
        },
      },
    });
    
    /**
     * Composed component
     */
    export interface CardProps extends BoxProps, CardBaseProps {}
    export const card = ({ margin, padding, shadow }: CardProps = {}) =>
      cx(box({ margin, padding }), cardBase({ shadow }));
  6. Install class-variance-authority

    main

    Install the class-variance-authority package using your preferred package manager. Note that while the package is currently named class-variance-authority, it is intended to be renamed to cva in version 1.0.0.

    pnpm i class-variance-authority
    # or
    npm i class-variance-authority
    # or
    yarn add class-variance-authority
    # or
    bun add class-variance-authority
    # or
    deno add class-variance-authority
  7. Use internal variants with underscore prefix

    main

    Prefixing a variant name with an underscore (e.g., _intent) marks it as an internal variant. Internal variants are available for use in the cva function and can be used in defaultVariants or compoundVariants, but they are automatically omitted from the types generated by VariantProps and the schema from getSchema. This is useful for managing component state internally without exposing those options to the public API.

    import { cva, type VariantProps } from "cva";
    
    const button = cva({
      base: "button",
      variants: {
        _intent: { primary: "button--primary", secondary: "button--secondary" },
        size: { small: "button--small", medium: "button--medium" },
      },
      defaultVariants: {
        _intent: "primary",
        size: "medium",
      },
    });
    
    // _intent is omitted from ButtonProps
    interface Props
      extends
        React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof button> {
      active?: boolean;
    }
    
    function Button({ active, size, className, ...props }: Props) {
      return (
        <button
          className={button({
            size,
            _intent: active ? "primary" : "secondary",
            class: className,
          })}
          {...props}
        />
      );
    }
  8. Configure Cloudflare Workers Builds watch paths

    main

    When deploying via Cloudflare Workers Builds, configure build watch paths in the Cloudflare dashboard (Settings → Build → Build watch paths). The following paths are included in the build scope (excludes are empty):

    • docs/* (covers nested files)
    • packages/cva/*
    • .config/*
    • package.json, tsconfig.json, pnpm-lock.yaml, pnpm-workspace.yaml, .prettierrc.json
    • .nvmrc (Node version)

    Note: Root .md files and other extensionless files (e.g., LICENSE, .gitignore) are excluded to prevent redeploying on prose-only changes.