A tiny, performant, utility for constructing variant based CSS class strings.
NPM:
npm i cvu
# or
npx jsr add @erictaylor/cvu
Yarn:
yarn add cvu
# or
yarn dlx jsr add @erictaylor/cvu
PNPM:
pnpm add cvu
# or
pnp dlx jsr add @erictaylor/cvu
Bun:
bun add cvu
# or
bux jsr add @erictaylor/cvu
Deno:
deno add @erictaylor/cvu
Note
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.
cvu
is 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 otherclsx
compatible 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
variants
API 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.