css-variants 2.1.1 โ 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1298 -219
- package/dist/cjs/cv.bench.d.ts +1 -0
- package/dist/cjs/cv.bench.js +207 -0
- package/dist/cjs/cv.bench.js.map +1 -0
- package/dist/cjs/cv.js +12 -10
- package/dist/cjs/cv.js.map +1 -1
- package/dist/cjs/scv.bench.d.ts +1 -0
- package/dist/cjs/scv.bench.js +409 -0
- package/dist/cjs/scv.bench.js.map +1 -0
- package/dist/cjs/scv.js +13 -11
- package/dist/cjs/scv.js.map +1 -1
- package/dist/cjs/ssv.bench.d.ts +1 -0
- package/dist/cjs/ssv.bench.js +506 -0
- package/dist/cjs/ssv.bench.js.map +1 -0
- package/dist/cjs/ssv.js +14 -12
- package/dist/cjs/ssv.js.map +1 -1
- package/dist/cjs/sv.bench.d.ts +1 -0
- package/dist/cjs/sv.bench.js +264 -0
- package/dist/cjs/sv.bench.js.map +1 -0
- package/dist/cjs/sv.js +16 -11
- package/dist/cjs/sv.js.map +1 -1
- package/dist/cjs/utils/merge-props.d.ts +1 -1
- package/dist/cjs/utils/merge-props.js +8 -6
- package/dist/cjs/utils/merge-props.js.map +1 -1
- package/dist/cjs/utils/types.d.ts +3 -1
- package/dist/esm/cv.bench.d.ts +1 -0
- package/dist/esm/cv.bench.js +205 -0
- package/dist/esm/cv.bench.js.map +1 -0
- package/dist/esm/cv.js +12 -10
- package/dist/esm/cv.js.map +1 -1
- package/dist/esm/scv.bench.d.ts +1 -0
- package/dist/esm/scv.bench.js +407 -0
- package/dist/esm/scv.bench.js.map +1 -0
- package/dist/esm/scv.js +13 -11
- package/dist/esm/scv.js.map +1 -1
- package/dist/esm/ssv.bench.d.ts +1 -0
- package/dist/esm/ssv.bench.js +504 -0
- package/dist/esm/ssv.bench.js.map +1 -0
- package/dist/esm/ssv.js +14 -12
- package/dist/esm/ssv.js.map +1 -1
- package/dist/esm/sv.bench.d.ts +1 -0
- package/dist/esm/sv.bench.js +262 -0
- package/dist/esm/sv.bench.js.map +1 -0
- package/dist/esm/sv.js +16 -11
- package/dist/esm/sv.js.map +1 -1
- package/dist/esm/utils/merge-props.d.ts +1 -1
- package/dist/esm/utils/merge-props.js +8 -6
- package/dist/esm/utils/merge-props.js.map +1 -1
- package/dist/esm/utils/types.d.ts +3 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,366 +1,1445 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-

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