tailwind-variants
Typed Tailwind CSS variant API with slots, compound variants, and built-in class-conflict resolution
Repository Health
Technical Analysis
Tailwind Variants is a TypeScript-first variant API for Tailwind CSS that lets you define base styles, variants, compound variants, default variants, and component slots as a single typed configuration object. It layers built-in Tailwind class-conflict resolution on top of the variant system, so composing and overriding classes never produces duplicate or conflicting utility classes.
Framework-agnostic and dependency-light, it is used to build variant-driven design systems and component libraries — including HeroUI’s own component set — where a consistent, type-safe styling API matters across React, Vue, Svelte, or plain JS projects.
What You Get
- A typed
tv()API for defining base styles, variants, compound variants, default variants, and multi-slot components in one config object - Built-in Tailwind class-conflict resolution (via an internal tailwind-merge-compatible engine) on
tv,createTV,cn, andcnMerge - A
/liteentry point that drops conflict resolution for a smaller bundle when merging isn’t needed cx,cn, andcnMergeutility functions for concatenating and optionally merging class names outside oftv()- Full TypeScript inference for variant props, slot names, and compound-variant conditions
Common Use Cases
- Building a variant-driven design system where each component (button, card, input) exposes typed size/color/state props that map to Tailwind classes
- Building multi-part components with slots (e.g. a card with header/body/footer slots) that share variant state across all slots
- Replacing ad hoc
clsx/template-literal class concatenation with a typed, cacheable resolver that also resolves Tailwind class conflicts - Powering component libraries (like HeroUI) that need a consistent styling API across many components and consumers using different bundlers or frameworks
Under The Hood
Architecture
The package is layered: index.ts is a thin public API that wraps internal/tv.ts’s getTailwindVariants factory, which returns the tv/createTV functions. class-resolver.ts’s createClassResolver branches on a resolved.mode of "plain" | "variant" | "slots" into three specialized resolver closures (createPlainResolver, createVariantResolver, createSlotsResolver) that are built lazily on first invocation so constructing a tv() component stays cheap. resolve-options.ts eagerly parses the raw config into a ResolvedOptions descriptor and lazily compiles variants, compound variants, and compound slots on first call via compileResolvedOptions. cache.ts implements a bounded result cache (256-entry limit) keyed by a props fingerprint plus a compound-variants signature, with the very first invocation always skipping the cache to avoid caching cold-start artifacts. tw-merge.ts implements its own tailwind-merge-compatible engine (parse-class-name, class-group-utils, sort-modifiers) rather than depending on the tailwind-merge package at runtime for the default entry point.
Tech Stack
TypeScript 5.6 in strict mode with exactOptionalPropertyTypes enabled. Built via tsup (esbuild-backed) into dual ESM (dist/index.js) and CJS (dist/index.cjs) bundles with generated .d.ts/.d.cts type files, plus separate lite and utils entry points declared through package.json exports. tailwind-merge and tailwindcss are optional peer dependencies (peerDependenciesMeta.optional: true), so the package still works when Tailwind isn’t present at runtime. The repo uses a pnpm workspace with .nvmrc/packageManager pinning, Biome 2.5 for linting instead of ESLint/Prettier, commitlint plus husky to enforce Conventional Commits, and bumpp + conventional-changelog to automate releases.
Code Quality
15 Vitest test files (cn.test.ts, create-tv.test.ts, merge-engine.test.ts, merge-parity.test.ts, tv-compound-slots.test.ts, tv-extend*.test.ts, tv-slots.test.ts, tv-tailwind-merge.test.ts, and more) cover the resolver modes, extension and composition behavior, and merge parity against upstream tailwind-merge semantics. A separate __types__ directory holds compile-time type tests (tsc -p .config/tsconfig.type-tests.json) that assert the public API’s inferred types for the core, slot, variant, and metadata APIs. strict: true plus exactOptionalPropertyTypes catch a class of undefined-vs-missing bugs at compile time. CI (ci.yml) runs alongside a dedicated benchmark.yml workflow that tracks performance regressions via tinybench.
API Design
The public surface is deliberately small — tv, createTV, cn, cx, cnMerge, and defaultConfig — with tv() itself handling plain, variant, and slot-based components based on the shape of the config object passed in, so there’s no separate API to learn per mode. Slots and compound variants are expressed declaratively in the same config rather than requiring separate composition calls, and createTV mirrors tv exactly but pre-bound to custom config, keeping the mental model consistent. The project is documented at tailwind-variants.org with a runnable quick-start in the README and versioned migration guides (v1→v2, v2→v3) for breaking changes.