CLI tool
shadcn-ui/cn avatar
shadcn-ui/cn

shadcn-ui/cn: a drop-in Tailwind class merger that replaces clsx and tailwind-merge

cn is a new engine for Tailwind class merging and conflict resolution. It replaces tailwind-merge and clsx. Same APIs. Full parity. And it is 30× faster.

1,510 stars15 forksTypeScriptMIT

At a glance

What is it?
cn is a zero-dependency Tailwind class merging engine from shadcn and aidenybai. It keeps the clsx and tailwind-merge APIs, claims full output parity, and ships a migration command, but it needs Tailwind v4 and Node 20 or newer.
Who is it for?
Adopt cn if you already run Tailwind CSS v4 and want one dependency instead of clsx plus tailwind-merge, especially if your components call cn(base, variant, condition && extra) in render loops. Do not adopt it on Tailwind v3, and do not adopt it if you rely on experimentalParseClassName, which the README lists as not supported.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 16, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem cn solves: two libraries doing one job

A Tailwind component usually needs two things at once. It has to join conditional class strings, and it has to resolve conflicts when a caller passes a className that overrides a base style. The common setup uses clsx for the joining and tailwind-merge for the conflict resolution, wrapped in a small cn helper. That works, but it means two dependencies, two bundles, and a merge pass over every class string on every render.

cn collapses both into one package. The README describes it as a new engine for Tailwind class merging and conflict resolution that replaces tailwind-merge and clsx, with the same APIs and full parity. The audience is anyone writing Tailwind components: React, Vue, Svelte, Solid, Astro, or plain server templates. The README states it works in any Tailwind CSS project and that shadcn/ui is not required, which matters because the package name suggests otherwise.

How the cn engine resolves conflicts and caches repeated calls

The mechanism is a table-driven parser plus a cache. Class strings are parsed into Tailwind class groups, and conflicts inside a group are resolved so the last one wins. That is the same job tailwind-merge does, and the README claims identical output for every input, verified with 356,000 differential tests.

The performance claim rests on caching rather than on a faster parser alone. The README says cn learns repeated call sequences, so a render loop's calls verify by identity and skip the work entirely. That is why the benchmark table shows a 30x figure for the shape cn(base, variant, condition && extra) with stable class strings, but only 1.9x for a cache hit and 3.0x for a cold render with many arbitrary values. The gap between those rows is the honest part of the story: the biggest wins come from repeated calls, not from first-time parsing.

The README also cites a corpus run: pnpm bench:corpus replays every cn() call harvested from 58 open source codebases, 144,265 calls, one isolated process per library and repository. The geometric mean across those repositories is 37x faster than clsx plus tailwind-merge. Those numbers come from the project's own benchmark harness, which the README invites you to run yourself.

Install cn and make a first call

The package installs from npm with no configuration step. Node 20 or newer is required, per the README's note about the CLI and the workspace engines field.

bash
npm i cn

After that, import cn and use it exactly where you would have used the old helper. It accepts strings, falsy values, and objects, so conditional joining and conflict resolution happen in one call.

tsx
import { cn } from "cn"

cn("px-2 py-1", isActive && "bg-blue-500", { "text-white": isActive })

In a component, the README's Button example passes a base string, a conditional variant, and the incoming className last so the caller's classes win.

tsx
import { cn } from "cn"

export function Button({ className, active, ...props }: React.ComponentProps<"button"> & { active?: boolean }) {
  return <button className={cn("rounded-md px-4 py-2 text-sm", active && "bg-primary", className)} {...props} />
}

If you already have a shadcn/ui project, the README gives a CLI migration, npx shadcn@latest migrate cn, or a manual edit. The manual route replaces the wrapper in lib/utils.ts with a re-export, then removes clsx and tailwind-merge from dependencies. If other packages still import those two, the README points to docs/aliasing.md so the bundle carries one implementation instead of three.

Where cn is the wrong tool

The clearest limitation is the Tailwind version. The README states cn supports Tailwind CSS v4, like tailwind-merge v3, and that on Tailwind v3 you should stay with tailwind-merge v2. If you are pinned to v3, this package is not a drop-in replacement no matter how the API looks.

