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

classigo

v2.0.0

Published

Fast class name utility for TypeScript — strings and objects, zero dependencies.

Readme

classigo

Fast class name utility for TypeScript — strings, objects, zero dependencies.

npm downloads bundle size MIT TypeScript

Version française : README.fr.md

import classigo from "classigo";

// Strings + falsy values
classigo("btn", isActive && "btn--active", isDisabled && "btn--disabled");
// → "btn btn--active"

// Object syntax
classigo("btn", {
  "btn--active":   isActive,
  "btn--disabled": isDisabled,
  "btn--loading":  isLoading,
});
// → "btn btn--active"

// Mixed
classigo("btn", `btn--${variant}`, { "btn--active": isActive });
// → "btn btn--primary btn--active"

Why classigo?

Honestly — not because the performance difference will matter in your app. A render cycle costs thousands of nanoseconds; your className utility costs tens. Nobody has ever shipped a slow product because they used clsx.

The real reason to use classigo is the contract. It does not support arrays, numbers, or nested calls — deliberately. That means:

  • Passing an array is a TypeScript compile error, not a silent runtime surprise.
  • The types accurately describe what the function actually does.
  • The bundle is small because there is no code for cases the function doesn't handle.

| | classigo | classigo/lite | clsx | classnames | | :--- | :---: | :---: | :---: | :---: | | Strings + falsy values | ✅ | ✅ | ✅ | ✅ | | Object syntax ({ [cls]: boolean }) | ✅ | ❌ | ✅ | ✅ | | Arrays / nested calls | ❌ | ❌ | ✅ | ✅ | | Numbers | ❌ | ❌ | ✅ | ✅ | | ESM only | ✅ | ✅ | ✅ | ❌ | | Zero dependencies | ✅ | ✅ | ✅ | ✅ | | Minified size | 217B | 137B | ~560B | ~1.1kB |

When to use classigo: you control all inputs — class names come from strings, template literals, conditionals, or objects. That covers the vast majority of component code in React / Vue / Svelte.

When to use clsx instead: you receive class lists from an external source (CMS, config, API) that may be arrays or nested structures, or a utility in your codebase returns string[] that you want to pass directly without spreading.

// classigo covers this — you control the inputs
classigo("btn", `btn--${variant}`, { "btn--active": isActive })

// clsx covers this — external array of unknown shape
const rules = fetchClassRulesFromCMS(); // (string | string[])[]
clsx(...rules)

// the spread workaround works when you know the array is flat
classigo(...myKnownFlatArray)  // ✅ fine
classigo(...myUnknownArray)    // ❌ breaks if any element is itself an array

Already using clsx? Don't migrate. The difference isn't perceptible in a real app. classigo makes sense if you're starting fresh and want a minimal, strict-contract utility — or if you just prefer knowing that an accidental array at a call site is caught at compile time rather than silently joined wrong.

Do you even need a class utility?

For a handful of conditionals, you might not:

// Perfectly fine without a library
const cls = ["btn", isActive && "btn--active", isDisabled && "btn--disabled"]
  .filter(Boolean)
  .join(" ");

The array approach is readable but allocates a new array every call. Once your component re-renders hundreds of times per second, or your design system calls a class util in every component, that allocation adds up. A tight utility that does one string concatenation pass pays for itself in high-frequency render paths.

Install

bun add classigo
# or
npm i classigo
# or
pnpm add classigo

Requires TypeScript ≥ 5.0. Zero runtime dependencies.

API

classigo(...classes)

Combines class names, filtering out falsy values. Accepts strings, falsy values (false, null, undefined), and objects ({ [className]: boolean }).

import classigo from "classigo";
// or
import { classigo } from "classigo";
// Strings
classigo("btn", "btn--primary")
// → "btn btn--primary"

// Falsy values are skipped
classigo("btn", false, null, undefined, "btn--active")
// → "btn btn--active"

// Conditional with &&
classigo("btn", isActive && "btn--active")
// → "btn btn--active"  (when isActive is true)
// → "btn"              (when isActive is false)

