css-variants 2.2.0 → 2.2.1
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 +29 -1383
- 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 +14 -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
|
@@ -9,43 +9,17 @@ Build powerful, flexible component style systems with variants. Perfect for Tail
|
|
|
9
9
|
[](https://bundlephobia.com/package/css-variants)
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
11
|
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
[](https://github.com/timphandev/css-variants)
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
## Features
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
- ⚡ **Tiny & Fast** — Zero dependencies, ~1KB minified+gzipped
|
|
17
|
+
- 🔒 **Type-Safe** — First-class TypeScript support with complete type inference
|
|
18
|
+
- 🧩 **Flexible** — Works with Tailwind, CSS modules, vanilla CSS, or inline styles
|
|
19
|
+
- 👨💻 **Developer-Friendly** — Intuitive API inspired by CVA and Panda CSS
|
|
20
|
+
- 🚀 **Production-Ready** — Battle-tested, fully tested, dual CJS/ESM builds
|
|
16
21
|
|
|
17
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
🚀 **Production-Ready** — Battle-tested, fully tested, dual CJS/ESM builds
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
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
|
-
---
|
|
45
|
-
|
|
46
|
-
## Quick Start
|
|
47
|
-
|
|
48
|
-
### Installation
|
|
22
|
+
## Installation
|
|
49
23
|
|
|
50
24
|
```bash
|
|
51
25
|
npm install css-variants
|
|
@@ -55,7 +29,7 @@ yarn add css-variants
|
|
|
55
29
|
pnpm add css-variants
|
|
56
30
|
```
|
|
57
31
|
|
|
58
|
-
|
|
32
|
+
## Quick Example
|
|
59
33
|
|
|
60
34
|
```typescript
|
|
61
35
|
import { cv } from 'css-variants'
|
|
@@ -66,7 +40,6 @@ const button = cv({
|
|
|
66
40
|
color: {
|
|
67
41
|
primary: 'bg-blue-600 text-white hover:bg-blue-700',
|
|
68
42
|
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
|
|
69
|
-
danger: 'bg-red-600 text-white hover:bg-red-700',
|
|
70
43
|
},
|
|
71
44
|
size: {
|
|
72
45
|
sm: 'px-3 py-1.5 text-sm',
|
|
@@ -80,1366 +53,39 @@ const button = cv({
|
|
|
80
53
|
},
|
|
81
54
|
})
|
|
82
55
|
|
|
83
|
-
//
|
|
84
|
-
button() // => '
|
|
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
|
-
lg: 'px-6 py-3 text-lg',
|
|
198
|
-
},
|
|
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
|
-
},
|
|
378
|
-
})
|
|
379
|
-
```
|
|
380
|
-
|
|
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
|
-
})
|
|
394
|
-
```
|
|
395
|
-
|
|
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
|
|
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
|
|
420
|
-
|
|
421
|
-
**Card Component**
|
|
422
|
-
|
|
423
|
-
```typescript
|
|
424
|
-
const card = scv({
|
|
425
|
-
slots: ['root', 'header', 'title', 'description', 'content', 'footer'],
|
|
426
|
-
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',
|
|
433
|
-
},
|
|
434
|
-
variants: {
|
|
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
|
-
},
|
|
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
|
-
},
|
|
464
|
-
},
|
|
465
|
-
},
|
|
466
|
-
defaultVariants: {
|
|
467
|
-
variant: 'default',
|
|
468
|
-
},
|
|
469
|
-
})
|
|
470
|
-
|
|
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
|
-
})
|
|
533
|
-
```
|
|
534
|
-
|
|
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
|
-
```
|
|
547
|
-
|
|
548
|
-
**Compound Variants with Slots**
|
|
549
|
-
|
|
550
|
-
```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
|
-
},
|
|
558
|
-
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
|
-
},
|
|
577
|
-
},
|
|
578
|
-
compoundVariants: [
|
|
579
|
-
{
|
|
580
|
-
size: 'lg',
|
|
581
|
-
color: 'primary',
|
|
582
|
-
classNames: {
|
|
583
|
-
root: 'shadow-lg',
|
|
584
|
-
label: 'font-bold',
|
|
585
|
-
},
|
|
586
|
-
},
|
|
587
|
-
],
|
|
588
|
-
})
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
---
|
|
592
|
-
|
|
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
|
-
}
|
|
56
|
+
button() // => 'font-semibold rounded-lg ... bg-blue-600 text-white ... px-4 py-2 text-base'
|
|
57
|
+
button({ color: 'secondary', size: 'lg' }) // => '... bg-gray-200 text-gray-900 ... px-6 py-3 text-lg'
|
|
614
58
|
```
|
|
615
59
|
|
|
616
|
-
|
|
60
|
+
## Documentation
|
|
617
61
|
|
|
618
|
-
**
|
|
62
|
+
**[View full documentation →](https://css-variants.vercel.app/)**
|
|
619
63
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
})
|
|
64
|
+
- [Getting Started](https://css-variants.vercel.app/getting-started/introduction/)
|
|
65
|
+
- [Core Concepts](https://css-variants.vercel.app/core-concepts/variants/)
|
|
66
|
+
- [API Reference](https://css-variants.vercel.app/api/cv/)
|
|
67
|
+
- [Tailwind CSS Integration](https://css-variants.vercel.app/guides/tailwind/)
|
|
643
68
|
|
|
644
|
-
|
|
645
|
-
// => { display: 'flex', borderRadius: '8px', padding: '24px', fontSize: '18px', backgroundColor: '#dbeafe', color: '#1e40af' }
|
|
646
|
-
```
|
|
69
|
+
## Contributing
|
|
647
70
|
|
|
648
|
-
|
|
71
|
+
Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md) before submitting PRs.
|
|
649
72
|
|
|
650
|
-
|
|
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
|
-
})
|
|
73
|
+
**Development Setup:**
|
|
669
74
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
75
|
+
```bash
|
|
76
|
+
git clone https://github.com/timphandev/css-variants.git
|
|
77
|
+
cd css-variants
|
|
78
|
+
yarn install
|
|
79
|
+
yarn test
|
|
80
|
+
yarn build
|
|
674
81
|
```
|
|
675
82
|
|
|
676
|
-
|
|
83
|
+
## ⭐ Like it? Star it!
|
|
677
84
|
|
|
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
|
-
```
|
|
85
|
+
If this library saves you time, a **⭐ on GitHub** means a lot. Thank you! 🚀
|
|
708
86
|
|
|
709
|
-
|
|
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',
|
|
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
|
|
87
|
+
## License
|
|
1433
88
|
|
|
1434
89
|
MIT © [Tim Phan](https://github.com/timphandev)
|
|
1435
90
|
|
|
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
91
|
**Made with ❤️ by developers, for developers**
|