Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/erictaylor/cvu

A tiny, performant, utility for constructing variant based CSS class strings.
https://github.com/erictaylor/cvu

Last synced: about 2 months ago
JSON representation

A tiny, performant, utility for constructing variant based CSS class strings.

Awesome Lists containing this project

README

        

cvu


A tiny, performant, utility for constructing variant based CSS class strings.









## Installation

NPM:

```sh
npm i cvu
# or
npx jsr add @erictaylor/cvu
```

Yarn:

```sh
yarn add cvu
# or
yarn dlx jsr add @erictaylor/cvu
```

PNPM:

```sh
pnpm add cvu
# or
pnp dlx jsr add @erictaylor/cvu
```

Bun:

```sh
bun add cvu
# or
bux jsr add @erictaylor/cvu
```

Deno:

```sh
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**

1. [Install the "Tailwind CSS IntelliSense" Visual Studio Code extension.](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)
2. Add the following to your [`settings.json`](https://code.visualstudio.com/docs/getstarted/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`](https://github.com/dcastil/tailwind-merge) package.

For bulletproof components, wrap your `cvu` calls with `twMerge`.

Example with tailwind-merge

```tsx
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
) => {
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

```tsx
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.

```ts
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.

```ts
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

```ts
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.

```ts
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.

```ts
import { cvu, type VariantProps } from "cvu";

type ButtonClassnamesProps = VariantProps;
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.

```ts
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

```ts
import { cvu, clsx, type VariantProps } from "cvu";

/**
* Box
*/
export type BoxClassnamesProps = VariantProps;
export const boxClassnames = cvu(/* … */);

/**
* Card
*/
type CardBaseClassNamesProps = VariantProps;
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.

```ts
import { cvu } from "cvu";

const classVariants = cvu("base", variantsConfig);
```

#### Parameters

1. `base` - the base class name (`string`, `string[]`, or other `clsx` compatible value).
2. `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.

```ts
import { config, clsx } from "cvu";

export const customCvu = config({
clsx: (...inputs) => twMerge(clsx(inputs)),
});
```

## Acknowledgements

- [Stitches](https://stitches.dev)

For pioneering the `variants` API movement.

- [cva](https://cva.style)

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](/LICENSE.md) © [Eric Taylor](https://github.com/erictaylor)