// Object — each key is included when its value is truthy
classigo("btn", { "btn--active": isActive, "btn--disabled": isDisabled })
// → "btn btn--active"

// Template literals
classigo(`card--${variant}`, `card--${size}`)
// → "card--primary card--lg"

// CSS Modules pattern
classigo(styles.root, { [styles.active]: isActive })
// → "root active"  (when isActive is true)

Type signature:

type ClassObject = Record<string, boolean | null | undefined>;
type ClassValue  = string | false | null | undefined | ClassObject;

function classigo(...classes: ClassValue[]): string;

classigo/lite

A strings-only variant — no object support, smallest possible bundle (137B). Useful when you control all inputs and never need object syntax.

import classigo from "classigo/lite";
// or
import { classigo } from "classigo/lite";
classigo("btn", isActive && "btn--active", isDisabled && "btn--disabled");
// → "btn btn--active"

Type signature:

type ClassLiteValue = string | false | null | undefined;

function classigo(...classes: ClassLiteValue[]): string;

Passing an object to classigo/lite is a TypeScript compile error — which is the point. If you want the type system to enforce that no object ever reaches this call site, use /lite.

Migrating from v1

v2 has two breaking changes:

1. ESM only. CommonJS (require("classigo")) is no longer supported. If your project needs CJS, stay on v1 or use a bundler that converts ESM.

2. Object syntax added. This is backwards-compatible for pure string usage, but the published types now accept ClassObject alongside strings. If you have strict type assertions that assumed ClassValue was string-only, update them or switch to classigo/lite.

// v1
classigo("btn", isActive && "btn--active");          // ✅ still works

// v2 — new
classigo("btn", { "btn--active": isActive });         // ✅ object syntax
import classigoLite from "classigo/lite";             // ✅ strings-only export

There are no API changes beyond these two. The function signature is otherwise identical.

Benchmarks

I bench because I like to know whether what I built is fast or slow — not because these numbers should drive your adoption decision. A className call costs tens of nanoseconds; a React render costs thousands. The bench exists for curiosity and for catching regressions if someone submits a PR that changes the hot path.

That said, here are the numbers. Measured with mitata on an i7-14700KF, Bun 1.2.13. Lower is better.

| Scenario | classigo v2 | classigo/lite | clsx | classnames | | :--- | ---: | ---: | ---: | ---: | | 3 strings | 8.67 ns | 11.54 ns | 24.46 ns | 18.03 ns | | conditional (4 args, 2× &&) | 17.24 ns | 13.42 ns | 31.97 ns | 18.00 ns | | complex (8 args, template literals) | 26.38 ns | 27.94 ns | 58.27 ns | 34.39 ns | | re-render — boolean toggle | 9.21 ns | 8.66 ns | 17.08 ns | 11.98 ns | | object syntax (3-key object) | 20.34 ns | — | 29.96 ns | 24.96 ns | | design-system (20 dynamic args) | 96.45 ns | 106.28 ns | 148.82 ns | 121.79 ns | | css-modules (15-key object) | 71.73 ns | — | 80.45 ns | 83.61 ns |

What the numbers actually tell you:

  • classigo is faster because it does less — no array handling, no recursion, no number coercion. The speed difference is a direct consequence of the narrower API, not an algorithmic breakthrough.
  • The gap compresses as workload grows (15-key object: 1.12× vs clsx). When for...in iteration dominates, every library converges to the same cost.
  • /lite wins on pure boolean-toggle patterns — no typeof check at all, shortest possible loop. Use it when you never need object syntax.
  • These numbers will not move the needle in your app. Profile your renders before concluding classnames is your bottleneck.

Run the suite yourself:

bun run bench

Contributing

See CONTRIBUTING.md for the full guidelines. The short version: use Bun, follow Conventional Commits, run bun run test && bun run build before opening a PR, and include a bench delta if you touch src/index.ts or src/lite.ts.

License

MIT © SUP2Ak