A tiny, performant, utility for constructing variant based CSS class strings.
NPM:
npm i cvu
# or
npx jsr add @erictaylor/cvuYarn:
yarn add cvu
# or
yarn dlx jsr add @erictaylor/cvuPNPM:
pnpm add cvu
# or
pnp dlx jsr add @erictaylor/cvuBun:
bun add cvu
# or
bux jsr add @erictaylor/cvuDeno:
deno add @erictaylor/cvuNote
This library is an ESM only package as of version 1.0.0.
If you're a Tailwind user, here are some additional (optional) steps to get the most out of cvu.
You can enable autocompletion inside cvu using the steps below:
VSCode
- Install the "Tailwind CSS IntelliSense" Visual Studio Code extension.
- Add the following to your
settings.json:
{
"tailwindCSS.experimental.classRegex": [
["cvu\\s*(?:<[\\s\\S]*?>)?\\s*\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}Although cvu's API is designed to help you avoid style conflicts, there is still a small margin of error.
If you're keen to lift that burden altogether, check out tailwind-merge package.
For bulletproof components, wrap your cvu calls with twMerge.
Example with tailwind-merge
import { cvu, type VariantProps } from "cvu";
import { twMerge } from "tailwind-merge";
const buttonVariants = cvu(["your", "base", "classes"], {
variants: {
intent: {
primary: ["your", "primary", "classes"],
},
},
defaultVariants: {
intent: "primary",
},
});
export const buttonClassNames = (
props: VariantProps<typeof buttonVariants>
) => {
return twMerge(buttonVariants(props));
};If you find yourself using twMerge a lot, you can create a custom cvu function that wraps twMerge for you.
Example with custom cvu
import { type ClassVariantUtility, config, clsx } from "cvu";
import { twMerge } from "tailwind-merge";
export const cvu: ClassVariantUtility = config({
clsx: (...inputs) => twMerge(clsx(inputs)),
});Here is a simple example of a cvu generated utility function for generating class names for a button component.
Note
The use of Tailwind CSS here is purely for demonstration purposes.
cvuis not tied to any specific CSS framework.
import { cvu } from "cvu";
const buttonClassnames = cvu(
["font-semibold", "border", "rounded"],
// --or--
// 'font-semibold border rounded'
{
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: {
sm: "text-sm py-1 px-2",
md: ["text-base", "py-2", "px-4"],
},
},
compoundVariants: [
{
intent: "primary",
size: "md",
className: "uppercase",
},
],
defaultVariants: {
intent: "primary",
size: "md",
},
}
);
buttonClassnames();
// => 'font-semibold border rounded bg-blue-500 text-white border-transparent hover:bg-blue-600 text-base py-2 px-4 uppercase'
buttonClassnames({ intent: "secondary", size: "sm" });
// => 'font-semibold border rounded bg-white text-gray-800 border-gray-400 hover:bg-gray-100 text-sm py-1 px-2'Variants that apply when multiple other variant conditions are met.
import { cvu } from "cvu";
const buttonClassnames = cva("…", {
variants: {
intent: {
primary: "…",
secondary: "…",
},
size: {
sm: "…",
md: "…",
},
},
compoundVariants: [
// Applied via
// `buttonClassnames({ intent: 'primary', size: 'md' });`
{
intent: "primary",
size: "md",
// This is the className that will be applied.
className: "…",
},
],
});import { cvu } from "cvu";
const buttonClassnames = cva("…", {
variants: {
intent: {
primary: "…",
secondary: "…",
},
size: {
sm: "…",
md: "…",
},
},
compoundVariants: [
// Applied via
// `buttonClassnames({ intent: 'primary', size: 'md' });`
// or
// `buttonClassnames({ intent: 'secondary', size: 'md' });`
{
intent: ["primary", "secondary"],
size: "md",
// This is the className that will be applied.
className: "…",
},
],
});All cvu utilities provide an optional string argument, which will be appended to the end of the generated class name.
This is useful in cases where want to pass a React className prop to be merged with the generated class name.
import { cvu } from "cvu";
const buttonClassnames = cvu("rounded", {
variants: {
intent: {
primary: "bg-blue-500",
},
},
});
buttonClassnames(undefined, "m-4");
// => 'rounded m-4'
buttonClassnames({ intent: "primary" }, "m-4");
// => 'rounded bg-blue-500 m-4'cvu offers the VariantProps helper to extract variant types from a cvu utility.
import { cvu, type VariantProps } from "cvu";
type ButtonClassnamesProps = VariantProps<typeof buttonClassnames>;
const buttonClassnames = cvu(/* … */);Additionally, cvu offers the VariantPropsWithRequired helper to extract variant types from a cvu utility, with the specified keys marked as required.
import { cvu, type VariantPropsWithRequired } from "cvu";
type ButtonClassnamesProps = VariantPropsWithRequired<
typeof buttonClassnames,
"intent"
>;
const buttonClassnames = cvu("…", {
variants: {
intent: {
primary: "…",
secondary: "…",
},
size: {
sm: "…",
md: "…",
},
},
});
const wrapper = (props: ButtonClassnamesProps) => {
return buttonClassnames(props);
};
// ❌ TypeScript Error:
// Argument of type "{}": is not assignable to parameter of type "ButtonClassnamesProps".
// Property "intent" is missing in type "{}" but required in type
// "ButtonClassnamesProps".
wrapper({});
// ✅
wrapper({ intent: "primary" });import { cvu, clsx, type VariantProps } from "cvu";
/**
* Box
*/
export type BoxClassnamesProps = VariantProps<typeof boxClassnames>;
export const boxClassnames = cvu(/* … */);
/**
* Card
*/
type CardBaseClassNamesProps = VariantProps<typeof cardBaseClassnames>;
const cardBaseClassnames = cvu(/* … */);
export interface CardClassnamesProps
extends BoxClassnamesProps,
CardBaseClassnamesProps {}
export const cardClassnames =
({}: /* destructured props */ CardClassnamesProps = {}) =>
clsx(
boxClassnames({
/* … */
}),
cardBaseClassnames({
/* … */
})
);Builds a typed utility function for constructing className strings with given variants.
import { cvu } from "cvu";
const classVariants = cvu("base", variantsConfig);-
base- the base class name (string,string[], or otherclsxcompatible value). -
variantsConfig- (optional)-
variants- your variants schema -
componentVariants- 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.
-
Allows you to provide your own underlying clsx implementation or wrapping logic.
import { config, clsx } from "cvu";
export const customCvu = config({
clsx: (...inputs) => twMerge(clsx(inputs)),
});-
For pioneering the
variantsAPI movement. -
For the inspiration behind
cvu. I personally didn't find the library to quite meet my needs or API preferences, but it's a great library nonetheless. -
An amazing library for lightweight utility for constructing className strings conditionally.