cva-best-practices
joe-bell/cva/skills/cva-best-practices/SKILL.md
Build and review typed component variants with cva. Use when defining cva components, exposing variant props, composing styles, handling class conflicts, or generating variant galleries. For version upgrades, use migration guidance instead.
Skill6.9k starsChanged 20 days ago
What's in it
- cva best practices
- Check the installed package
- Define variants once
- Keep props inferred
- Join classes without another dependency
- Compose classes and preserve semantics
- Generate galleries from the schema
---
name: cva-best-practices
description: Build and review typed component variants with cva. Use when defining cva components, exposing variant props, composing styles, handling class conflicts, or generating variant galleries. For version upgrades, use migration guidance instead.
metadata:
source: hand-maintained for the joe-bell/cva repository
---
# cva best practices
Use the project's styling approach and component conventions. Apply the guidance below to the requested component rather than refactoring unrelated code. The [beta docs](https://cva.style/beta/) are the source of truth for these recommendations.
## Check the installed package
Read the consuming package's manifest, lockfile, and existing imports before choosing an API. `cva@1.0.0-beta.x` and `class-variance-authority@0.x` are different packages. Beta releases can change without semver guarantees; verify the installed exports and types when an API below is unavailable. Do not upgrade dependencies as part of ordinary component work.
This guidance targets the `cva` 1.0 betas (verified against `cva@1.0.0-beta.12`): the single-object `cva` call, `composes`, `defineConfig` from `cva/config`, and `getSchema` from `cva/tools`. Older betas may have different entry points or APIs. For stable `class-variance-authority`, follow the [stable docs](https://cva.style/): import from `class-variance-authority` and use `cva(base, options)`. Do not apply beta-only configuration, composition, schema, or Tailwind CSS exports to stable projects. When an upgrade is requested, consult the release-specific migration guidance instead.
## Define variants once
- Define a class function outside the component render body. Put invariant classes in `base`, independent choices in `variants`, and combinations in `compoundVariants`.
- Prefer framework prop defaults in React, Svelte, or Vue wrappers so the same value reaches styling and markup. `defaultVariants` only supplies defaults inside the class function; it cannot set an element's attributes. Use it when the class function should supply defaults to its callers. Omitted props and `undefined` use those defaults. Avoid duplicating defaults in both places, and remember `getSchema` cannot read framework prop defaults.
- To select no classes for a variant, declare a named option such as `unset: null`, then pass `"unset"`. Do not assume passing `null` disables a beta variant.
- Treat the configuration and referenced objects as immutable after creating a class function. Create another function if the configuration changes.
- Prefer server-side rendering or static generation for static components when the framework permits it. Do not add client-side JavaScript solely to generate a static class string.
See [Variants](https://cva.style/beta/getting-started/variants/), [Default variants](https://cva.style/beta/getting-started/variants/#default-variants), and the [`cva` API reference](https://cva.style/beta/api-reference/#cva).
## Keep props inferred
Use `VariantProps<typeof button>` rather than repeating variant unions. It contains public variant props, not `class` or `className`, and omits variant names prefixed with `_`. Internal variants still work in defaults, compound variants, and direct calls; they are not a runtime access-control mechanism.
For a React wrapper, combine variant types with native element props and forward `className` to the class function. If names overlap incompatibly, omit the overlapping native props before combining them. Forward semantic props such as `disabled` to the actual HTML element as well as the class function. Use TypeScript's `Required<Pick<...>>` and `Omit` when a public variant must be required.
See [Extracting variant types](https://cva.style/beta/getting-started/typescript/#extracting-variant-types) and the [React gallery](https://cva.style/beta/getting-started/tools/#generate-a-react-variant-gallery).
## Join classes without another dependency
The preset exports `cx`, backed by `clsx`, for strings, nested arrays, and conditional objects. Use it instead of adding `clsx` or `classnames` for ordinary conditional class joining. `cx` does not deduplicate classes or resolve CSS conflicts.
For Tailwind CSS, write complete utility class strings in variant definitions. Do not interpolate fragments such as `bg-${tone}-500`: Tailwind scans source text and cannot infer the resulting names. Map variant values to static class strings instead, as shown in [Installation](https://cva.style/beta/getting-started/installation/#tailwind-css).
Pass extra classes through the class function's `class` or `className` prop. Appending a utility does not guarantee a CSS override. For Tailwind CSS, follow the project's existing conflict strategy:
- **`cn`**: `import { cn as merge } from "cn"`, then `defineConfig({ cx: merge })`.
- **`cva/tailwindcss`**: import after Tailwind CSS and prefix overridable component defaults with `base:`, including defaults selected by variants or compound variants. Ordinary utilities override them through the cascade. Keep hover and disabled state styles ordinary; any ordinary utility also beats conditional `base:` defaults. Put `base:` before pseudo-element variants, and remember important declarations reverse layer priority.
- **`tailwind-merge`**: combine its `twMerge` with the preset `cx` to preserve conditional objects. Bare `twMerge` has a narrower input grammar.
For the last option:
```ts
import { defineConfig } from "cva/config";
import { cx as joinClasses } from "cva";
import { twMerge } from "tailwind-merge";
export const { cva, cx: cn } = defineConfig({
cx: (...inputs) => twMerge(joinClasses(...inputs)),
});
```
Import the configured functions throughout that project. Do not accidentally use the preset for components expected to merge conflicts. A custom concatenator owns the input grammar and must support empty calls, variadic inputs, and composed strings.
See [Handling class conflicts](https://cva.style/beta/getting-started/installation/#handling-class-conflicts) and [Extending Components](https://cva.style/beta/getting-started/extending-components/).
## Compose classes and preserve semantics
Use `composes` for reusable `cva` class functions. Pass one function or an inline array; use `as const` for a stored array to retain tuple inference. Overlapping variant values add each component's matching classes. Defaults merge with the last composed default winning, then local defaults take precedence across the composition.
A `cva` class function produces a string, so apply it to the appropriate HTML element. For a React render-prop API, the docs recommend Base UI's `useRender`; do not invent a `cva` styled API. For compound component styling, consider the CSS cascade, custom properties, and selectors such as `:has()` before introducing JavaScript state solely to share styles.
`cva` does not interpret responsive variant objects. Express responsive styles in your CSS, define a named variant with breakpoint utilities, or show/hide variants at breakpoints as appropriate for the existing UI.
See [Composing Components](https://cva.style/beta/getting-started/composing-components/), [Compound Components](https://cva.style/beta/getting-started/compound-components/), [Polymorphism](https://cva.style/beta/getting-started/polymorphism/), and [FAQs](https://cva.style/beta/faqs/).
## Generate galleries from the schema
Call `getSchema` on the `cva` class function, not its React or other framework wrapper. Read the schema outside rendering and iterate typed `values` to generate galleries, documentation, or controls without duplicate lists. Boolean and numeric values retain their types; internal and empty variants are omitted. `defaultValue` is present only when that variant has a declared default.
Keep an omitted-prop case when demonstrating defaults. Derive layout counts from the displayed schema arrays rather than hard-coding them. Put schema consumers in stories or documentation when the application does not need them at runtime.
See [Tools](https://cva.style/beta/getting-started/tools/). Verify changed class outputs against the installed package and type-check consuming components using the project's checks.
More agent context in joe-bell/cva
18 other files this repository gives its agents.
Skill
- deslop.agents/skills/deslop/SKILL.md
- find-skills.agents/skills/find-skills/SKILL.md
- pnpm.agents/skills/pnpm/SKILL.md
- security-review.agents/skills/security-review/SKILL.md
- tailwind-css-v4.agents/skills/tailwind-css-v4/SKILL.md
- vitest.agents/skills/vitest/SKILL.md
- web-design-guidelines.agents/skills/web-design-guidelines/SKILL.md
- wrangler.agents/skills/wrangler/SKILL.md
- writing-guidelines.agents/skills/writing-guidelines/SKILL.md
- cva-migrateskills/cva-migrate/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

