css-variants 2.2.0 → 2.2.2

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.
package/README.md CHANGED
@@ -1,8 +1,11 @@
1
+ <div align="center">
2
+
1
3
  # css-variants
2
4
 
3
- > **Zero-dependency, type-safe CSS variant composition for modern JavaScript**
5
+ **Zero-dependency, type-safe CSS variant composition for modern JavaScript**
4
6
 
5
- Build powerful, flexible component style systems with variants. Perfect for Tailwind CSS, vanilla CSS, or any CSS-in-JS solution.
7
+ Build powerful, flexible component style systems with variants.<br/>
8
+ Perfect for Tailwind CSS, vanilla CSS, or any CSS-in-JS solution.
6
9
 
7
10
  [![test](https://github.com/timphandev/css-variants/actions/workflows/ci.yml/badge.svg)](https://github.com/timphandev/css-variants/actions/workflows/ci.yml)
8
11
  [![npm version](https://img.shields.io/npm/v/css-variants.svg)](https://www.npmjs.com/package/css-variants)
@@ -10,52 +13,30 @@ Build powerful, flexible component style systems with variants. Perfect for Tail
10
13
  [![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue)](https://www.typescriptlang.org/)
11
14
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
15
 
13
- ---
14
-
15
- ## Why css-variants?
16
-
17
- ⚡ **Tiny & Fast** — Zero dependencies, ~1KB minified+gzipped
18
-
19
- 🔒 **Type-Safe** — First-class TypeScript support with complete type inference
20
-
21
- 🧩 **Flexible** — Works with Tailwind, CSS modules, vanilla CSS, or inline styles
22
-
23
- 👨‍💻 **Developer-Friendly** — Intuitive API inspired by CVA and Panda CSS
16
+ [Documentation](https://css-variants.vercel.app/) · [Getting Started](https://css-variants.vercel.app/getting-started/introduction/) · [API Reference](https://css-variants.vercel.app/api/cv/)
24
17
 
25
- 🚀 **Production-Ready** — Battle-tested, fully tested, dual CJS/ESM builds
18
+ </div>
26
19
 
27
20
  ---
28
21
 
29
- ## Table of Contents
30
-
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)
43
-
44
- ---
22
+ ## Why css-variants?
45
23
 
46
- ## Quick Start
24
+ | | Feature |
25
+ |---|---------|
26
+ | ⚡ | **~1KB** — Zero dependencies, tree-shakeable |
27
+ | 🔒 | **Type-Safe** — Full TypeScript inference for variants & props |
28
+ | 🧩 | **Flexible** — Works with Tailwind, CSS Modules, vanilla CSS, inline styles |
29
+ | 🚀 | **Fast** — Up to 10x faster than alternatives in complex scenarios |
47
30
 
48
- ### Installation
31
+ ## Installation
49
32
 
50
33
  ```bash
51
34
  npm install css-variants
52
- # or
53
- yarn add css-variants
54
- # or
55
- pnpm add css-variants
56
35
  ```
57
36
 
58
- ### Your First Variant
37
+ ## Quick Start
38
+
39
+ ### Single-Element Variants (`cv`)
59
40
 
60
41
  ```typescript
61
42
  import { cv } from 'css-variants'
@@ -66,1380 +47,101 @@ const button = cv({
66
47
  color: {
67
48
  primary: 'bg-blue-600 text-white hover:bg-blue-700',
68
49
  secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
69
- danger: 'bg-red-600 text-white hover:bg-red-700',
70
50
  },
71
51
  size: {
72
52
  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
- ```
87
-
88
- ### Framework Examples
89
-
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>
101
- ```
102
-
103
- **Vue**
104
- ```vue
105
- <template>
106
- <button :class="button({ color, size })">
107
- <slot />
108
- </button>
109
- </template>
110
-
111
- <script setup>
112
- import { cv } from 'css-variants'
113
-
114
- const props = defineProps(['color', 'size'])
115
- const button = cv({ /* config */ })
116
- </script>
117
- ```
118
-
119
- ---
120
-
121
- ## Core Concepts
122
-
123
- ### Variants
124
-
125
- Variants are named groups of style options. Each variant has a set of possible values:
126
-
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
- })
137
-
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
- ```
182
-
183
- ### Compound Variants
184
-
185
- Apply styles when **multiple variants match simultaneously**:
186
-
187
- ```typescript
188
- const button = cv({
189
- base: 'rounded font-medium',
190
- variants: {
191
- color: {
192
- primary: 'bg-blue-600 text-white',
193
- secondary: 'bg-gray-200 text-gray-900',
194
- },
195
- size: {
196
- sm: 'px-3 py-1 text-sm',
197
53
  lg: 'px-6 py-3 text-lg',
198
54
  },
199
- disabled: {
200
- true: 'opacity-50 cursor-not-allowed',
201
- false: 'cursor-pointer',
202
- },
203
- },
204
- compoundVariants: [
205
- // Large primary buttons get extra bold text
206
- {
207
- color: 'primary',
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
- },
216
- ],
217
- })
218
-
219
- button({ color: 'primary', size: 'lg' })
220
- // => Includes 'font-bold shadow-lg' from compound variant
221
- ```
222
-
223
- **Array Matching** — Match against multiple variant values:
224
-
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
- })
248
- ```
249
-
250
- ### Boolean Variants
251
-
252
- Boolean variants use string keys `'true'` and `'false'`, but accept actual booleans in props:
253
-
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
- },
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',
281
- variants: {
282
- color: {
283
- primary: 'bg-blue-600 text-white',
284
- },
285
- },
286
- })
287
-
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
55
  },
56
+ defaultVariants: { color: 'primary', size: 'sm' },
378
57
  })
379
- ```
380
58
 
381
- **Array and Object ClassValues**
382
-
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
- },
393
- })
59
+ button() // Primary + Small (defaults)
60
+ button({ color: 'secondary' }) // Secondary + Small
61
+ button({ size: 'lg', className: 'w-full' }) // Primary + Large + custom class
394
62
  ```
395
63
 
396
- ---
397
-
398
- ### `scv` - Slot Class Variants
399
-
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
64
+ ### Multi-Element Components (`scv`)
403
65
 
404
66
  ```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