The second limitation is class names that look like Tailwind utilities without being real ones. The README says a class such as text-2xs is treated as a Tailwind utility, and points to tailwind-merge's limitations page for the same guidance. If your design system invents utility-shaped names, the merger will make assumptions about them.

The third is build-time compilation. With cn build, dynamically constructed class names such as "p-" + size cannot be detected, the same rule Tailwind itself applies, and the README directs you to --safelist. Finally, experimentalParseClassName from tailwind-merge is listed as not supported. If your setup depends on it, the migration stalls there.

cn versus tailwind-merge plus clsx, and versus the lite entry point

The honest comparison is not cn against some unrelated library. It is cn against the pair it replaces. tailwind-merge plus clsx is the incumbent: two packages, a wrapper function you maintain yourself, and a merge pass on every call. cn keeps the same function names, twMerge and twJoin are exported identically from "cn", and extendTailwindMerge keeps its name but moves to "cn/config". The difference is packaging and caching, not semantics.

There is also an internal alternative worth knowing about. cn/lite exports clsx and a strings-only join, described as clsx/lite parity. If you only need conditional joining and never resolve Tailwind conflicts, the lite entry point is the smaller choice, and the full engine is unnecessary weight.

For custom themes, cn/config accepts the same { extend, override, prefix } shape under the same names. The README shows an extendTailwindMerge call being rewritten as createCn with an identical config object, and notes that fromTheme, validators, mergeConfigs, and defaultConfig are all exported from cn/config. Tailwind v4 prefixes are supported through createCn({ prefix: "tw" }).

Maintenance, licence, and what a migration costs you

The repository is not archived. Its last push was on 2026-09-12, and the most recent release listed is cn@0.3.0 on the same day, following cn@0.2.6 on 2026-09-06 and cn@0.2.5 on 2026-09-04. The version numbers are still in the 0.x range, which is worth weighing: the API is presented as stable and parity-complete, but a 0.x line leaves room for breaking changes that a 1.0 would not.

The licence is MIT, which permits commercial use and modification. That is the same licence tailwind-merge uses, so a migration does not change your legal position. This is not legal advice; check the LICENSE file in the repository for the exact terms.

The upgrade cost is mostly mechanical. The README's migration command rewrites the wrapper, and the manual diff is a handful of lines in lib/utils.ts. The risk sits in the edges: removing clsx and tailwind-merge from dependencies while other packages still import them, and the unsupported experimentalParseClassName export. The repository is a pnpm workspace with a changesets release flow, so version bumps are documented through changelogs rather than dropped silently.

Editorial conclusion

Adopt cn if you already run Tailwind CSS v4 and want one dependency instead of clsx plus tailwind-merge, especially if your components call cn(base, variant, condition && extra) in render loops. Do not adopt it on Tailwind v3, and do not adopt it if you rely on experimentalParseClassName, which the README lists as not supported. Verify two things before you rip out the old packages: that your Tailwind version is v4, and that nothing else in your tree still imports clsx or tailwind-merge without the aliasing documented in docs/aliasing.md.

Frequently asked questions

What does shadcn ui do?

shadcn/ui is the component collection this package comes from, and the README is explicit that cn works in any Tailwind CSS project and does not require shadcn/ui. cn itself is only the class merging helper: it joins conditional class strings and resolves Tailwind conflicts.

How do I install Shadcn UI?

For this package specifically, the README gives npm i cn, and existing shadcn/ui projects can run npx shadcn@latest migrate cn to swap the wrapper. The CLI needs Node 20 or newer.

What are the disadvantages of Shadcn UI?

For cn, the README lists the real constraints: it supports Tailwind CSS v4 only, so Tailwind v3 projects should stay on tailwind-merge v2; classes that merely look like Tailwind utilities are treated as utilities; cn build cannot detect dynamically constructed class names; and experimentalParseClassName is not supported.

Is shadcn ui free?

The cn repository is licensed under MIT, which allows commercial use and modification. The README does not describe any paid tier or hosted service for this package.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. shadcn-ui/cn on GitHub
Community notes

Community notes