W↓
All docs
🔑
Sign Up/Sign In
beta.cva.style/
Public Link
Apr 10, 2025, 4:15:46 AM - complete - 18.8 kB
Starting URLs:
https://beta.cva.style/
## Page: https://beta.cva.style/  CSS-in-TS libraries such as Stitches and Vanilla Extract are **fantastic** options for building type-safe UI components; taking away all the worries of class names and StyleSheet composition. …but CSS-in-TS (or CSS-in-JS) isn’t for everyone. You may need full control over your StyleSheet output. Your job might require you to use a framework such as Tailwind CSS. You might just prefer writing your own CSS. Creating variants with the “traditional” CSS approach can become an arduous task; manually matching classes to props and manually adding types. `cva` aims to take those pain points away, allowing you to focus on the more fun aspects of UI development. ## Acknowledgements * **Stitches** (WorkOS) Huge thanks to the WorkOS team for pioneering the `variants` API movement – your open-source contributions are immensely appreciated * **clb** (Bill Criswell) This project originally started out with the intention of merging into the wonderful `clb` library, but after some discussion with Bill, we felt it was best to go down the route of a separate project. I’m so grateful to Bill for sharing his work publicly and for getting me excited about building a type-safe variants API for classes. If you have a moment, please go and star the project on GitHub. Thank you Bill! * **clsx** (Luke Edwards) Previously, this project surfaced a custom `cx` utility for flattening classes, but it lacked the ability to handle variadic arguments or objects. clsx provided those extra features with quite literally zero increase to the bundle size – a no-brainer to switch! * **Vanilla Extract** (Seek) ## Downloads * Wallpaper ## License Apache-2.0 License © Joe Bell --- ## Page: https://beta.cva.style/getting-started/whats-new * * * What’s changed since `class-variance-authority@0.*`? ## Features ### 1\. `compose` Shallow merge any number of cva components into a single component via the new `compose` method. ### 2\. `defineConfig` Extend `cva` via the new `defineConfig` API. Want to use `cva`/`cx` with `tailwind-merge`? No problem! ## Enhancements One of the biggest – and let’s be honest, most important – complaints about `class-variance-authority` was that the name was just too damn long. Shout-out to GitHub for transferring `npm` ownership of `cva`! ### 2\. `cva` now accepts a single parameter Base styles are now applied via the named `base` property. import { cva } from "class-variance-authority"; import { cva } from "cva";const component = cva({ base: "your-base-class" }); ### 3\. Goodbye `null` Previously, passing `null` to a variant would disable a variant completely – to match the behaviour of Stitches.js – however this caused a great deal of confusion. Instead, we now recommend explicitly rolling your own `unset` variant. ### 4\. Clearer Type Guards `cva` uses generic type parameters to infer variant types, but this was often confused as something that could be customized. If you attempt to pass a generic type parameter, `cva` will now throw an error. --- ## Page: https://beta.cva.style/getting-started/installation * pnpm * npm * yarn * bun pnpm i cva@beta ## Tailwind CSS If you’re a Tailwind user, here are some additional (optional) steps to get the most out of `cva`: ### IntelliSense You can enable autocompletion inside `cva` using the steps below: * Visual Studio Code * Zed * Neovim * WebStorm 1. Install the “Tailwind CSS IntelliSense” Visual Studio Code extension 2. Add the following to your `.vscode/settings.json`: { "tailwindCSS.experimental.classRegex": [ ["cva\\(((?:[^()]|\\([^()]*\\))*)\\)", "[\"'`]?([^\"'`]+)[\"'`]?"], ["cx\\(((?:[^()]|\\([^()]*\\))*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] ]} ### Handling Style Conflicts If you want to merge Tailwind CSS classes without conflicts, you may wish to roll-your-own `cva` with the `tailwind-merge` package: Example with tailwind-merge import { defineConfig } from "cva";import { twMerge } from "tailwind-merge";export const { cva, cx, compose } = defineConfig({ hooks: { onComplete: (className) => twMerge(className), },}); import { cx, cva } from "../cva.config";export const button = cva({ // 1. `twMerge` strips out `bg-gray-200`… base: "font-semibold bg-gray-200 border rounded", variants: { intent: { // 2. …as variant `bg-*` values take precedence primary: "bg-blue-500 text-white border-transparent hover:bg-blue-600", secondary: "bg-white text-gray-800 border-gray-400 hover:bg-gray-100", }, } defaultVariants: { intent: "primary", },});button();// => "font-semibold border rounded bg-blue-500 text-white border-transparent hover:bg-blue-600 text-base py-2 px-4 uppercase"cx("bg-gray-200", "bg-blue-500");// => "bg-blue-500" --- ## Page: https://beta.cva.style/getting-started/variants ## Creating Variants To kick things off, let’s build a “basic” `button` component, using `cva` to handle our variant’s classes import { cva } from "cva";const button = cva({ base: "rounded border font-semibold", // **or** // base: ["font-semibold", "border", "rounded"], variants: { intent: { primary: "border-transparent bg-blue-500 text-white hover:bg-blue-600", // **or** // primary: [ // "bg-blue-500", // "text-white", // "border-transparent", // "hover:bg-blue-600", // ], secondary: "border-gray-400 bg-white text-gray-800 hover:bg-gray-100", }, size: { small: "px-2 py-1 text-sm", medium: "px-4 py-2 text-base", }, }, compoundVariants: [ { intent: "primary", size: "medium", class: "uppercase", // **or** if you're a React.js user, `className` may feel more consistent: // className: "uppercase" }, ], defaultVariants: { intent: "primary", size: "medium", },});button();// => "font-semibold border rounded bg-blue-500 text-white border-transparent hover:bg-blue-600 text-base py-2 px-4 uppercase"button({ intent: "secondary", size: "small" });// => "font-semibold border rounded bg-white text-gray-800 border-gray-400 hover:bg-gray-100 text-sm py-1 px-2" ## Compound Variants Variants that apply when multiple other variant conditions are met. import { cva } from "cva";const button = cva({ base: "…", variants: { intent: { primary: "…", secondary: "…" }, size: { small: "…", medium: "…" }, }, compoundVariants: [ // Applied via: // `button({ intent: "primary", size: "medium" })` { intent: "primary", size: "medium", class: "…", }, ],}); ### Targeting Multiple Variant Conditions import { cva } from "cva";const button = cva({ base: "…", variants: { intent: { primary: "…", secondary: "…" }, size: { small: "…", medium: "…" }, }, compoundVariants: [ // Applied via: // `button({ intent: "primary", size: "medium" })` // or // `button({ intent: "secondary", size: "medium" })` { intent: ["primary", "secondary"], size: "medium", class: "…", }, ],}); ## Disabling Variants Want to disable a variant completely? Provide an option with a value of `null`. If you’re stuck on naming, we recommend setting an explicit `"unset"` option (similar to the CSS keyword). import { cva } from "cva";const button = cva({ base: "button", variants: { intent: { unset: null, primary: "button--primary", secondary: "button--secondary", }, },});button({ intent: "unset" });// => "button" --- ## Page: https://beta.cva.style/getting-started/compound-components For larger, more complex components, you may end up wanting to create a set of composable components that work together: “Compound Components” e.g. `<Accordion.Subcomponent />` instead of `<Accordion />` 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> );} `cva` encourages you to build these compound components **with the power of CSS**; leverage the cascade, custom properties, `:has()` selectors, and more… ## Examples * React with Tailwind CSS (Compound Components) --- ## Page: https://beta.cva.style/getting-started/extending-components ## Extending Components All `cva` components provide an optional `class` **or** `className` prop, which can be used to pass additional classes to the component. import { cva } from "cva";const button = cva(/* … */);button({ class: "m-4" });// => "…buttonClasses m-4"button({ className: "m-4" });// => "…buttonClasses m-4" --- ## Page: https://beta.cva.style/getting-started/composing-components Any number of `cva` components can be shallow merged into a single component via the `compose` method: import { cva, compose } 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", }, },});export interface CardProps extends VariantProps<typeof card> {}export const card = compose(box, root);card({ margin: 2, shadow: "md" });// => "box box-border m-2 card border-solid border-slate-300 rounded drop-shadow-md"card({ margin: 2, shadow: "md", class: "adhoc-class" });// => "box box-border m-2 card border-solid border-slate-300 rounded drop-shadow-md adhoc-class" --- ## Page: https://beta.cva.style/getting-started/polymorphism `cva` components are polymorphic (and framework-agnostic) by default; just apply the class to your preferred HTML element… import { button } from "./components/button";export default () => ( <a className={button()} href="/sign-up"> Sign up </a>); ## Alternative Approaches ### React If you’d prefer to use a React-based API, `cva` strongly recommends using `@radix-ui`’s `Slot` component to create your own `asChild` prop. import * as React from "react";import { Slot } from "@radix-ui/react-slot";import { cva, type VariantProps } from "cva";const button = cva({ base: "button", variants: { intent: { primary: "bg-blue-500 text-white border-transparent hover:bg-blue-600", secondary: "bg-white text-gray-800 border-gray-400 hover:bg-gray-100", }, }, defaultVariants: { intent: "primary", },});export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof button> { asChild?: boolean;}export const Button: React.FC<ButtonProps> = ({ asChild, className, intent, ...props}) => { const Comp = asChild ? Slot : "button"; return <Comp className={button({ intent, className })} {...props} />;}; #### Usage import { Button } from "./components/button";// Renders:// <a href="/sign-up" class="bg-blue-500 text-white border-transparent hover:bg-blue-600">// Sign up// </a>export default () => ( <Button asChild> <a href="/sign-up">Sign up</a> </Button>); --- ## Page: https://beta.cva.style/getting-started/typescript `cva` offers the `VariantProps` helper to extract variant types import type { VariantProps } from "cva";import { cva, cx } from "cva";/** * Button */export type ButtonProps = VariantProps<typeof button>;export const button = cva(/* … */); ## Required Variants To keep the API small and unopinionated, `cva` **doesn’t** offer a built-in solution for setting required variants. Instead, we recommend using TypeScript’s Utility Types: import { cva, type VariantProps } from "cva";export type ButtonVariantProps = VariantProps<typeof buttonVariants>;export const buttonVariants = cva({ base: "…", variants: { optional: { a: "…", b: "…" }, required: { a: "…", b: "…" }, },});/** * Button */export interface ButtonProps extends Omit<ButtonVariantProps, "required">, Required<Pick<ButtonVariantProps, "required">> {}export const button = (props: ButtonProps) => buttonVariants(props);// ❌ TypeScript Error:// Argument of type "{}": is not assignable to parameter of type "ButtonProps".// Property "required" is missing in type "{}" but required in type// "ButtonProps".button({});// ✅button({ required: "a" }); --- ## Page: https://beta.cva.style/examples/11ty ## Tailwind CSS const { cva } = require("class-variance-authority");// ⚠️ Disclaimer: Use of Tailwind CSS is optionalconst button = cva("button", { variants: { intent: { primary: [ "bg-blue-500", "text-white", "border-transparent", "hover:bg-blue-600", ], secondary: [ "bg-white", "text-gray-800", "border-gray-400", "hover:bg-gray-100", ], }, size: { small: ["text-sm", "py-1", "px-2"], medium: ["text-base", "py-2", "px-4"], }, }, compoundVariants: [{ intent: "primary", size: "medium", class: "uppercase" }], defaultVariants: { intent: "primary", size: "medium", },});module.exports = function ({ label, intent, size }) { return `<button class="${button({ intent, size })}">${label}</button>`;}; --- ## Page: https://beta.cva.style/examples/astro ## Tailwind CSS View on GitHub ↗ --- ## Page: https://beta.cva.style/examples/bem .button { /* */}.button--primary { /* */}.button--secondary { /* */}.button--small { /* */}.button--medium { /* */}.button--primary-small { /* */} import { cva } from "class-variance-authority";const button = cva("button", { variants: { intent: { primary: "button--primary", secondary: "button--secondary", }, size: { small: "button--small", medium: "button--medium", }, }, compoundVariants: [ { intent: "primary", size: "medium", class: "button--primary-small" }, ], defaultVariants: { intent: "primary", size: "medium", },});button();// => "button button--primary button--medium"button({ intent: "secondary", size: "small" });// => "button button--secondary button--small" --- ## Page: https://beta.cva.style/examples/react/css-modules View on GitHub ↗ --- ## Page: https://beta.cva.style/examples/react/tailwindcss ## Basic Component View on GitHub ↗ ## Compound Components View on GitHub ↗ --- ## Page: https://beta.cva.style/examples/svelte View on GitHub ↗ --- ## Page: https://beta.cva.style/examples/vue View on GitHub ↗ --- ## Page: https://beta.cva.style/examples/other-use-cases ## Other Use Cases Although primarily designed for handling class names, at its core, `cva` is really just a fancy way of managing a string… ## Dynamic Text Content const greeter = cva("Good morning!", { variants: { isLoggedIn: { true: "Here's a secret only logged in users can see", false: "Log in to find out more…", }, }, defaultVariants: { isLoggedIn: "false", },});greeter();// => "Good morning! Log in to find out more…"greeter({ isLoggedIn: "true" });// => "Good morning! Here's a secret only logged in users can see" --- ## Page: https://beta.cva.style/api-reference Builds a `cva` component import { cva } from "cva";const component = cva(options); ### Parameters 1. `options` * `base`: the base class name (`string`, `string[]` or other `clsx` value) * `variants`: your variants schema * `compoundVariants`: variants based on a combination of previously defined variants * `defaultVariants`: set default values for previously defined variants. ### Returns A `cva` component function ## `cx` Concatenates class names (an alias of `clsx`) import { cx } from "cva";const className = cx(classes); ### Parameters * `classes`: array of classes to be concatenated (see `clsx` usage) ### Returns `string` ## `compose` Shallow merges any number of `cva` components into a single component. import { compose } from "cva";const composedComponent = compose(options); ### Parameters `options`: array of `cva` components ### Returns A `cva` component function ## `defineConfig` Generate `cva`, `cx` and `compose` functions based on your preferred configuration. Store in a `cva.config.ts` file, and import across your project. import { defineConfig } from "cva";export const { cva, cx, compose } = defineConfig(options); 1. `options` * `hooks` * `onComplete`: returns a concatenated class string of all classes passed to `cx`, `cva` or `compose`. --- ## Page: https://beta.cva.style/tutorials ## Authoring Components with CVA + tailwindcss React Tips with Brooks Lybrand • 29th August 2023 --- ## Page: https://beta.cva.style/faqs ## Why Don’t You Provide a `styled` API? Long story short: it’s unnecessary. `cva` encourages you to think of components as traditional CSS classes: * Less JavaScript is better * They’re framework agnostic; truly reusable * Polymorphism is free; just apply the class to your preferred HTML element * Less opinionated; you’re free to build components with `cva` however you’d like See the “Polymorphism” documentation for further recommendations. ## How Can I Create Responsive Variants like Stitches.js? You can’t. `cva` doesn’t know about how you choose to apply CSS classes, and it doesn’t want to. We recommend either: * Showing/hiding elements with different variants, based on your preferred breakpoint. Example: With Tailwind export const Example = () => ( <> <div className="hidden sm:inline-flex"> <button className={button({ intent: "primary" })}>Hidden until sm</button> </div> <div className="inline-flex sm:hidden"> <button className={button({ intent: "secondary" })}> Hidden after sm </button> </div> </>); * Create a bespoke variant that changes based on the breakpoint. _e.g. `button({ intent: "primaryUntilMd" })`_ This is something I’ve been thinking about since the project’s inception, and I’ve gone back and forth many times on the idea of building it. It’s a large undertaking and brings all the complexity of supporting many different build tools and frameworks. In my experience, “responsive variants” are typically rare, and hiding/showing different elements is usually good enough to get by. To be frank, I’m probably not going to build/maintain a solution unless someone periodically gives me a thick wad of cash to do so, and even then I’d probably rather spend my free time living my life.