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 🙏

© 2026 – Pkg Stats / Ryan Hefner

cn-variants

v1.0.0

Published

Tiny utilities for working with Tailwind CSS class names. Combines clsx + tailwind-merge with a typed variants helper.

Readme

cn-variants

Tiny utilities for working with Tailwind CSS class names. Combines clsx + tailwind-merge with a typed variants helper.

Install

npm install cn-variants

cn(...inputs)

Merges class names using clsx and tailwind-merge. Handles conditionals, duplicates, and Tailwind conflicts.

import { cn } from "cn-variants";

// Tailwind conflict resolution — last value wins
cn("px-4 py-2", "px-6");
// → "py-2 px-6"

// Conditionals
cn("text-red-500", isActive && "text-blue-500");
// → "text-blue-500" (when isActive is true)

// Object syntax
cn("flex", { "gap-4": hasGap, "items-center": centered });
// → "flex gap-4 items-center" (when both are true)

// Arrays
cn(["rounded-lg", "shadow-md"], "p-4");
// → "rounded-lg shadow-md p-4"

// Mixed — all clsx input types work
cn("base", ["flex", "gap-2"], { "font-bold": isActive }, undefined, null, false);
// → "base flex gap-2 font-bold" (when isActive is true)

variants(map)

Creates a typed lookup function for Tailwind class variants. Returns "" for unknown keys at runtime, relying on TypeScript for compile-time safety.

import { cn, variants } from "cn-variants";

const buttonVariant = variants({
  primary: "bg-indigo-600 text-white border-none",
  secondary: "bg-transparent text-indigo-600 border border-indigo-600",
  danger: "bg-red-600 text-white border-none",
});

const buttonSize = variants({
  sm: "px-3 py-1 text-xs",
  md: "px-4 py-2 text-sm",
  lg: "px-6 py-3 text-base",
});

The returned function exposes a frozen .options object with the original map, useful for deriving union types:

type ButtonVariant = keyof typeof buttonVariant.options;
// → "primary" | "secondary" | "danger"

type ButtonSize = keyof typeof buttonSize.options;
// → "sm" | "md" | "lg"

Using variants with cn in components

interface ButtonProps {
  variant?: ButtonVariant;
  size?: ButtonSize;
  className?: string;
  children: React.ReactNode;
}

export function Button({ variant = "primary", size = "md", className, children }: ButtonProps) {
  return (
    <button
      className={cn("rounded-md font-medium", buttonVariant(variant), buttonSize(size), className)}
    >
      {children}
    </button>
  );
}

Callers can override any style through className — tailwind-merge ensures the caller's classes win:

<Button variant="primary" className="bg-purple-600">
  {/* bg-purple-600 overrides the primary variant's bg-indigo-600 */}
</Button>

Compound variants

For styles that depend on a combination of variants, use conditionals in cn:

cn(
  "rounded-md font-medium",
  buttonVariant(variant),
  buttonSize(size),
  variant === "primary" && size === "lg" && "uppercase tracking-wide",
  variant === "danger" && "ring-2 ring-red-300",
  className,
);

ClassValue type

If you write wrapper functions around cn, you can import the ClassValue type directly:

import { type ClassValue, cn } from "cn-variants";

function card(...classes: ClassValue[]) {
  return cn("rounded-lg border bg-white shadow-sm", ...classes);
}

Tailwind IntelliSense

To get autocomplete for class strings inside variants() and cn(), add them to the classFunctions setting in your editor's Tailwind CSS configuration.

VS Code

Install the Tailwind CSS IntelliSense extension, then add to your .vscode/settings.json:

{
  "tailwindCSS.classFunctions": ["cn", "variants"]
}

Zed

Add to your project's .zed/settings.json:

{
  "lsp": {
    "tailwindcss-language-server": {
      "settings": {
        "classFunctions": ["cn", "variants"]
      }
    }
  }
}

IntelliJ IDEA / WebStorm

Install the Tailwind CSS plugin, then add to your tailwind.config.js:

module.exports = {
  classFunctions: ["cn", "variants"],
};

Tree-shaking

cn and variants are independent. If you only import variants, your bundler will tree-shake away cn and its dependencies (clsx, tailwind-merge), keeping your bundle minimal.

Versioning policy

cn-variants follows semver and pins to the current major of its dependencies: clsx ^2 and tailwind-merge ^3.

  • Patch/minor upstream releases are absorbed automatically. No action needed on your part.
  • Major upstream releases may change observable behavior (e.g. how tailwind-merge resolves conflicting utilities). When this happens, cn-variants will release a new major version that bumps the dependency range.

If cn("px-2", "px-4") returns a different result because of an upstream update, that's a breaking change from your perspective and will be treated as one.

License

MIT