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 +60 -1358
- package/dist/cjs/cx.js +2 -1
- package/dist/cjs/cx.js.map +1 -1
- package/dist/esm/cx.js +2 -1
- package/dist/esm/cx.js.map +1 -1
- package/package.json +15 -10
- package/dist/cjs/cv.bench.d.ts +0 -1
- package/dist/cjs/cv.bench.js +0 -207
- package/dist/cjs/cv.bench.js.map +0 -1
- package/dist/cjs/scv.bench.d.ts +0 -1
- package/dist/cjs/scv.bench.js +0 -409
- package/dist/cjs/scv.bench.js.map +0 -1
- package/dist/cjs/ssv.bench.d.ts +0 -1
- package/dist/cjs/ssv.bench.js +0 -506
- package/dist/cjs/ssv.bench.js.map +0 -1
- package/dist/cjs/sv.bench.d.ts +0 -1
- package/dist/cjs/sv.bench.js +0 -264
- package/dist/cjs/sv.bench.js.map +0 -1
- package/dist/esm/cv.bench.d.ts +0 -1
- package/dist/esm/cv.bench.js +0 -205
- package/dist/esm/cv.bench.js.map +0 -1
- package/dist/esm/scv.bench.d.ts +0 -1
- package/dist/esm/scv.bench.js +0 -407
- package/dist/esm/scv.bench.js.map +0 -1
- package/dist/esm/ssv.bench.d.ts +0 -1
- package/dist/esm/ssv.bench.js +0 -504
- package/dist/esm/ssv.bench.js.map +0 -1
- package/dist/esm/sv.bench.d.ts +0 -1
- package/dist/esm/sv.bench.js +0 -262
- package/dist/esm/sv.bench.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# css-variants
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
**Zero-dependency, type-safe CSS variant composition for modern JavaScript**
|
|
4
6
|
|
|
5
|
-
Build powerful, flexible component style systems with variants
|
|
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
|
[](https://github.com/timphandev/css-variants/actions/workflows/ci.yml)
|
|
8
11
|
[](https://www.npmjs.com/package/css-variants)
|
|
@@ -10,52 +13,30 @@ Build powerful, flexible component style systems with variants. Perfect for Tail
|
|
|
10
13
|
[](https://www.typescriptlang.org/)
|
|
11
14
|
[](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
|
-
|
|
18
|
+
</div>
|
|
26
19
|
|
|
27
20
|
---
|
|
28
21
|
|
|
29
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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', '
|
|
70
|
+
slots: ['root', 'header', 'body', 'footer'],
|
|
426
71
|
base: {
|
|
427
|
-
root: 'rounded-
|
|
428
|
-
header: 'border-b
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
|
472
|
-
//
|
|
473
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
Apply styles when multiple variants match:
|
|
549
93
|
|
|
550
94
|
```typescript
|
|
551
|
-
const button =
|
|
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
|
-
|
|
560
|
-
|
|
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
|
-
|
|
108
|
+
For inline styles instead of class names:
|
|
598
109
|
|
|
599
110
|
```typescript
|
|
600
|
-
|
|
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'
|
|
629
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
135
|
+
**[Full documentation →](https://css-variants.vercel.app/)**
|
|
710
136
|
|
|
711
|
-
|
|
712
|
-
const card = sv({
|
|
713
|
-
base: { padding: '16px', borderRadius: '8px' },
|
|
714
|
-
})
|
|
137
|
+
## Contributing
|
|
715
138
|
|
|
716
|
-
|
|
717
|
-
|
|
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**
|