better-variants
Version:
A lightweight utility for working with CSS classes and variants.
173 lines (125 loc) • 5.61 kB
Markdown
# better-variants
A lightweight utility for working with CSS classes and variants.
## Why does this exist?
If you're using Tailwind CSS, you’ve likely used `clsx`, `tailwind-merge`, or `class-variance-authority (cva)` as well. Possibly all three, given the popularity of shadcn/ui.
These tools have been serving me well, but I couldn't help thinking about simplifying them for a better DX and a leaner bundle size. That's the aim of `better-variants`: unify the strengths of `clsx`and `cva` with sensible constraints and streamlined API.
## Features
- **Conditional Class Name Composition**: Merge classes conditionally, similar to `clsx`, with support for strings, objects, booleans, and nullish values
- **Variant Class Management**: Define and apply variants (e.g., styles for different states or sizes) with a simple, type-safe API inspired by `cva`
- **Type Safety**: Use TypeScript generics to ensure props match your config, providing auto-completion and error checking.
- **Required Defaults**: Variants and sizes must include a `default` key, ensuring predictable fallbacks without optional props.
## Installation
```bash
npm install better-variants
# or
pnpm add better-variants
# or
yarn add better-variants
```
## Usage
### Merging Classes with `cx`
Use `cx` to combine classes conditionally:
```typescript
const classes = cx("bg-gray-200", { "text-white": true }, "px-4", null);
// -> "bg-gray-200 text-white px-4"
```
### Creating Variants with `createVariants`
Define a config object with `base`, `variant`, and `size` properties:
```typescript
const button = createVariants({
base: "inline-flex items-center justify-center rounded text-sm",
variant: {
default: "bg-gray-200 text-black",
primary: "bg-blue-500 text-white",
destructive: "bg-red-600 text-white",
},
size: {
default: "h-9 px-4",
sm: "h-8 px-3 text-xs",
},
});
```
Apply the variant function with optional props:
```typescript
// Uses defaults
const defaultClasses = button();
// -> "inline-flex items-center justify-center rounded text-sm bg-gray-200 text-black h-9 px-4"
// Custom variant and size
button({ variant: "primary", size: "sm" });
// -> "inline-flex items-center justify-center rounded text-sm bg-blue-500 text-white h-8 px-3 text-xs"
// Only custom variant (uses default size)
button({ variant: "primary" });
// -> "inline-flex items-center justify-center rounded text-sm bg-blue-500 text-white h-9 px-4"
// Only custom size (uses default variant)
button({ size: "sm" });
// -> "inline-flex items-center justify-center rounded text-sm bg-gray-200 text-black h-8 px-3 text-xs"
```
### Type-Safe Props
The `VariantProps` utility extracts the expected prop types from your config:
```typescript
import { type VariantProps } from "better-variants";
type ButtonProps = VariantProps<typeof button>;
// type ButtonProps = { variant?: "default" | "primary" | "destructive"; size?: "default" | "sm" }
```
### Button Component Example
```tsx
import { createVariants, type VariantProps } from "better-variants";
import { cn } from "@/lib/utils";
const buttonVariants = createVariants({
base: "inline-flex items-center justify-center rounded text-sm font-medium transition-colors focus:outline-none",
variant: {
default: "bg-gray-200 text-gray-900 hover:bg-gray-300",
primary: "bg-blue-500 text-white hover:bg-blue-600",
destructive: "bg-red-500 text-white hover:bg-red-600",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-8 px-3 text-sm",
},
});
type ButtonProps = React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>;
const Button = ({ className, variant, size, children, ...props }: ButtonProps) => {
return (
<button className={cn(buttonVariants({ variant, size }), className)} {...props}>
{children}
</button>
);
};
export { Button, buttonVariants };
```
Usage:
```tsx
<Button variant="primary" size="sm">Submit</Button>
```
## API Reference
### `cx(...inputs: ClassValue[])`
- **Parameters**: Variable number of `string`, `Record<string, boolean>`, `boolean`, `null`, `undefined`.
- **Returns**: A string of space-separated class names.
- **Purpose**: Merges classes, ignoring falsy or non-true values.
### `createVariants<T extends TConfig>(config: T)`
- **Parameters**:
- `config`: An object with `base`, `variant`, and `size` properties.
- `base`: Classes that will always be applied.
- `variant`: Variant-specific classes. Must include a 'default' which is used as a fallback.
- `size`: Size-specific classes. Must include a 'default' which is used as a fallback.
- **Returns**: A function that optionally takes `variant` and `size` props and returns a combined class string.
- **Purpose**: Generates a variant selection function with type-safe props.
### `VariantProps<T>`
- **Parameters**: A function returned by `createVariants`
- **Returns**: The inferred `variant` and `size` types
- **Purpose**: Type helper for component prop definitions
## Limitations
- ❌ No compound variants supported (e.g., `variant=primary` + `size=lg` → add special class)
- 🔒 Configuration is limited to `base`, `variant`, and `size` (no arbitrary keys for now)
## Contributing
Contributions are welcome! If you:
* Found a bug
* Have a feature request
* Want to improve the docs
* Or just feel like helping out
Feel free to open an issue or submit a PR.
Before contributing, please:
* Check open issues and discussions to avoid duplicates
* Keep PRs focused and minimal
* Follow existing coding and documentation style
Thanks for helping make `better-variants` better!