
Security News
Socket Releases Free Certified Patches for Critical vm2 Sandbox Escape
A critical vm2 sandbox escape can allow untrusted JavaScript to break isolation and execute commands on the host Node.js process.
classname-variants
Advanced tools
Library to create type-safe components that render their class name based on a set of variants.
npm install classname-variants
// Core β vanilla DOM, no framework dependency
import { variants, classNames, tw } from "classname-variants";
// React β styled components, variantProps, type utilities
import { styled, variantProps, tw } from "classname-variants/react";
import type { VariantPropsOf } from "classname-variants/react";
// Preact β same API, accepts both `class` and `className`
import { styled, variantProps, tw } from "classname-variants/preact";
import type { VariantPropsOf } from "classname-variants/preact";
Here is an example that uses React and Tailwind CSS:
import { styled } from "classname-variants/react";
const Button = styled("button", {
variants: {
size: {
small: "text-xs",
large: "text-lg",
},
primary: {
true: "bg-teal-500 text-white",
},
},
});
function UsageExample() {
return <Button primary size="large" />;
}
While the library has been designed with tools like Tailwind in mind, it can be also used with custom classes or CSS modules:
import { styled } from "classname-variants/preact";
import styles from "./styles.module.css";
const Button = styled("button", {
variants: {
size: {
small: styles.small,
large: styles.large,
},
},
});
The core of the library is completely framework-agnostic:
import { variants } from "classname-variants";
const button = variants({
base: "rounded text-white",
variants: {
color: {
brand: "bg-sky-500",
accent: "bg-teal-500",
},
},
});
document.write(`
<button class="${button({ color: "accent" })}">
Click Me!
</button>
`);
You can add any number of variants by using the variants key.
{
variants: {
color: {
primary: "bg-teal",
secondary: "bg-indigo",
danger: "bg-red"
},
size: {
small: "text-sm",
medium: "text-md",
large: "text-lg",
}
}
}
Variants can be typed as boolean by using true / false as key:
{
variants: {
primary: {
true: "bg-teal-500",
},
},
}
<Button primary>Click Me!</Button>
The compoundVariants option can be used to apply class names based on a combination of other variants:
{
variants: {
color: {
neutral: "bg-gray-200",
accent: "bg-teal-400",
},
outlined: {
true: "border-2",
},
},
compoundVariants: [
{
variants: {
color: "accent",
outlined: true,
},
className: "border-teal-500",
},
],
}
If you define a variant it becomes a required prop unless you specify a default (or the variant is boolean). You can use the defaultVariants property to specify defaults:
{
variants: {
color: {
primary: "bg-teal-300",
secondary: "bg-teal-100"
},
},
defaultVariants: {
color: "secondary",
}
}
Use the base property to specify class names that should always be applied:
{
base: "text-black rounded-full px-2",
variants: {
// ...
}
}
Sometimes it can be useful to define styled components that don't have any variants, which can be done like this:
import { styled } from "classname-variants/react";
const Button = styled("button", "bg-transparent border p-2");
If your underlying element (or custom component) expects props that you want to
provide automatically, you can use the defaultProps option. All defaulted
props become optional in TypeScript β even when you later render the component
with a polymorphic as prop.
const Button = styled("button", {
base: "inline-flex items-center gap-2",
defaultProps: {
type: "button",
},
});
// `type` is optional but still overridable
<Button />;
<Button type="submit" />;
// Works together with `as`
<Button as="a" href="/docs" />;
When a variant mirrors an existing prop (such as disabled on a button), add
it to forwardProps so the resolved value is passed through to the rendered
element or custom component.
const Button = styled("button", {
variants: {
disabled: {
true: "cursor-not-allowed",
},
},
forwardProps: ["disabled"],
});
// Renders with both the class name and the DOM `disabled` prop applied.
<Button disabled />;
Styled components accept a className prop that gets merged with the variant output. This is useful for one-off overrides:
<Button className="mt-4" size="large">Submit</Button>
The Preact adapter accepts both class and className β use whichever you prefer:
<Button class="mt-4" size="large">Submit</Button>
All styled() components support refs via React.forwardRef:
const Input = styled("input", {
base: "border rounded px-2",
variants: { ... },
});
const ref = useRef<HTMLInputElement>(null);
<Input ref={ref} />;
variantProps()The lower-level variantProps() function lets you separate variant logic from rendering. This is useful for headless components or when you need more control:
import { variantProps } from "classname-variants/react";
const buttonProps = variantProps({
base: "rounded px-4 py-2",
variants: {
intent: {
primary: "bg-teal-500 text-white",
secondary: "bg-slate-200",
},
},
});
function Button(props) {
// Extracts variant props, returns { className, ...rest }
const { className, ...rest } = buttonProps(props);
return <button className={className} {...rest} />;
}
VariantPropsOf<T>Use this utility type to extract the variant props accepted by a variantProps function β helpful when building wrapper components:
import { variantProps, type VariantPropsOf } from "classname-variants/react";
const buttonProps = variantProps({ ... });
type ButtonProps = VariantPropsOf<typeof buttonProps>;
// { intent: "primary" | "secondary"; className?: string }
You can style any custom React/Preact component as long as they accept a className prop (or class in case of Preact).
function MyComponent(props) {
return <div {...props}>I'm a stylable custom component.</div>;
}
const MyStyledComponent = styled(MyComponent, {
base: "some-class",
variants: {
// ...
},
});
If you want to keep all the variants you have defined for a component but want to render a different HTML tag or a different custom component, you can use the as prop to do so:
import { styled } from "classname-variants/react";
const Button = styled("button", {
variants: {
//...
},
});
The component can then be rendered as button or as anchor or even as custom component exposed by some router:
<>
<Button>I'm a button</Button>
<Button as="a" href="/">
I'm a link!
</Button>
<Button as={Link} to="/">
I'm a styled Link component
</Button>
</>
The built-in strategy for combining multiple class names into one string is simple and straightforward:
(classes) => classes.filter(Boolean).join(" ");
If you want, you can use a custom strategy like tailwind-merge instead:
import { classNames } from "classname-variants";
import { twMerge } from "tailwind-merge";
classNames.combine = twMerge;
styled() creates ready-to-use React/Preact componentsstyled() API with polymorphic as prop, ref forwarding, and defaultPropsforwardProps maps variant values to DOM attributes (e.g. disabled)VariantProps extractionIf you're coming from cva: cva() maps to variants(), and VariantProps<typeof x> maps to VariantPropsOf<typeof x>.
In order to get auto-completion for the CSS classes themselves, you can use the Tailwind CSS IntelliSense plugin for VS Code. In order to make it recognize the strings inside your variants-config, you have to somehow mark them and configure the plugin accordingly.
One way of doing so is by using tagged template literals:
import { variants, tw } from "classname-variants";
const button = variants({
base: tw`px-5 py-2 text-white`,
variants: {
color: {
neutral: tw`bg-slate-500 hover:bg-slate-400`,
accent: tw`bg-teal-500 hover:bg-teal-400`,
},
},
});
You can then set the Tailwind CSS: Class Functions option to tw.
[!NOTE] The
twhelper function is just an alias forString.raw()which has the nice side effect backslashes are not treated as escape character in JSX.
In order to get type coverage even for your Tailwind classes, you can use a tool like tailwind-ts.
For comprehensive technical documentation optimized for LLMs, see llms.txt.
MIT
FAQs
Variant API for plain class names
The npm package classname-variants receives a total of 1,500 weekly downloads. As such, classname-variants popularity was classified as popular.
We found that classname-variants demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
A critical vm2 sandbox escape can allow untrusted JavaScript to break isolation and execute commands on the host Node.js process.

Research
Five malicious NuGet packages impersonate Chinese .NET libraries to deploy a stealer targeting browser credentials, crypto wallets, SSH keys, and local files.

Security News
pnpm 11 turns on a 1-day Minimum Release Age and blocks exotic subdeps by default, adding safeguards against fast-moving supply chain attacks.