cvu
A tiny, performant, utility for constructing variant based CSS class strings.
Installation
NPM:
npm i cvu
npx jsr add @erictaylor/cvu
Yarn:
yarn add cvu
yarn dlx jsr add @erictaylor/cvu
PNPM:
pnpm add cvu
pnp dlx jsr add @erictaylor/cvu
Bun:
bun add cvu
bux jsr add @erictaylor/cvu
Deno:
deno add @erictaylor/cvu
[!NOTE]
This library is an ESM only package as of version 1.0.0.
Tailwind CSS
If you're a Tailwind user, here are some additional (optional) steps to get the most out of cvu
.
IntelliSense
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*\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
Handling Style Conflicts
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)),
});
Getting Started
Your First Utility
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"],
{
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();
buttonClassnames({ intent: "secondary", size: "sm" });
Compound Variants
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: [
{
intent: "primary",
size: "md",
className: "…",
},
],
});
Targeting Multiple Variant Conditions
import { cvu } from "cvu";
const buttonClassnames = cva("…", {
variants: {
intent: {
primary: "…",
secondary: "…",
},
size: {
sm: "…",
md: "…",
},
},
compoundVariants: [
{
intent: ["primary", "secondary"],
size: "md",
className: "…",
},
],
});
Additional Classes
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");
buttonClassnames({ intent: "primary" }, "m-4");
TypeScript
VariantProps
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();
VariantPropsWithRequired
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);
};
wrapper({});
wrapper({ intent: "primary" });
Composing Utilities
import { cvu, clsx, type VariantProps } from "cvu";
export type BoxClassnamesProps = VariantProps<typeof boxClassnames>;
export const boxClassnames = cvu();
type CardBaseClassNamesProps = VariantProps<typeof cardBaseClassnames>;
const cardBaseClassnames = cvu();
export interface CardClassnamesProps
extends BoxClassnamesProps,
CardBaseClassnamesProps {}
export const cardClassnames =
({}: CardClassnamesProps = {}) =>
clsx(
boxClassnames({
}),
cardBaseClassnames({
})
);
API
cvu
Builds a typed utility function for constructing className strings with given variants.
import { cvu } from "cvu";
const classVariants = cvu("base", variantsConfig);
Parameters
-
base
- the base class name (string
, string[]
, or other clsx
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
.
config
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)),
});
Acknowledgements
-
Stitches
For pioneering the variants
API movement.
-
cva
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.
-
clsx
An amazing library for lightweight utility for constructing className strings conditionally.
License
MIT © Eric Taylor