cvu
v1.0.0
Published
A tiny, performant, utility for constructing variant based CSS class strings.
Downloads
31
Maintainers
Readme
Installation
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.
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
.
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.
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"],
// --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'
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: [
// Applied via
// `buttonClassnames({ intent: 'primary', size: 'md' });`
{
intent: "primary",
size: "md",
// This is the className that will be applied.
className: "…",
},
],
});
Targeting Multiple Variant Conditions
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: "…",
},
],
});
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");
// => 'rounded m-4'
buttonClassnames({ intent: "primary" }, "m-4");
// => 'rounded bg-blue-500 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);
};
// ❌ 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" });
Composing Utilities
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({
/* … */
})
);
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 otherclsx
compatible value).variantsConfig
- (optional)variants
- your variants schemacomponentVariants
- variants based on a combination of previously defined variantsdefaultVariants
- 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
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.