420
-
421
- **Card Component**
67
+ import { scv } from 'css-variants'
422
68
 
423
- ```typescript
424
69
  const card = scv({
425
- slots: ['root', 'header', 'title', 'description', 'content', 'footer'],
70
+ slots: ['root', 'header', 'body', 'footer'],
426
71
  base: {
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',
72
+ root: 'rounded-xl border shadow-sm',
73
+ header: 'px-6 py-4 border-b',
74
+ body: 'px-6 py-4',
75
+ footer: 'px-6 py-3 bg-gray-50',
433
76
  },
434
77
  variants: {
435
78
  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
- },
454
- sm: {
455
- content: 'p-4',
456
- header: 'p-4',
457
- footer: 'px-4 py-2',
458
- },
459
- lg: {
460
- content: 'p-8',
461
- header: 'p-8',
462
- footer: 'px-8 py-4',
463
- },
79
+ default: { root: 'bg-white border-gray-200' },
80
+ danger: { root: 'bg-red-50 border-red-200', header: 'text-red-900' },
464
81
  },
465
82
  },
466
- defaultVariants: {
467
- variant: 'default',
468
- },
469
83
  })
470
84
 
471
- const classes = card({ variant: 'primary' })
472
- // => {
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'
479
- // }
480
- ```
481
-
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,
531
- },
532
- })
85
+ const styles = card({ variant: 'danger' })
86
+ // styles.root → 'rounded-xl border shadow-sm bg-red-50 border-red-200'
87
+ // styles.header 'px-6 py-4 border-b text-red-900'
533
88
  ```
534
89
 
535
- **Slot-Specific Overrides**
536
-
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
- ```
90
+ ### Compound Variants
547
91
 
548
- **Compound Variants with Slots**
92
+ Apply styles when multiple variants match:
549
93
 
550
94
  ```typescript
551
- const button = scv({
552
- slots: ['root', 'icon', 'label'],
553
- base: {
554
- root: 'inline-flex items-center gap-2 rounded font-medium',
555
- icon: 'w-5 h-5',
556
- label: '',
557
- },
95
+ const button = cv({
558
96
  variants: {
559
- size: {
560
- sm: {
561
- root: 'px-3 py-1.5 text-sm',
562
- icon: 'w-4 h-4',
563
- },
564
- lg: {
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
- },
97
+ color: { primary: '...', danger: '...' },
98
+ size: { sm: '...', lg: '...' },
577
99
  },
578
100
  compoundVariants: [
579
- {
580
- size: 'lg',
581
- color: 'primary',
582
- classNames: {
583
- root: 'shadow-lg',
584
- label: 'font-bold',
585
- },
586
- },
101
+ { color: 'danger', size: 'lg', className: 'font-bold uppercase' },
587
102
  ],
588
103
  })
589
104
  ```
590
105
 
591
- ---
592
-
593
- ### `sv` - Style Variants
594
-
595
- Create variants for **inline CSS styles** (React's `style` prop, Vue's `:style`, etc.).
106
+ ### Style Variants (`sv`)
596
107
 
