UNPKG

class-variance-authority

Version:

Class Variance Authority 🧬

695 lines (556 loc) • 16 kB
![CVA](/.github/assets/meta.png) <h1 align="center">cva</h1> <p align="center"> <strong>C</strong>lass <a href="https://www.youtube.com/watch?v=9ZcyoZlY0aU"><strong>V</strong>ariance <strong>A</strong>uthority</a> </p> <p align="center"> <a href="https://www.npmjs.com/package/class-variance-authority"> <img alt="NPM Version" src="https://badgen.net/npm/v/class-variance-authority" /> </a> <a href="https://www.npmjs.com/package/class-variance-authority"> <img alt="Types Included" src="https://badgen.net/npm/types/class-variance-authority" /> </a> <a href="https://bundlephobia.com/result?p=class-variance-authority"> <img alt="Minizipped Size" src="https://img.shields.io/bundlephobia/minzip/class-variance-authority" /> </a> <a href="https://github.com/joe-bell/cva/blob/main/LICENSE"> <img alt="Apache-2.0 License" src="https://badgen.net/github/license/joe-bell/cva" /> </a> <a href="https://www.npmjs.com/package/class-variance-authority"> <img alt="NPM Downloads" src="https://badgen.net/npm/dm/class-variance-authority" /> </a> <a href="https://twitter.com/joebell_"> <img alt="Follow @joebell_ on Twitter" src="https://img.shields.io/twitter/follow/joebell_.svg?style=social&label=Follow" /> </a> </p> <br /> ## Introduction CSS-in-TS libraries such as [Stitches](https://stitches.dev/docs/variants) and [Vanilla Extract](https://vanilla-extract.style/documentation/) 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**](https://stitches.dev/) ([Modulz](http://modulz.app)) Huge thanks to the Modulz team for pioneering the `variants` API movement – your open-source contributions are immensely appreciated - [**clb**](https://github.com/crswll/clb) ([Bill Criswell](https://github.com/crswll)) This project originally started out with the intention of merging into the wonderful [`clb`](https://github.com/crswll/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](https://github.com/crswll/clb). Thank you Bill! - [**Vanilla Extract**](http://vanilla-extract.style) ([Seek](https://github.com/seek-oss)) ## Installation ```sh npm i class-variance-authority ``` <details> <summary>"Do I really have to write such a long package name for every import?"</summary> Unfortunately, yes. Originally, the plan was the publish the package as `cva`, but this name [has been taken and marked as a "placeholder"](https://www.npmjs.com/package/cva). I've reached out to the author and NPM support, but have yet to hear back. In the meantime, you can always alias the package for your convenience… ### Aliasing 1. Alias the package with [`npm install`](https://docs.npmjs.com/cli/v6/commands/npm-install) ```sh npm i cva@npm:class-variance-authority ``` 2. Then import like so: ```ts import { cva } from "cva"; // … ``` </details> ### Tailwind CSS IntelliSense If you're using the ["Tailwind CSS IntelliSense" Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss), you can enable autocompletion inside `cva` by adding the following to your [`settings.json`](https://code.visualstudio.com/docs/getstarted/settings): ```json { "tailwindCSS.experimental.classRegex": [ ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] ] } ``` ## Getting Started > **Disclaimer**: Although `cva` is a [**tiny**](https://bundlephobia.com/package/class-variance-authority) library, it's best to use in a SSR/SSG environment – your user probably doesn't need this JavaScript, especially for static components. ### Your First Component To kick things off, let's build a "basic" `button` component, using `cva` to handle our variant's classes > **Note:** Use of Tailwind CSS is optional ```ts // components/button.ts import { cva } from "class-variance-authority"; const button = cva(["font-semibold", "border", "rounded"], { variants: { intent: { primary: [ "bg-blue-500", "text-white", "border-transparent", "hover:bg-blue-600", ], // **or** // 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", }, }); 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" ``` ### Additional Classes All `cva` components provide an optional `class` prop, which can be used to pass additional classes to the component. ```ts // components/button.ts import { cva } from "class-variance-authority"; const button = cva(/* … */); button({ class: "m-4" }); // => "…buttonClasses m-4" ``` ### TypeScript Helpers `cva` offers the `VariantProps` helper to extract variant types ```ts // components/button.ts import type { VariantProps } from "class-variance-authority"; import { cva, cx } from "class-variance-authority"; /** * Button */ export type ButtonProps = VariantProps<typeof button>; export const button = cva(/* … */); ``` ### Composing Components Whilst `cva` doesn't yet offer a built-in method for composing components, it does offer the tools to _extend_ components on your own terms… For example; two `cva` components, concatenated together with `cx`: ```ts // components/card.ts import type { VariantProps } from "class-variance-authority"; import { cva, cx } from "class-variance-authority"; /** * Box */ 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, }, }); /** * Card */ 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", }, }, }); export interface CardProps extends BoxProps, CardBaseProps {} export const card = ({ margin, padding, shadow }: CardProps = {}) => cx(box({ margin, padding }), cardBase({ shadow })); ``` ## API Reference ### `cva` Builds a `cva` component ```ts const component = cva("base", options); ``` #### Parameters 1. `base`: the base class name (`string`, `string[]` or `null`) 1. `options` _(optional)_ - `variants`: your variants schema - `compoundVariants`: variants based on a combination of previously defined variants - `defaultVariants`: set default values for previously defined variants. _note: these default values can be removed completely by setting the variant as `null`_ #### Returns A `cva` component function ### `cx` Concatenates class names ```ts const className = cx(classes); ``` #### Parameters - `classes`: array of classes to be concatenated #### Returns `string` ## Examples > ⚠️ Warning: The examples below are purely demonstrative and haven't been tested thoroughly (yet) <details> <summary>Astro</summary> ```astro --- import { cva, type VariantProps } from "class-variance-authority"; const 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" }], }); interface Props extends VariantProps<typeof button> {} /** * For Astro components, we recommend setting your defaultVariants within * Astro.props (which are `undefined` by default) */ const { intent = "primary", size = "medium" } = Astro.props; --- <button class={button({ intent, size })}> <slot /> </button> ``` </details> <details> <summary>BEM</summary> ```css /* styles.css */ .button { /* */ } .button--primary { /* */ } .button--secondary { /* */ } .button--small { /* */ } .button--medium { /* */ } .button--primary-small { /* */ } ``` ```ts 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" ``` </details> <details> <summary>11ty (with Tailwind)</summary> ```js // button.11ty.js const { cva } = require("class-variance-authority"); // ⚠️ Disclaimer: Use of Tailwind CSS is optional const 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>`; }; ``` </details> <details> <summary>React (with CSS Modules)</summary> ```css /* button.module.css */ .base { /* */ } .primary { /* */ } .secondary { /* */ } .small { /* */ } .medium { /* */ } .primaryMedium { /* */ } ``` ```tsx // button.tsx import React from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { base, primary, secondary, small, medium, primaryMedium, } from "./button.module.css"; const button = cva(base, { variants: { intent: { primary, secondary, }, size: { small, medium, }, }, compoundVariants: [ { intent: "primary", size: "medium", class: primaryMedium }, ], defaultVariants: { intent: "primary", size: "medium", }, }); export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement>, VariantProps<typeof button> {} export const Button: React.FC<ButtonProps> = ({ className, intent, size, ...props }) => ( <button className={button({ intent, size, class: className })} {...props} /> ); ``` </details> <details> <summary>React (with Tailwind)</summary> ```tsx // button.tsx import React from "react"; import { cva, type VariantProps } from "class-variance-authority"; // ⚠️ Disclaimer: Use of Tailwind CSS is optional const 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", }, }); export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement>, VariantProps<typeof button> {} export const Button: React.FC<ButtonProps> = ({ className, intent, size, ...props }) => ( <button className={button({ intent, size, class: className })} {...props} /> ); ``` </details> <details> <summary>Svelte</summary> ```svelte <!-- button.svelte --> <script lang="ts"> import { cva, type VariantProps } 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-medium" }, ], defaultVariants: { intent: "primary", size: "medium", }, }); type ButtonProps = VariantProps<typeof button>; export let intent: ButtonProps["intent"]; export let size: ButtonProps["size"]; </script> <button class={button({ intent, size })}><slot /></button> <style> .button { /* … */ } .button--primary { /* … */ } .button--secondary { /* … */ } .button--small { /* … */ } .button--medium { /* … */ } .button--primary-medium { /* … */ } </style> ``` </details> <details> <summary>Vue 3</summary> ```vue <!-- button.vue --> <script setup lang="ts"> import { cva, type VariantProps } 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-medium" }, ], defaultVariants: { intent: "primary", size: "medium", }, }); type ButtonProps = VariantProps<typeof button>; defineProps<{ intent: ButtonProps["intent"]; size: ButtonProps["size"]; }>(); </script> <template> <button :class="button({ intent, size })"> <slot /> </button> </template> <style> .button { /* … */ } .button--primary { /* … */ } .button--secondary { /* … */ } .button--small { /* … */ } .button--medium { /* … */ } .button--primary-medium { /* … */ } </style> ``` </details> ### Other Use Cases Although primarily designed for handling class names, at its core, `cva` is really just a fancy way of managing a string… <details> <summary>Dynamic Text Content</summary> ```ts 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" ``` </details>