A TypeScript utility for grouping Tailwind variant prefixes.
Avoid repeating hover:, focus:, dark: prefixes.
Visit the NPM package here - https://www.npmjs.com/package/tw-variant
npm install tw-variantimport { tv } from "tw-variant"
const buttonStyles = tv({
base: "px-4 py-2 rounded font-medium",
hover: "bg-blue-600 shadow-lg",
focus: "ring-2 ring-offset-2",
active: "scale-95",
groupHover: "opacity-90"
})
<button className={buttonStyles}>Click me</button>Group Tailwind classes by variant and return a single class string.
Parameters:
base(string, optional): Base classes applied to all states.[variant](string): Any Tailwind variant (hover, focus, dark, group-hover, etc.). Prefer camelCase keys likegroupHoverfor hyphenated variants so quotes are not required.
Returns:
- A string with all variant prefixes applied.
Note: tv has no runtime dependencies and returns a plain class string.
tv({
base: "px-4 py-2 rounded",
hover: "bg-blue-500 text-white shadow-lg",
focus: "ring-2 ring-offset-2 outline-none"
})
// Output:
// "px-4 py-2 rounded hover:bg-blue-500 hover:text-white hover:shadow-lg focus:ring-2 focus:ring-offset-2 focus:outline-none"Use tv when you want to keep Tailwind class names readable and avoid repeating variant prefixes across multiple classes.
className={tv({
base: "px-4 py-2 rounded",
hover: "bg-blue-500 shadow-lg",
focus: "ring-2 ring-offset-2"
})}This is especially useful for reusable component styles and design system tokens.
For extra conditional or dynamic classes, keep tv focused on variant grouping and compose it with clsx:
import clsx from "clsx";
const classVariants = tv({
base: "px-4 py-2 rounded",
hover: "bg-blue-500 shadow-lg"
});
className={clsx(
classVariants,
isDisabled && "opacity-50 cursor-not-allowed",
customClass
)}Install clsx if you need a lightweight utility for conditional class composition.
Works with Tailwind v1, v2, v3, and v4+.
Works in any framework:
- React / Next.js
- Vue / Nuxt
- Svelte / SvelteKit
- Solid.js
- Angular
- Vanilla JavaScript
- Node.js 14+
- Bun
- Deno
- Modern browsers (ESM)
- Single API — only
tv - Less repetition — group variant classes
- Clearer code — easier to read and maintain
- Reusable patterns — define variants once and reuse them
import { tv } from "tw-variant";
import clsx from "clsx";
<div className={clsx(
"p-4 rounded-lg border transition-all",
tv({
hover: "shadow-lg",
focus: "ring-2",
dark: "bg-gray-900 border-gray-700"
})
)} />// design-system/variants.ts
import { tv, type VariantMap } from "tw-variant"
export const cardHover: VariantMap = {
hover: "shadow-xl -translate-y-1 border-blue-300",
focus: "ring-2 ring-blue-400",
dark: "bg-gray-800"
}
// Then use anywhere
export const cardClasses = tv(cardHover)Since hv() and tv() generate classes at runtime, Tailwind's JIT scanner may not detect them during build time.
- Groups variant prefixes to reduce repetition
- Works with any Tailwind variant
- Composes naturally with other utilities
- Zero dependencies
- Full TypeScript support
- Handle conditional classes (use plain JavaScript or your own helper for that)
- Resolve class conflicts (use
tailwind-mergeor another conflict resolver if needed) - Validate class names (Tailwind doesn't either)
- Replace
cn()— it's additive only