597
- #### Type Signature
108
+ For inline styles instead of class names:
598
109
 
599
110
  ```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
111
+ import { sv } from 'css-variants'
617
112
 
618
- **Basic Style Variant**
619
-
620
- ```typescript
621
113
  const box = sv({
622
- base: {
623
- display: 'flex',
624
- borderRadius: '8px',
625
- },
114
+ base: { display: 'flex', borderRadius: '8px' },
626
115
  variants: {
627
116
  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' },
117
+ sm: { padding: '8px' },
118
+ lg: { padding: '24px' },
636
119
  },
637
120
  },
638
- defaultVariants: {
639
- size: 'md',
640
- color: 'gray',
641
- },
642
121
  })
643
122
 
644
- box({ size: 'lg', color: 'blue' })
645
- // => { display: 'flex', borderRadius: '8px', padding: '24px', fontSize: '18px', backgroundColor: '#dbeafe', color: '#1e40af' }
123
+ box({ size: 'lg' }) // => { display: 'flex', borderRadius: '8px', padding: '24px' }
646
124
  ```
647
125
 
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
- ```
126
+ ## Performance
675
127
 
676
- **Compound Style Variants**
128
+ | vs cva | vs tailwind-variants |
129
+ |:------:|:--------------------:|
130
+ | 3-4x faster (compound variants) | 5-6x faster (compound variants) |
131
+ | 4-9x faster (complex components) | 10-11x faster (complex components) |
677
132
 
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
- ```
133
+ ## Documentation
708
134
 
709
- **Runtime Style Overrides**
135
+ **[Full documentation →](https://css-variants.vercel.app/)**
710
136
 
711
- ```typescript
712
- const card = sv({
713
- base: { padding: '16px', borderRadius: '8px' },
714
- })
137
+ ## Contributing
715
138
 
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)' }
139
+ ```bash
140
+ git clone https://github.com/timphandev/css-variants.git
141
+ cd css-variants && yarn install
142
+ yarn test && yarn build
718
143
  ```
719
144
 
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',
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',
831
- },
832
- })
833
-
834
- const styles = tooltip({ placement: 'bottom', variant: 'light' })
835
- // => {
836
- // container: { position: 'relative', display: 'inline-block' },
837
- // arrow: { ... },
838
- // content: { backgroundColor: '#f9fafb', color: '#1f2937', ... }
839
- // }
840
- ```
841
-
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**
899
-
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
- ```
909
-
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
939
- import { cx } from 'css-variants'
940
-
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
- ```
945
-
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
- ```
959
-
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
- ```
973
-
974
- **React Example**
975
-
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
- }
989
- ```
990
-
991
- ---
992
-
993
- ## Advanced Patterns
994
-
995
- ### Tailwind CSS Integration
996
-
997
- By default, `cv` and `scv` don't handle Tailwind class conflicts. Integrate with `tailwind-merge` for proper resolution:
998
-
999
- ```typescript
1000
- import { cv, cx } from 'css-variants'
1001
- import { twMerge } from 'tailwind-merge'
1002
-
1003
- const classNameResolver: typeof cx = (...args) => twMerge(cx(...args))
1004
-
1005
- const button = cv({
1006
- base: 'px-4 py-2 text-sm',
1007
- variants: {
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
- },
1104
- },
1105
- })
1106
-
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
- })
1116
- ```
1117
-
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
- ```
1126
-
1127
- ### Type-Safe Props with TypeScript
1128
-
1129
- **Extract Variant Props**
1130
-
1131
- ```typescript
1132
- import { cv } from 'css-variants'
1133
-
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({
1152
- variants: {
1153
- size: {
1154
- xs: 'text-xs',
1155
- sm: 'text-sm',
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
- },
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]
1226
-
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
- }
1236
- ```
1237
-
1238
- ### Vue 3
1239
-
1240
- ```vue
1241
- <template>
1242
- <button :class="buttonClass">
1243
- <slot />
1244
- </button>
1245
- </template>
1246
-
1247
- <script setup lang="ts">
1248
- import { computed } from 'vue'
1249
- import { cv } from 'css-variants'
1250
-
1251
- const button = cv({ /* ... */ })
1252
-
1253
- type ButtonVariants = Parameters<typeof button>[0]
1254
-
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
- ]
1365
- ```
1366
-
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.
1401
-
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
- ---
1431
-
1432
- ## License
145
+ ## License
1433
146
 
1434
147
  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
- ---
1444
-
1445
- **Made with ❤️ by developers, for developers**