npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

cvu

v1.0.0

Published

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

Downloads

62

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

  1. Install the "Tailwind CSS IntelliSense" Visual Studio Code extension.
  2. 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

  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.

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