css-variants 2.1.1 โ†’ 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +1298 -219
  2. package/dist/cjs/cv.bench.d.ts +1 -0
  3. package/dist/cjs/cv.bench.js +207 -0
  4. package/dist/cjs/cv.bench.js.map +1 -0
  5. package/dist/cjs/cv.js +12 -10
  6. package/dist/cjs/cv.js.map +1 -1
  7. package/dist/cjs/scv.bench.d.ts +1 -0
  8. package/dist/cjs/scv.bench.js +409 -0
  9. package/dist/cjs/scv.bench.js.map +1 -0
  10. package/dist/cjs/scv.js +13 -11
  11. package/dist/cjs/scv.js.map +1 -1
  12. package/dist/cjs/ssv.bench.d.ts +1 -0
  13. package/dist/cjs/ssv.bench.js +506 -0
  14. package/dist/cjs/ssv.bench.js.map +1 -0
  15. package/dist/cjs/ssv.js +14 -12
  16. package/dist/cjs/ssv.js.map +1 -1
  17. package/dist/cjs/sv.bench.d.ts +1 -0
  18. package/dist/cjs/sv.bench.js +264 -0
  19. package/dist/cjs/sv.bench.js.map +1 -0
  20. package/dist/cjs/sv.js +16 -11
  21. package/dist/cjs/sv.js.map +1 -1
  22. package/dist/cjs/utils/merge-props.d.ts +1 -1
  23. package/dist/cjs/utils/merge-props.js +8 -6
  24. package/dist/cjs/utils/merge-props.js.map +1 -1
  25. package/dist/cjs/utils/types.d.ts +3 -1
  26. package/dist/esm/cv.bench.d.ts +1 -0
  27. package/dist/esm/cv.bench.js +205 -0
  28. package/dist/esm/cv.bench.js.map +1 -0
  29. package/dist/esm/cv.js +12 -10
  30. package/dist/esm/cv.js.map +1 -1
  31. package/dist/esm/scv.bench.d.ts +1 -0
  32. package/dist/esm/scv.bench.js +407 -0
  33. package/dist/esm/scv.bench.js.map +1 -0
  34. package/dist/esm/scv.js +13 -11
  35. package/dist/esm/scv.js.map +1 -1
  36. package/dist/esm/ssv.bench.d.ts +1 -0
  37. package/dist/esm/ssv.bench.js +504 -0
  38. package/dist/esm/ssv.bench.js.map +1 -0
  39. package/dist/esm/ssv.js +14 -12
  40. package/dist/esm/ssv.js.map +1 -1
  41. package/dist/esm/sv.bench.d.ts +1 -0
  42. package/dist/esm/sv.bench.js +262 -0
  43. package/dist/esm/sv.bench.js.map +1 -0
  44. package/dist/esm/sv.js +16 -11
  45. package/dist/esm/sv.js.map +1 -1
  46. package/dist/esm/utils/merge-props.d.ts +1 -1
  47. package/dist/esm/utils/merge-props.js +8 -6
  48. package/dist/esm/utils/merge-props.js.map +1 -1
  49. package/dist/esm/utils/types.d.ts +3 -1
  50. package/package.json +7 -3
package/README.md CHANGED
@@ -1,366 +1,1445 @@
1
- [![test](https://github.com/timphandev/css-variants/actions/workflows/ci.yml/badge.svg)](https://github.com/timphandev/css-variants/actions/workflows/ci.yml)
2
- [![license](https://img.shields.io/github/license/timphandev/css-variants)](https://github.com/timphandev/css-variants/blob/main/LICENSE)
3
- [![npm](https://img.shields.io/npm/dm/css-variants)](https://npmjs.com/package/css-variants)
4
- ![npm](https://img.shields.io/npm/v/css-variants)
1
+ # css-variants
2
+
3
+ > **Zero-dependency, type-safe CSS variant composition for modern JavaScript**
5
4
 
6
- # css-variants โ€” Compose class names & styles with variants
5
+ Build powerful, flexible component style systems with variants. Perfect for Tailwind CSS, vanilla CSS, or any CSS-in-JS solution.
7
6
 
8
- Lightweight helpers to compose class names and inline styles using "variants". Zero runtime deps, small bundle, and first-class TypeScript support.
7
+ [![test](https://github.com/timphandev/css-variants/actions/workflows/ci.yml/badge.svg)](https://github.com/timphandev/css-variants/actions/workflows/ci.yml)
8
+ [![npm version](https://img.shields.io/npm/v/css-variants.svg)](https://www.npmjs.com/package/css-variants)
9
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/css-variants)](https://bundlephobia.com/package/css-variants)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue)](https://www.typescriptlang.org/)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
12
 
10
- <p align="center">
11
- <img src="/.github/assets/logo.png" alt="css-variants" />
12
- </p>
13
+ ---
13
14
 
14
- ## Features
15
+ ## Why css-variants?
15
16
 
16
- ๐ŸŒฑ **Zero deps** โ€” No runtime dependencies; tiny bundle and minimal maintenance.
17
+ โšก **Tiny & Fast** โ€” Zero dependencies, ~1KB minified+gzipped
17
18
 
18
- ๐Ÿช **Tailwind-friendly** โ€” First-class compatibility with Tailwind via `tw-merge` (see "Tailwind Integration (tw-merge)"), so conflicting utilities are resolved predictably.
19
+ ๐Ÿ”’ **Type-Safe** โ€” First-class TypeScript support with complete type inference
19
20
 
20
- ๐Ÿ”’ **TypeScript-safe** โ€” Strong inference and mapped-type helpers keep variant props typed correctly.
21
+ ๐Ÿงฉ **Flexible** โ€” Works with Tailwind, CSS modules, vanilla CSS, or inline styles
21
22
 
22
- ๐Ÿงฉ **Variants & compound rules** โ€” Simple `variants` maps plus `compoundVariants` for combination rules (e.g., size + color).
23
+ ๐Ÿ‘จโ€๐Ÿ’ป **Developer-Friendly** โ€” Intuitive API inspired by CVA and Panda CSS
23
24
 
24
- ๐Ÿงญ **Slot support** โ€” `scv` / `ssv` manage multiple named slots with per-slot `base`, `variants`, and overrides.
25
+ ๐Ÿš€ **Production-Ready** โ€” Battle-tested, fully tested, dual CJS/ESM builds
25
26
 
26
- โš™๏ธ **Flexible resolver** โ€” Default `cx`, with an option to pass a custom `classNameResolver` (recommended: `twMerge(cx(...))`).
27
+ ---
27
28
 
28
- โšก **Performance & tree-shaking** โ€” Minimal runtime and tree-shakeable code paths for small bundles.
29
+ ## Table of Contents
29
30
 
30
- ๐Ÿงช **Developer ergonomics** โ€” Colocated `*.test.ts` (Vitest), clear build scripts (`yarn build`) and linting (`yarn lint`).
31
+ - [Quick Start](#quick-start)
32
+ - [Core Concepts](#core-concepts)
33
+ - [API Reference](#api-reference)
34
+ - [`cv` - Class Variants](#cv---class-variants)
35
+ - [`scv` - Slot Class Variants](#scv---slot-class-variants)
36
+ - [`sv` - Style Variants](#sv---style-variants)
37
+ - [`ssv` - Slot Style Variants](#ssv---slot-style-variants)
38
+ - [`cx` - Class Name Merger](#cx---class-name-merger)
39
+ - [Advanced Patterns](#advanced-patterns)
40
+ - [Framework Integration](#framework-integration)
41
+ - [Migration Guide](#migration-guide)
42
+ - [Performance](#performance)
31
43
 
32
- Use cases: design-system components, Tailwind + component libraries, SSR-friendly UI primitives.
44
+ ---
33
45
 
34
- ## Installation
46
+ ## Quick Start
35
47
 
36
- Install with your preferred package manager:
48
+ ### Installation
37
49
 
38
50
  ```bash
39
- # npm
40
51
  npm install css-variants
41
-
42
- # yarn
52
+ # or
43
53
  yarn add css-variants
44
-
45
- # pnpm
54
+ # or
46
55
  pnpm add css-variants
47
56
  ```
48
57
 
49
- TypeScript types are included. Import the package in ESM or CJS projects:
58
+ ### Your First Variant
59
+
60
+ ```typescript
61
+ import { cv } from 'css-variants'
62
+
63
+ const button = cv({
64
+ base: 'font-semibold rounded-lg transition-colors',
65
+ variants: {
66
+ color: {
67
+ primary: 'bg-blue-600 text-white hover:bg-blue-700',
68
+ secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
69
+ danger: 'bg-red-600 text-white hover:bg-red-700',
70
+ },
71
+ size: {
72
+ sm: 'px-3 py-1.5 text-sm',
73
+ md: 'px-4 py-2 text-base',
74
+ lg: 'px-6 py-3 text-lg',
75
+ },
76
+ },
77
+ defaultVariants: {
78
+ color: 'primary',
79
+ size: 'md',
80
+ },
81
+ })
82
+
83
+ // Usage
84
+ button() // => 'font-semibold rounded-lg transition-colors bg-blue-600 text-white hover:bg-blue-700 px-4 py-2 text-base'
85
+ button({ color: 'danger', size: 'lg' }) // => '... bg-red-600 text-white hover:bg-red-700 px-6 py-3 text-lg'
86
+ ```
50
87
 
51
- ```ts
52
- // ESM
53
- import { cv, scv, cx } from 'css-variants'
88
+ ### Framework Examples
54
89
 
55
- // CJS
56
- const { cv, scv, cx } = require('css-variants')
90
+ **React**
91
+ ```tsx
92
+ function Button({ color, size, children, ...props }) {
93
+ return (
94
+ <button className={button({ color, size })} {...props}>
95
+ {children}
96
+ </button>
97
+ )
98
+ }
99
+
100
+ <Button color="danger" size="lg">Delete Account</Button>
57
101
  ```
58
102
 
59
- ## Core Utilities
103
+ **Vue**
104
+ ```vue
105
+ <template>
106
+ <button :class="button({ color, size })">
107
+ <slot />
108
+ </button>
109
+ </template>
60
110
 
61
- Quick reference for the main exports. Each utility has full examples below.
111
+ <script setup>
112
+ import { cv } from 'css-variants'
62
113
 
63
- - ๐Ÿงฉ [`cv`](#cv---class-variants) โ€” Class Variants (single element)
64
- - Use to compose class names for one element. Supports `base`, `variants`, `compoundVariants`, and `defaultVariants`.
65
- - Quick: `const btn = cv({ base: 'btn', variants: { size: { sm: 'p-2', lg: 'p-4' } } })`
114
+ const props = defineProps(['color', 'size'])
115
+ const button = cv({ /* config */ })
116
+ </script>
117
+ ```
66
118
 
67
- - ๐ŸŽจ [`sv`](#sv---style-variants) โ€” Style Variants (single element)
68
- - Compose inline style objects similarly to `cv` but returning CSS props.
69
- - Quick: `const s = sv({ base: { display: 'flex' }, variants: { size: { sm: { gap: '4px' } } } })`
119
+ ---
70
120
 
71
- - ๐Ÿงฐ [`scv`](#scv-slot-class-variants) โ€” Slot Class Variants (multi-slot)
72
- - Manage class names across named slots (`slots: ['root','title']`) with per-slot `base`, `variants`, and `classNames` overrides.
73
- - Quick: `const card = scv({ slots: ['root','title'], base: { root: 'card' } })`
121
+ ## Core Concepts
74
122
 
75
- - ๐Ÿงพ [`ssv`](#ssv---slot-style-variants) โ€” Slot Style Variants (multi-slot styles)
76
- - Same as `scv` but composes inline style objects per slot.
123
+ ### Variants
77
124
 
78
- - โš™๏ธ [`cx`](#cx---class-merger) โ€” Class merger
79
- - Small, typed `clsx`-like utility used as the default `classNameResolver`.
80
- - Quick: `cx('a', { b: true }, ['c']) // => 'a b c'`
125
+ Variants are named groups of style options. Each variant has a set of possible values:
81
126
 
82
- ### [cv](./src/cv.ts) - Class Variants
83
- Compose class names for a single element. Config keys: `base`, `variants`, `defaultVariants`, `compoundVariants`, and optional `classNameResolver` (defaults to `cx`).
84
- `cv` returns a typed function you call with variant props (and optional `className`) to get the final class string.
127
+ ```typescript
128
+ const alert = cv({
129
+ variants: {
130
+ variant: {
131
+ info: 'bg-blue-100 text-blue-900',
132
+ warning: 'bg-yellow-100 text-yellow-900',
133
+ error: 'bg-red-100 text-red-900',
134
+ },
135
+ },
136
+ })
85
137
 
86
- ```ts
87
- import { cv } from 'css-variants'
138
+ alert({ variant: 'error' }) // => 'bg-red-100 text-red-900'
139
+ ```
140
+
141
+ ### Base Styles
142
+
143
+ Base styles are applied to **all** instances, regardless of variants:
144
+
145
+ ```typescript
146
+ const card = cv({
147
+ base: 'rounded-lg shadow-md overflow-hidden',
148
+ variants: {
149
+ padding: {
150
+ none: '',
151
+ sm: 'p-4',
152
+ md: 'p-6',
153
+ lg: 'p-8',
154
+ },
155
+ },
156
+ })
157
+
158
+ card({ padding: 'sm' }) // => 'rounded-lg shadow-md overflow-hidden p-4'
159
+ ```
160
+
161
+ ### Default Variants
162
+
163
+ Set default values for variants when no props are provided:
164
+
165
+ ```typescript
166
+ const input = cv({
167
+ variants: {
168
+ size: {
169
+ sm: 'text-sm px-2 py-1',
170
+ md: 'text-base px-3 py-2',
171
+ lg: 'text-lg px-4 py-3',
172
+ },
173
+ },
174
+ defaultVariants: {
175
+ size: 'md',
176
+ },
177
+ })
178
+
179
+ input() // => 'text-base px-3 py-2' (uses default)
180
+ input({ size: 'lg' }) // => 'text-lg px-4 py-3' (overrides default)
181
+ ```
88
182
 
183
+ ### Compound Variants
184
+
185
+ Apply styles when **multiple variants match simultaneously**:
186
+
187
+ ```typescript
89
188
  const button = cv({
90
- base: 'font-bold rounded-lg',
189
+ base: 'rounded font-medium',
91
190
  variants: {
92
191
  color: {
93
- primary: 'bg-blue-500 text-white',
94
- secondary: 'bg-gray-500 text-white'
192
+ primary: 'bg-blue-600 text-white',
193
+ secondary: 'bg-gray-200 text-gray-900',
95
194
  },
96
195
  size: {
97
- sm: 'text-sm px-2 py-1',
98
- lg: 'text-lg px-4 py-2'
99
- }
196
+ sm: 'px-3 py-1 text-sm',
197
+ lg: 'px-6 py-3 text-lg',
198
+ },
199
+ disabled: {
200
+ true: 'opacity-50 cursor-not-allowed',
201
+ false: 'cursor-pointer',
202
+ },
100
203
  },
101
204
  compoundVariants: [
205
+ // Large primary buttons get extra bold text
102
206
  {
103
207
  color: 'primary',
104
- size: 'lg',
105
- className: 'uppercase'
106
- }
208
+ size: 'lg',
209
+ className: 'font-bold shadow-lg',
210
+ },
211
+ // Disabled state removes hover effects
212
+ {
213
+ disabled: true,
214
+ className: 'pointer-events-none',
215
+ },
107
216
  ],
108
- defaultVariants: {
109
- color: 'primary',
110
- size: 'sm'
111
- }
112
217
  })
113
218
 
114
- // Usage
115
- button() // => 'font-bold rounded-lg bg-blue-500 text-white text-sm px-2 py-1'
219
+ button({ color: 'primary', size: 'lg' })
220
+ // => Includes 'font-bold shadow-lg' from compound variant
221
+ ```
116
222
 
117
- button({ size: 'lg' }) // => 'font-bold rounded-lg bg-blue-500 text-white text-lg px-4 py-2 uppercase'
223
+ **Array Matching** โ€” Match against multiple variant values:
118
224
 
119
- button({ size: 'lg', className: 'custom' }) // => 'font-bold rounded-lg bg-blue-500 text-white text-lg px-4 py-2 uppercase custom'
225
+ ```typescript
226
+ const text = cv({
227
+ variants: {
228
+ size: {
229
+ xs: 'text-xs',
230
+ sm: 'text-sm',
231
+ md: 'text-base',
232
+ lg: 'text-lg',
233
+ xl: 'text-xl',
234
+ },
235
+ weight: {
236
+ normal: 'font-normal',
237
+ bold: 'font-bold',
238
+ },
239
+ },
240
+ compoundVariants: [
241
+ {
242
+ size: ['lg', 'xl'], // Match multiple sizes
243
+ weight: 'bold',
244
+ className: 'tracking-tight', // Apply tighter letter spacing to large bold text
245
+ },
246
+ ],
247
+ })
120
248
  ```
121
249
 
122
- ### [sv](./src/sv.ts) - Style Variants
123
-
124
- Compose inline style objects for a single element. Config keys: `base`, `variants`, `defaultVariants`, and `compoundVariants`.
125
- `sv` returns a typed function that accepts variant props and an optional `style` object which is shallow-merged into the result.
250
+ ### Boolean Variants
126
251
 
127
- ```ts
128
- import { sv } from 'css-variants'
252
+ Boolean variants use string keys `'true'` and `'false'`, but accept actual booleans in props:
129
253
 
130
- const button = sv({
131
- base: {
132
- fontWeight: 'bold',
133
- borderRadius: '8px'
254
+ ```typescript
255
+ const checkbox = cv({
256
+ base: 'w-4 h-4 rounded border',
257
+ variants: {
258
+ checked: {
259
+ true: 'bg-blue-600 border-blue-600',
260
+ false: 'bg-white border-gray-300',
261
+ },
262
+ disabled: {
263
+ true: 'opacity-50 cursor-not-allowed',
264
+ false: 'cursor-pointer',
265
+ },
134
266
  },
267
+ })
268
+
269
+ // TypeScript allows boolean props
270
+ checkbox({ checked: true, disabled: false })
271
+ // => 'w-4 h-4 rounded border bg-blue-600 border-blue-600 cursor-pointer'
272
+ ```
273
+
274
+ ### Runtime Class Overrides
275
+
276
+ Override or extend classes at runtime:
277
+
278
+ ```typescript
279
+ const button = cv({
280
+ base: 'px-4 py-2 rounded',
135
281
  variants: {
136
282
  color: {
137
- primary: {
138
- backgroundColor: 'blue',
139
- color: 'white'
140
- },
141
- secondary: {
142
- backgroundColor: 'gray',
143
- color: 'white'
144
- }
145
- }
146
- }
283
+ primary: 'bg-blue-600 text-white',
284
+ },
285
+ },
147
286
  })
148
287
 
149
- // Usage
150
- button({ color: 'primary' })
151
- // => { fontWeight: 'bold', borderRadius: '8px', backgroundColor: 'blue', color: 'white' }
288
+ button({ color: 'primary', className: 'mt-4 w-full' })
289
+ // => 'px-4 py-2 rounded bg-blue-600 text-white mt-4 w-full'
290
+ ```
291
+
292
+ ---
293
+
294
+ ## API Reference
295
+
296
+ ### `cv` - Class Variants
297
+
298
+ Create variants for **single-element components** using CSS class names.
299
+
300
+ #### Type Signature
301
+
302
+ ```typescript
303
+ function cv<T extends ClassVariantRecord | undefined>(
304
+ config: ClassVariantDefinition<T>
305
+ ): ClassVariantFn<T>
306
+
307
+ interface ClassVariantDefinition<T> {
308
+ base?: ClassValue
309
+ variants?: T
310
+ compoundVariants?: (ObjectKeyArrayPicker<T> & { className: ClassValue })[]
311
+ defaultVariants?: ObjectKeyPicker<T>
312
+ classNameResolver?: typeof cx
313
+ }
314
+ ```
315
+
316
+ #### Parameters
317
+
318
+ | Parameter | Type | Description |
319
+ |-----------|------|-------------|
320
+ | `base` | `ClassValue` | Base classes applied to all instances |
321
+ | `variants` | `Record<string, Record<string, ClassValue>>` | Variant definitions |
322
+ | `compoundVariants` | `Array` | Conditional styles when multiple variants match |
323
+ | `defaultVariants` | `Object` | Default variant selections |
324
+ | `classNameResolver` | `Function` | Custom class merger (default: `cx`) |
325
+
326
+ #### Examples
327
+
328
+ **Basic Variant**
329
+
330
+ ```typescript
331
+ const badge = cv({
332
+ base: 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
333
+ variants: {
334
+ variant: {
335
+ default: 'bg-gray-100 text-gray-800',
336
+ success: 'bg-green-100 text-green-800',
337
+ warning: 'bg-yellow-100 text-yellow-800',
338
+ error: 'bg-red-100 text-red-800',
339
+ },
340
+ },
341
+ })
342
+ ```
343
+
344
+ **Complex Variant with Compound Rules**
345
+
346
+ ```typescript
347
+ const toast = cv({
348
+ base: 'rounded-lg p-4 shadow-lg transition-all',
349
+ variants: {
350
+ variant: {
351
+ info: 'bg-blue-50 text-blue-900 border-blue-200',
352
+ success: 'bg-green-50 text-green-900 border-green-200',
353
+ error: 'bg-red-50 text-red-900 border-red-200',
354
+ },
355
+ position: {
356
+ 'top-right': 'top-4 right-4',
357
+ 'bottom-right': 'bottom-4 right-4',
358
+ 'top-left': 'top-4 left-4',
359
+ 'bottom-left': 'bottom-4 left-4',
360
+ },
361
+ dismissible: {
362
+ true: 'pr-10',
363
+ false: '',
364
+ },
365
+ },
366
+ compoundVariants: [
367
+ {
368
+ variant: 'error',
369
+ dismissible: true,
370
+ className: 'border-l-4 border-l-red-600',
371
+ },
372
+ ],
373
+ defaultVariants: {
374
+ variant: 'info',
375
+ position: 'top-right',
376
+ dismissible: false,
377
+ },
378
+ })
379
+ ```
380
+
381
+ **Array and Object ClassValues**
152
382
 
153
- button({
154
- color: 'secondary',
155
- style: { padding: '4px' },
383
+ ```typescript
384
+ const container = cv({
385
+ base: ['max-w-7xl', 'mx-auto', { 'px-4': true, 'sm:px-6': true }],
386
+ variants: {
387
+ spacing: {
388
+ tight: ['py-8', 'gap-4'],
389
+ normal: ['py-12', 'gap-6'],
390
+ loose: ['py-16', 'gap-8'],
391
+ },
392
+ },
156
393
  })
157
- // => { fontWeight: 'bold', borderRadius: '8px', backgroundColor: 'gray', color: 'white', padding: '4px' }
158
394
  ```
159
395
 
160
- ### [scv](./src/scv.ts) - Slot Class Variants
396
+ ---
161
397
 
162
- Compose and merge class names across named slots.
163
- `scv` accepts `slots` plus per-slot `base`, `variants`, `compoundVariants`,
164
- and runtime `classNames` overrides, and returns an object mapping each slot to
165
- its final merged class string. Ideal for components with multiple sub-elements
166
- (for example: `root`, `title`, `content`).
398
+ ### `scv` - Slot Class Variants
167
399
 
168
- ```ts
169
- import { scv } from 'css-variants'
400
+ Create variants for **multi-element components** using CSS class names. Perfect for complex UI components like cards, modals, or navigation menus.
401
+
402
+ #### Type Signature
403
+
404
+ ```typescript
405
+ function scv<S extends string, T extends SlotClassVariantRecord<S> | undefined>(
406
+ config: SlotClassVariantDefinition<S, T>
407
+ ): SlotClassVariantFn<S, T>
408
+
409
+ interface SlotClassVariantDefinition<S, T> {
410
+ slots: S[]
411
+ base?: PartialRecord<S, ClassValue>
412
+ variants?: T
413
+ compoundVariants?: (ObjectKeyArrayPicker<T> & { classNames: PartialRecord<S, ClassValue> })[]
414
+ defaultVariants?: ObjectKeyPicker<T>
415
+ classNameResolver?: typeof cx
416
+ }
417
+ ```
418
+
419
+ #### Examples
170
420
 
421
+ **Card Component**
422
+
423
+ ```typescript
171
424
  const card = scv({
172
- slots: ['root', 'title', 'content'],
425
+ slots: ['root', 'header', 'title', 'description', 'content', 'footer'],
173
426
  base: {
174
- root: 'rounded-lg shadow',
175
- title: 'text-xl font-bold',
176
- content: 'mt-2'
427
+ root: 'rounded-lg border bg-white shadow-sm',
428
+ header: 'border-b p-6',
429
+ title: 'text-2xl font-semibold',
430
+ description: 'text-sm text-gray-500 mt-1',
431
+ content: 'p-6',
432
+ footer: 'border-t bg-gray-50 px-6 py-3',
177
433
  },
178
434
  variants: {
179
- size: {
435
+ variant: {
436
+ default: {
437
+ root: 'border-gray-200',
438
+ },
439
+ primary: {
440
+ root: 'border-blue-200',
441
+ title: 'text-blue-900',
442
+ },
443
+ danger: {
444
+ root: 'border-red-200 bg-red-50',
445
+ title: 'text-red-900',
446
+ },
447
+ },
448
+ padding: {
449
+ none: {
450
+ content: 'p-0',
451
+ header: 'p-0',
452
+ footer: 'p-0',
453
+ },
180
454
  sm: {
181
- root: 'p-4',
182
- title: 'text-base'
455
+ content: 'p-4',
456
+ header: 'p-4',
457
+ footer: 'px-4 py-2',
183
458
  },
184
459
  lg: {
185
- root: 'p-6',
186
- title: 'text-2xl'
187
- }
188
- }
189
- }
460
+ content: 'p-8',
461
+ header: 'p-8',
462
+ footer: 'px-8 py-4',
463
+ },
464
+ },
465
+ },
466
+ defaultVariants: {
467
+ variant: 'default',
468
+ },
190
469
  })
191
470
 
192
- // Usage
193
- card({ size: 'sm' })
471
+ const classes = card({ variant: 'primary' })
194
472
  // => {
195
- // root: 'rounded-lg shadow p-4',
196
- // title: 'text-xl font-bold text-base',
197
- // content: 'mt-2'
473
+ // root: 'rounded-lg border bg-white shadow-sm border-blue-200',
474
+ // header: 'border-b p-6',
475
+ // title: 'text-2xl font-semibold text-blue-900',
476
+ // description: 'text-sm text-gray-500 mt-1',
477
+ // content: 'p-6',
478
+ // footer: 'border-t bg-gray-50 px-6 py-3'
198
479
  // }
480
+ ```
199
481
 
200
- card({
201
- size: 'lg',
202
- classNames: {
203
- content: 'custom',
482
+ **React Usage**
483
+
484
+ ```tsx
485
+ function Card({ variant, padding, children }) {
486
+ const classes = card({ variant, padding })
487
+
488
+ return (
489
+ <div className={classes.root}>
490
+ <div className={classes.header}>
491
+ <h3 className={classes.title}>Card Title</h3>
492
+ <p className={classes.description}>Card description</p>
493
+ </div>
494
+ <div className={classes.content}>{children}</div>
495
+ <div className={classes.footer}>Footer content</div>
496
+ </div>
497
+ )
498
+ }
499
+ ```
500
+
501
+ **Modal Component**
502
+
503
+ ```typescript
504
+ const modal = scv({
505
+ slots: ['overlay', 'container', 'content', 'header', 'body', 'footer', 'closeButton'],
506
+ base: {
507
+ overlay: 'fixed inset-0 bg-black/50 flex items-center justify-center',
508
+ container: 'relative bg-white rounded-lg shadow-xl',
509
+ content: 'flex flex-col',
510
+ header: 'flex items-center justify-between px-6 py-4 border-b',
511
+ body: 'px-6 py-4',
512
+ footer: 'flex justify-end gap-2 px-6 py-4 border-t bg-gray-50',
513
+ closeButton: 'text-gray-400 hover:text-gray-600',
514
+ },
515
+ variants: {
516
+ size: {
517
+ sm: { container: 'max-w-md' },
518
+ md: { container: 'max-w-lg' },
519
+ lg: { container: 'max-w-2xl' },
520
+ xl: { container: 'max-w-4xl' },
521
+ full: { container: 'max-w-full mx-4' },
522
+ },
523
+ centered: {
524
+ true: { overlay: 'items-center justify-center' },
525
+ false: { overlay: 'items-start justify-center pt-16' },
526
+ },
527
+ },
528
+ defaultVariants: {
529
+ size: 'md',
530
+ centered: true,
204
531
  },
205
532
  })
206
- // => {
207
- // root: 'rounded-lg shadow p-6',
208
- // title: 'text-xl font-bold text-2xl',
209
- // content: 'mt-2 custom'
210
- // }
211
533
  ```
212
534
 
213
- ### [ssv](./src/ssv.ts) - Slot Style Variants
535
+ **Slot-Specific Overrides**
214
536
 
215
- Compose and merge inline style objects across named slots.
216
- `ssv` accepts `slots` plus per-slot `base`, `variants`, `compoundVariants`,
217
- and runtime `styles` overrides, and returns an object mapping each slot to
218
- its final merged style. Useful for components with multiple styled
219
- sub-elements (for example: `root`, `title`, `content`).
537
+ ```typescript
538
+ const classes = card({
539
+ variant: 'primary',
540
+ classNames: {
541
+ root: 'max-w-2xl mx-auto', // Add additional classes to root
542
+ title: 'text-3xl', // Override title size
543
+ footer: 'flex justify-between', // Change footer layout
544
+ },
545
+ })
546
+ ```
220
547
 
221
- ```ts
222
- import { ssv } from 'css-variants'
548
+ **Compound Variants with Slots**
223
549
 
224
- const card = ssv({
225
- slots: ['root', 'title'],
550
+ ```typescript
551
+ const button = scv({
552
+ slots: ['root', 'icon', 'label'],
226
553
  base: {
227
- root: { padding: '1rem' },
228
- title: { fontWeight: 'bold' }
554
+ root: 'inline-flex items-center gap-2 rounded font-medium',
555
+ icon: 'w-5 h-5',
556
+ label: '',
229
557
  },
230
558
  variants: {
231
559
  size: {
232
560
  sm: {
233
- root: { maxWidth: '300px' },
234
- title: { fontSize: '14px' }
561
+ root: 'px-3 py-1.5 text-sm',
562
+ icon: 'w-4 h-4',
235
563
  },
236
564
  lg: {
237
- root: { maxWidth: '600px' },
238
- title: { fontSize: '18px' }
239
- }
240
- }
241
- }
565
+ root: 'px-6 py-3 text-lg',
566
+ icon: 'w-6 h-6',
567
+ },
568
+ },
569
+ color: {
570
+ primary: {
571
+ root: 'bg-blue-600 text-white',
572
+ },
573
+ danger: {
574
+ root: 'bg-red-600 text-white',
575
+ },
576
+ },
577
+ },
578
+ compoundVariants: [
579
+ {
580
+ size: 'lg',
581
+ color: 'primary',
582
+ classNames: {
583
+ root: 'shadow-lg',
584
+ label: 'font-bold',
585
+ },
586
+ },
587
+ ],
242
588
  })
589
+ ```
243
590
 
244
- // Usage
245
- card({ size: 'sm' })
246
- // => {
247
- // root: { padding: '1rem', maxWidth: '300px' },
248
- // title: { fontWeight: 'bold', fontSize: '14px' }
249
- // }
591
+ ---
250
592
 
251
- card({
252
- size: 'lg',
253
- styles: {
254
- title: {
255
- color: 'red',
593
+ ### `sv` - Style Variants
594
+
595
+ Create variants for **inline CSS styles** (React's `style` prop, Vue's `:style`, etc.).
596
+
597
+ #### Type Signature
598
+
599
+ ```typescript
600
+ function sv<T extends StyleVariantRecord | undefined>(
601
+ config: StyleVariantDefinition<T>
602
+ ): StyleVariantFn<T>
603
+
604
+ interface StyleVariantDefinition<T> {
605
+ base?: CssProperties
606
+ variants?: T
607
+ compoundVariants?: (ObjectKeyArrayPicker<T> & { style: CssProperties })[]
608
+ defaultVariants?: ObjectKeyPicker<T>
609
+ }
610
+
611
+ type CssProperties = Properties<string | number> & {
612
+ [key: `--${string}`]: string | number // CSS custom properties
613
+ }
614
+ ```
615
+
616
+ #### Examples
617
+
618
+ **Basic Style Variant**
619
+
620
+ ```typescript
621
+ const box = sv({
622
+ base: {
623
+ display: 'flex',
624
+ borderRadius: '8px',
625
+ },
626
+ variants: {
627
+ size: {
628
+ sm: { padding: '8px', fontSize: '14px' },
629
+ md: { padding: '16px', fontSize: '16px' },
630
+ lg: { padding: '24px', fontSize: '18px' },
631
+ },
632
+ color: {
633
+ gray: { backgroundColor: '#f3f4f6', color: '#1f2937' },
634
+ blue: { backgroundColor: '#dbeafe', color: '#1e40af' },
635
+ red: { backgroundColor: '#fee2e2', color: '#991b1b' },
636
+ },
637
+ },
638
+ defaultVariants: {
639
+ size: 'md',
640
+ color: 'gray',
641
+ },
642
+ })
643
+
644
+ box({ size: 'lg', color: 'blue' })
645
+ // => { display: 'flex', borderRadius: '8px', padding: '24px', fontSize: '18px', backgroundColor: '#dbeafe', color: '#1e40af' }
646
+ ```
647
+
648
+ **CSS Custom Properties (CSS Variables)**
649
+
650
+ ```typescript
651
+ const theme = sv({
652
+ base: {
653
+ '--spacing-unit': '8px',
654
+ '--border-radius': '4px',
655
+ },
656
+ variants: {
657
+ theme: {
658
+ light: {
659
+ '--color-bg': '#ffffff',
660
+ '--color-text': '#000000',
661
+ },
662
+ dark: {
663
+ '--color-bg': '#1a1a1a',
664
+ '--color-text': '#ffffff',
665
+ },
666
+ },
667
+ },
668
+ })
669
+
670
+ // React usage
671
+ <div style={theme({ theme: 'dark' })}>
672
+ <p style={{ color: 'var(--color-text)' }}>Dark mode text</p>
673
+ </div>
674
+ ```
675
+
676
+ **Compound Style Variants**
677
+
678
+ ```typescript
679
+ const progressBar = sv({
680
+ base: {
681
+ width: '100%',
682
+ height: '8px',
683
+ borderRadius: '9999px',
684
+ overflow: 'hidden',
685
+ },
686
+ variants: {
687
+ variant: {
688
+ default: { backgroundColor: '#e5e7eb' },
689
+ success: { backgroundColor: '#d1fae5' },
690
+ error: { backgroundColor: '#fee2e2' },
691
+ },
692
+ animated: {
693
+ true: { transition: 'all 0.3s ease' },
694
+ false: {},
695
+ },
696
+ },
697
+ compoundVariants: [
698
+ {
699
+ variant: 'success',
700
+ animated: true,
701
+ style: {
702
+ boxShadow: '0 0 0 2px rgba(16, 185, 129, 0.2)',
703
+ },
704
+ },
705
+ ],
706
+ })
707
+ ```
708
+
709
+ **Runtime Style Overrides**
710
+
711
+ ```typescript
712
+ const card = sv({
713
+ base: { padding: '16px', borderRadius: '8px' },
714
+ })
715
+
716
+ card({ style: { marginTop: '24px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' } })
717
+ // => { padding: '16px', borderRadius: '8px', marginTop: '24px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }
718
+ ```
719
+
720
+ ---
721
+
722
+ ### `ssv` - Slot Style Variants
723
+
724
+ Create variants for **multi-element components** using inline CSS styles.
725
+
726
+ #### Type Signature
727
+
728
+ ```typescript
729
+ function ssv<S extends string, T extends SlotStyleVariantRecord<S> | undefined>(
730
+ config: SlotStyleVariantDefinition<S, T>
731
+ ): SlotStyleVariantFn<S, T>
732
+
733
+ interface SlotStyleVariantDefinition<S, T> {
734
+ slots: S[]
735
+ base?: PartialRecord<S, CssProperties>
736
+ variants?: T
737
+ compoundVariants?: (ObjectKeyArrayPicker<T> & { styles: PartialRecord<S, CssProperties> })[]
738
+ defaultVariants?: ObjectKeyPicker<T>
739
+ }
740
+ ```
741
+
742
+ #### Examples
743
+
744
+ **Tooltip Component**
745
+
746
+ ```typescript
747
+ const tooltip = ssv({
748
+ slots: ['container', 'arrow', 'content'],
749
+ base: {
750
+ container: {
751
+ position: 'relative',
752
+ display: 'inline-block',
753
+ },
754
+ arrow: {
755
+ position: 'absolute',
756
+ width: 0,
757
+ height: 0,
758
+ borderStyle: 'solid',
256
759
  },
760
+ content: {
761
+ position: 'absolute',
762
+ padding: '8px 12px',
763
+ borderRadius: '6px',
764
+ fontSize: '14px',
765
+ whiteSpace: 'nowrap',
766
+ zIndex: 1000,
767
+ },
768
+ },
769
+ variants: {
770
+ placement: {
771
+ top: {
772
+ content: { bottom: '100%', left: '50%', transform: 'translateX(-50%)' },
773
+ arrow: {
774
+ top: '100%',
775
+ left: '50%',
776
+ transform: 'translateX(-50%)',
777
+ borderWidth: '6px 6px 0',
778
+ borderColor: '#1f2937 transparent transparent',
779
+ },
780
+ },
781
+ bottom: {
782
+ content: { top: '100%', left: '50%', transform: 'translateX(-50%)' },
783
+ arrow: {
784
+ bottom: '100%',
785
+ left: '50%',
786
+ transform: 'translateX(-50%)',
787
+ borderWidth: '0 6px 6px',
788
+ borderColor: 'transparent transparent #1f2937',
789
+ },
790
+ },
791
+ left: {
792
+ content: { right: '100%', top: '50%', transform: 'translateY(-50%)' },
793
+ arrow: {
794
+ left: '100%',
795
+ top: '50%',
796
+ transform: 'translateY(-50%)',
797
+ borderWidth: '6px 0 6px 6px',
798
+ borderColor: 'transparent transparent transparent #1f2937',
799
+ },
800
+ },
801
+ right: {
802
+ content: { left: '100%', top: '50%', transform: 'translateY(-50%)' },
803
+ arrow: {
804
+ right: '100%',
805
+ top: '50%',
806
+ transform: 'translateY(-50%)',
807
+ borderWidth: '6px 6px 6px 0',
808
+ borderColor: 'transparent #1f2937 transparent transparent',
809
+ },
810
+ },
811
+ },
812
+ variant: {
813
+ dark: {
814
+ content: {
815
+ backgroundColor: '#1f2937',
816
+ color: '#ffffff',
817
+ },
818
+ },
819
+ light: {
820
+ content: {
821
+ backgroundColor: '#f9fafb',
822
+ color: '#1f2937',
823
+ border: '1px solid #e5e7eb',
824
+ },
825
+ },
826
+ },
827
+ },
828
+ defaultVariants: {
829
+ placement: 'top',
830
+ variant: 'dark',
257
831
  },
258
832
  })
833
+
834
+ const styles = tooltip({ placement: 'bottom', variant: 'light' })
259
835
  // => {
260
- // root: { padding: '1rem', maxWidth: '600px' },
261
- // title: { fontWeight: 'bold', fontSize: '18px', color: 'red' }
836
+ // container: { position: 'relative', display: 'inline-block' },
837
+ // arrow: { ... },
838
+ // content: { backgroundColor: '#f9fafb', color: '#1f2937', ... }
262
839
  // }
263
840
  ```
264
841
 
265
- ### [cx](./src/cx.ts) - Class Merger
842
+ **Split Pane Component**
843
+
844
+ ```typescript
845
+ const splitPane = ssv({
846
+ slots: ['container', 'leftPane', 'divider', 'rightPane'],
847
+ base: {
848
+ container: {
849
+ display: 'flex',
850
+ width: '100%',
851
+ height: '100%',
852
+ },
853
+ leftPane: {
854
+ overflow: 'auto',
855
+ },
856
+ divider: {
857
+ cursor: 'col-resize',
858
+ backgroundColor: '#e5e7eb',
859
+ },
860
+ rightPane: {
861
+ flex: 1,
862
+ overflow: 'auto',
863
+ },
864
+ },
865
+ variants: {
866
+ orientation: {
867
+ horizontal: {
868
+ container: { flexDirection: 'row' },
869
+ divider: { width: '4px' },
870
+ },
871
+ vertical: {
872
+ container: { flexDirection: 'column' },
873
+ divider: { height: '4px', cursor: 'row-resize' },
874
+ },
875
+ },
876
+ leftPaneSize: {
877
+ sm: { leftPane: { width: '200px' } },
878
+ md: { leftPane: { width: '300px' } },
879
+ lg: { leftPane: { width: '400px' } },
880
+ },
881
+ },
882
+ compoundVariants: [
883
+ {
884
+ orientation: 'vertical',
885
+ leftPaneSize: ['sm', 'md', 'lg'],
886
+ styles: {
887
+ leftPane: { width: 'auto', height: '200px' },
888
+ },
889
+ },
890
+ ],
891
+ defaultVariants: {
892
+ orientation: 'horizontal',
893
+ leftPaneSize: 'md',
894
+ },
895
+ })
896
+ ```
897
+
898
+ **Runtime Style Overrides**
266
899
 
267
- Similar to `clsx/classnames` but with better TypeScript support.
900
+ ```typescript
901
+ const styles = tooltip({
902
+ placement: 'top',
903
+ styles: {
904
+ content: { maxWidth: '300px', whiteSpace: 'normal' }, // Override content styles
905
+ arrow: { display: 'none' }, // Hide arrow
906
+ },
907
+ })
908
+ ```
268
909
 
269
- ```tsx
910
+ ---
911
+
912
+ ### `cx` - Class Name Merger
913
+
914
+ A lightweight utility for merging class names. Supports strings, arrays, objects, and nested combinations.
915
+
916
+ #### Type Signature
917
+
918
+ ```typescript
919
+ function cx(...args: ClassValue[]): string
920
+
921
+ type ClassValue =
922
+ | string
923
+ | number
924
+ | bigint
925
+ | boolean
926
+ | null
927
+ | undefined
928
+ | ClassDictionary
929
+ | ClassValue[]
930
+
931
+ type ClassDictionary = Record<string, unknown>
932
+ ```
933
+
934
+ #### Examples
935
+
936
+ **Basic Usage**
937
+
938
+ ```typescript
270
939
  import { cx } from 'css-variants'
271
940
 
272
- // Basic usage
273
941
  cx('foo', 'bar') // => 'foo bar'
942
+ cx('foo', null, 'bar', undefined, 'baz') // => 'foo bar baz'
943
+ cx('foo', false && 'bar', 'baz') // => 'foo baz'
944
+ ```
274
945
 
275
- // With conditions
276
- cx('foo', {
277
- 'bar': true,
278
- 'baz': false
279
- }) // => 'foo bar'
946
+ **Object Syntax**
947
+
948
+ ```typescript
949
+ cx({ foo: true, bar: false, baz: true }) // => 'foo baz'
950
+ cx('base', { active: isActive, disabled: isDisabled })
951
+ ```
952
+
953
+ **Array Syntax**
954
+
955
+ ```typescript
956
+ cx(['foo', 'bar']) // => 'foo bar'
957
+ cx(['foo', null, 'bar']) // => 'foo bar'
958
+ ```
280
959
 
281
- // With arrays
282
- cx('foo', ['bar', 'baz']) // => 'foo bar baz'
960
+ **Mixed Syntax**
961
+
962
+ ```typescript
963
+ cx(
964
+ 'base-class',
965
+ ['array-class-1', 'array-class-2'],
966
+ { conditional: true, ignored: false },
967
+ condition && 'conditional-class',
968
+ 42,
969
+ null,
970
+ undefined
971
+ ) // => 'base-class array-class-1 array-class-2 conditional conditional-class 42'
972
+ ```
283
973
 
284
- // With nested structures
285
- cx('foo', {
286
- bar: true,
287
- baz: [
288
- 'qux',
289
- { quux: true }
290
- ]
291
- }) // => 'foo bar qux quux'
974
+ **React Example**
292
975
 
293
- // With falsy values (they're ignored)
294
- cx('foo', null, undefined, false, 0, '') // => 'foo'
976
+ ```tsx
977
+ function Component({ isActive, isDisabled, className }) {
978
+ return (
979
+ <div className={cx(
980
+ 'base-class',
981
+ isActive && 'active',
982
+ isDisabled && 'disabled',
983
+ className
984
+ )}>
985
+ Content
986
+ </div>
987
+ )
988
+ }
295
989
  ```
296
990
 
297
- ## Tailwind Integration (tw-merge)
991
+ ---
992
+
993
+ ## Advanced Patterns
994
+
995
+ ### Tailwind CSS Integration
298
996
 
299
- Use a resolver that combines `cx` with `tw-merge` to properly merge Tailwind classes
300
- and let `tw-merge` remove conflicting utility classes (recommended for Tailwind users).
997
+ By default, `cv` and `scv` don't handle Tailwind class conflicts. Integrate with `tailwind-merge` for proper resolution:
301
998
 
302
- ```ts
999
+ ```typescript
303
1000
  import { cv, cx } from 'css-variants'
304
1001
  import { twMerge } from 'tailwind-merge'
305
1002
 
1003
+ const classNameResolver: typeof cx = (...args) => twMerge(cx(...args))
1004
+
306
1005
  const button = cv({
307
- base: 'btn',
1006
+ base: 'px-4 py-2 text-sm',
308
1007
  variants: {
309
- color: {
310
- primary: 'bg-blue-500',
311
- danger: 'bg-red-500'
312
- }
1008
+ size: {
1009
+ lg: 'px-6 py-3 text-lg', // Conflicts with base padding/text
1010
+ },
1011
+ },
1012
+ classNameResolver,
1013
+ })
1014
+
1015
+ button({ size: 'lg' })
1016
+ // Without twMerge: 'px-4 py-2 text-sm px-6 py-3 text-lg' (conflicting classes)
1017
+ // With twMerge: 'px-6 py-3 text-lg' (conflicts resolved)
1018
+ ```
1019
+
1020
+ **Recommended Setup**
1021
+
1022
+ ```typescript
1023
+ // lib/variants.ts
1024
+ import { cv as cvBase, scv as scvBase, cx as cxBase } from 'css-variants'
1025
+ import { twMerge } from 'tailwind-merge'
1026
+
1027
+ export const cx: typeof cxBase = (...args) => twMerge(cxBase(...args))
1028
+ export const cv = (config) => cvBase({ ...config, classNameResolver: cx })
1029
+ export const scv = (config) => scvBase({ ...config, classNameResolver: cx })
1030
+ ```
1031
+
1032
+ ```typescript
1033
+ // components/Button.tsx
1034
+ import { cv } from '@/lib/variants'
1035
+
1036
+ const button = cv({
1037
+ base: 'px-4 py-2',
1038
+ variants: { /* ... */ },
1039
+ })
1040
+ ```
1041
+
1042
+ ### CSS Modules Integration
1043
+
1044
+ ```typescript
1045
+ import { cv } from 'css-variants'
1046
+ import styles from './Button.module.css'
1047
+
1048
+ const button = cv({
1049
+ base: styles.button,
1050
+ variants: {
1051
+ variant: {
1052
+ primary: styles.primary,
1053
+ secondary: styles.secondary,
1054
+ },
1055
+ size: {
1056
+ sm: styles.sm,
1057
+ md: styles.md,
1058
+ lg: styles.lg,
1059
+ },
1060
+ },
1061
+ })
1062
+ ```
1063
+
1064
+ ### Responsive Variants (Tailwind)
1065
+
1066
+ ```typescript
1067
+ const container = cv({
1068
+ base: 'w-full mx-auto',
1069
+ variants: {
1070
+ size: {
1071
+ sm: 'max-w-screen-sm px-4',
1072
+ md: 'max-w-screen-md px-6',
1073
+ lg: 'max-w-screen-lg px-8',
1074
+ xl: 'max-w-screen-xl px-8',
1075
+ },
1076
+ },
1077
+ defaultVariants: {
1078
+ size: 'lg',
1079
+ },
1080
+ })
1081
+
1082
+ // Responsive: different sizes at different breakpoints
1083
+ <div className={cx(
1084
+ container({ size: 'sm' }),
1085
+ 'md:max-w-screen-md lg:max-w-screen-lg'
1086
+ )}>
1087
+ Content
1088
+ </div>
1089
+ ```
1090
+
1091
+ ### Component Composition
1092
+
1093
+ **Extending Variants**
1094
+
1095
+ ```typescript
1096
+ const baseButton = cv({
1097
+ base: 'rounded font-medium transition-colors',
1098
+ variants: {
1099
+ size: {
1100
+ sm: 'px-3 py-1.5 text-sm',
1101
+ md: 'px-4 py-2 text-base',
1102
+ lg: 'px-6 py-3 text-lg',
1103
+ },
313
1104
  },
314
- // recommended resolver: compose `cx` then `twMerge`
315
- classNameResolver: (...args) => twMerge(cx(...args))
316
1105
  })
317
1106
 
318
- // Later classes and conflicting utilities are resolved by `tw-merge`:
319
- button({ color: 'primary', className: 'bg-red-600' })
320
- // => 'btn bg-red-600' (tw-merge will prefer the later `bg-red-600` value)
1107
+ const iconButton = cv({
1108
+ base: baseButton({ size: 'md' }),
1109
+ variants: {
1110
+ variant: {
1111
+ ghost: 'bg-transparent hover:bg-gray-100',
1112
+ solid: 'bg-gray-900 text-white hover:bg-gray-700',
1113
+ },
1114
+ },
1115
+ })
321
1116
  ```
322
1117
 
323
- ## TypeScript Support
1118
+ **Composing with cx**
1119
+
1120
+ ```typescript
1121
+ const primaryButton = (props) => cx(
1122
+ button({ color: 'primary', ...props }),
1123
+ 'shadow-lg hover:shadow-xl'
1124
+ )
1125
+ ```
324
1126
 
325
- Full TypeScript support with automatic type inference:
1127
+ ### Type-Safe Props with TypeScript
326
1128
 
327
- ```ts
1129
+ **Extract Variant Props**
1130
+
1131
+ ```typescript
328
1132
  import { cv } from 'css-variants'
329
1133
 
330
1134
  const button = cv({
1135
+ variants: {
1136
+ color: { primary: '...', secondary: '...' },
1137
+ size: { sm: '...', md: '...', lg: '...' },
1138
+ },
1139
+ })
1140
+
1141
+ type ButtonVariants = Parameters<typeof button>[0]
1142
+ // => { color?: 'primary' | 'secondary', size?: 'sm' | 'md' | 'lg', className?: ClassValue }
1143
+ ```
1144
+
1145
+ ### Design System Example
1146
+
1147
+ ```typescript
1148
+ // design-system/variants.ts
1149
+ import { cv } from 'css-variants'
1150
+
1151
+ export const text = cv({
331
1152
  variants: {
332
1153
  size: {
1154
+ xs: 'text-xs',
333
1155
  sm: 'text-sm',
334
- lg: 'text-lg'
335
- }
336
- }
1156
+ base: 'text-base',
1157
+ lg: 'text-lg',
1158
+ xl: 'text-xl',
1159
+ '2xl': 'text-2xl',
1160
+ },
1161
+ weight: {
1162
+ normal: 'font-normal',
1163
+ medium: 'font-medium',
1164
+ semibold: 'font-semibold',
1165
+ bold: 'font-bold',
1166
+ },
1167
+ color: {
1168
+ default: 'text-gray-900',
1169
+ muted: 'text-gray-600',
1170
+ subtle: 'text-gray-500',
1171
+ primary: 'text-blue-600',
1172
+ error: 'text-red-600',
1173
+ success: 'text-green-600',
1174
+ },
1175
+ },
1176
+ defaultVariants: {
1177
+ size: 'base',
1178
+ weight: 'normal',
1179
+ color: 'default',
1180
+ },
1181
+ })
1182
+
1183
+ export const spacing = cv({
1184
+ variants: {
1185
+ p: {
1186
+ 0: 'p-0',
1187
+ 1: 'p-1',
1188
+ 2: 'p-2',
1189
+ 4: 'p-4',
1190
+ 6: 'p-6',
1191
+ 8: 'p-8',
1192
+ },
1193
+ m: {
1194
+ 0: 'm-0',
1195
+ 1: 'm-1',
1196
+ 2: 'm-2',
1197
+ 4: 'm-4',
1198
+ 6: 'm-6',
1199
+ 8: 'm-8',
1200
+ },
1201
+ },
337
1202
  })
1203
+ ```
1204
+
1205
+ ---
1206
+
1207
+ ## Framework Integration
1208
+
1209
+ ### React
1210
+
1211
+ ```tsx
1212
+ import { cv } from 'css-variants'
1213
+
1214
+ const button = cv({ /* ... */ })
1215
+
1216
+ export function Button({ color, size, className, children, ...props }) {
1217
+ return (
1218
+ <button className={button({ color, size, className })} {...props}>
1219
+ {children}
1220
+ </button>
1221
+ )
1222
+ }
1223
+
1224
+ // TypeScript version
1225
+ type ButtonVariants = Parameters<typeof button>[0]
338
1226
 
339
- type ButtonProps = Parameters<typeof button>[0]
340
- // => { size?: 'sm' | 'lg' | undefined }
1227
+ type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & ButtonVariants
1228
+
1229
+ export function Button({ color, size, className, children, ...props }: ButtonProps) {
1230
+ return (
1231
+ <button className={button({ color, size, className })} {...props}>
1232
+ {children}
1233
+ </button>
1234
+ )
1235
+ }
341
1236
  ```
342
1237
 
343
- ## Inspiration
1238
+ ### Vue 3
1239
+
1240
+ ```vue
1241
+ <template>
1242
+ <button :class="buttonClass">
1243
+ <slot />
1244
+ </button>
1245
+ </template>
344
1246
 
345
- This library is inspired by several excellent projects:
1247
+ <script setup lang="ts">
1248
+ import { computed } from 'vue'
1249
+ import { cv } from 'css-variants'
346
1250
 
347
- - [CVA (Class Variance Authority)](https://github.com/joe-bell/cva)
348
- - [Panda CSS](https://github.com/chakra-ui/panda)
1251
+ const button = cv({ /* ... */ })
349
1252
 
350
- ## Developer commands
1253
+ type ButtonVariants = Parameters<typeof button>[0]
351
1254
 
352
- ```bash
353
- yarn test # run vitest tests
354
- yarn build # build CJS + ESM artifacts into dist/
355
- yarn lint # eslint + prettier
1255
+ const props = defineProps<ButtonVariants>()
1256
+
1257
+ const buttonClass = computed(() => button({
1258
+ color: props.color,
1259
+ size: props.size,
1260
+ }))
1261
+ </script>
1262
+ ```
1263
+
1264
+ ### Solid.js
1265
+
1266
+ ```tsx
1267
+ import { cv } from 'css-variants'
1268
+ import type { JSX } from 'solid-js'
1269
+
1270
+ const button = cv({ /* ... */ })
1271
+
1272
+ type ButtonVariants = Parameters<typeof button>[0]
1273
+
1274
+ type ButtonProps = ButtonVariants & {
1275
+ children: JSX.Element
1276
+ }
1277
+
1278
+ export function Button(props: ButtonProps) {
1279
+ return (
1280
+ <button class={button({ color: props.color, size: props.size })}>
1281
+ {props.children}
1282
+ </button>
1283
+ )
1284
+ }
1285
+ ```
1286
+
1287
+ ---
1288
+
1289
+ ## Migration Guide
1290
+
1291
+ ### From CVA (Class Variance Authority)
1292
+
1293
+ `css-variants` is largely compatible with CVA's API:
1294
+
1295
+ ```typescript
1296
+ // CVA
1297
+ import { cva } from 'class-variance-authority'
1298
+
1299
+ const button = cva('btn', {
1300
+ variants: { /* ... */ },
1301
+ defaultVariants: { /* ... */ },
1302
+ })
1303
+
1304
+ // css-variants
1305
+ import { cv } from 'css-variants'
1306
+
1307
+ const button = cv({
1308
+ base: 'btn', // 'base' instead of first argument
1309
+ variants: { /* ... */ },
1310
+ defaultVariants: { /* ... */ },
1311
+ })
1312
+ ```
1313
+
1314
+ **Key Differences:**
1315
+ - Use `base` instead of first argument
1316
+ - Use `classNameResolver` instead of `className` for custom mergers
1317
+ - Compound variants use `className` key (CVA uses `class`)
1318
+
1319
+ ---
1320
+
1321
+ ## Performance
1322
+
1323
+ ### Bundle Size
1324
+
1325
+ - **Core library**: ~1KB minified + gzipped
1326
+ - **Tree-shakeable**: Import only what you need
1327
+ - **Zero dependencies**: No additional packages bundled
1328
+
1329
+ ```typescript
1330
+ // Only imports cv (~400 bytes)
1331
+ import { cv } from 'css-variants'
1332
+
1333
+ // Only imports scv (~600 bytes)
1334
+ import { scv } from 'css-variants'
1335
+ ```
1336
+
1337
+ ### Production Tips
1338
+
1339
+ **1. Create variants outside components**
1340
+
1341
+ ```typescript
1342
+ // Good โœ“ - Created once
1343
+ const button = cv({ /* ... */ })
1344
+
1345
+ function Button(props) {
1346
+ return <button className={button(props)} />
1347
+ }
1348
+
1349
+ // Bad โœ— - Recreated on every render
1350
+ function Button(props) {
1351
+ const button = cv({ /* ... */ }) // Don't do this!
1352
+ return <button className={button(props)} />
1353
+ }
1354
+ ```
1355
+
1356
+ **2. Minimize compound variants**
1357
+
1358
+ ```typescript
1359
+ // Each compound variant is checked at runtime
1360
+ compoundVariants: [
1361
+ { size: 'lg', color: 'primary', className: '...' },
1362
+ { size: 'lg', color: 'secondary', className: '...' },
1363
+ // Keep this array as small as possible
1364
+ ]
356
1365
  ```
357
1366
 
358
- ## Contribute
1367
+ ---
1368
+
1369
+ ## TypeScript Tips
1370
+
1371
+ ### Extract Types
1372
+
1373
+ ```typescript
1374
+ import { cv } from 'css-variants'
1375
+
1376
+ const button = cv({ /* ... */ })
1377
+
1378
+ // Extract prop types
1379
+ type ButtonVariants = Parameters<typeof button>[0]
1380
+
1381
+ type ButtonProps = ButtonVariants & {
1382
+ loading?: boolean
1383
+ }
1384
+
1385
+ // Use in component
1386
+ function Button(props: ButtonProps) { /* ... */ }
1387
+ ```
1388
+
1389
+ ---
1390
+
1391
+ ## FAQ
1392
+
1393
+ **Q: How is this different from CVA?**
1394
+ A: Very similar API! Main differences: `base` property instead of first argument, optimized for smaller bundle size, and includes built-in `cx` utility.
1395
+
1396
+ **Q: Can I use this without Tailwind?**
1397
+ A: Absolutely! Works with any CSS approach: vanilla CSS, CSS modules, styled-components, emotion, etc.
1398
+
1399
+ **Q: Does it work with Tailwind's `@apply`?**
1400
+ A: Yes, but we recommend using variants instead of `@apply` for better tree-shaking and smaller CSS bundles.
359
1401
 
360
- Please open PRs with focused changes and unit tests under `src/*.test.ts`. Keep runtime footprint minimal and preserve the exported API (`cv`, `sv`, `scv`, `ssv`, `cx`). See [CONTRIBUTING.md](./CONTRIBUTING.md) for process details.
1402
+ **Q: How do I handle responsive variants?**
1403
+ A: Use Tailwind's responsive prefixes in your variant classes: `'sm:text-sm md:text-base lg:text-lg'`
1404
+
1405
+ **Q: Can I use this in a design system?**
1406
+ A: Yes! Create base variants and export them for consistent styling across your application.
1407
+
1408
+ **Q: What about dark mode?**
1409
+ A: Use Tailwind's `dark:` prefix in variant classes, or create separate variants: `variant: { light: '...', dark: '...' }`
1410
+
1411
+ **Q: How do I migrate from CVA?**
1412
+ A: Very minimal changes needed. See the [Migration Guide](#migration-guide).
1413
+
1414
+ ---
1415
+
1416
+ ## Contributing
1417
+
1418
+ Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md) before submitting PRs.
1419
+
1420
+ **Development Setup:**
1421
+
1422
+ ```bash
1423
+ git clone https://github.com/timphandev/css-variants.git
1424
+ cd css-variants
1425
+ yarn install
1426
+ yarn test
1427
+ yarn build
1428
+ ```
1429
+
1430
+ ---
361
1431
 
362
1432
  ## License
363
1433
 
364
- Licensed under the MIT License.
1434
+ MIT ยฉ [Tim Phan](https://github.com/timphandev)
1435
+
1436
+ ---
1437
+
1438
+ ## Credits
1439
+
1440
+ - Class merging inspired by [clsx](https://github.com/lukeed/clsx) by Luke Edwards
1441
+ - API design inspired by [CVA](https://github.com/joe-bell/cva) and [Panda CSS](https://panda-css.com)
1442
+
1443
+ ---
365
1444
 
366
- See [MIT license](./LICENSE) for more information.
1445
+ **Made with โค๏ธ by developers, for developers**