nestable-tailwind-variants 0.1.4 → 0.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 CHANGED
@@ -1,238 +1,614 @@
1
1
  # nestable-tailwind-variants
2
2
 
3
- A variant styling library for Tailwind CSS that expresses complex style combinations through nested condition definitions instead of flat `compoundVariants` patterns.
3
+ Tailwind CSS variant library with nestable conditions for compound styling instead of flat compoundVariants.
4
4
 
5
- Inspired by [React Spectrum's conditional styles](https://react-spectrum.adobe.com/styling#conditional-styles), with some ideas from [tailwind-variants](https://www.tailwind-variants.org/).
5
+ Inspired by [React Spectrum's style macro](https://github.com/adobe/react-spectrum/tree/main/packages/%40react-spectrum/s2) and [Tailwind Variants](https://www.tailwind-variants.org/).
6
6
 
7
- ## Installation
7
+ ## Why nestable-tailwind-variants?
8
8
 
9
- ```bash
10
- npm install nestable-tailwind-variants
11
- ```
9
+ With [Tailwind Variants](https://www.tailwind-variants.org/), combining variants with [React Aria Components render props](https://react-aria.adobe.com/styling#render-props) like `isHovered` and `isPressed` requires `compoundVariants`. This quickly becomes verbose as combinations grow:
10
+
11
+ ```tsx
12
+ import { tv } from 'tailwind-variants';
13
+
14
+ const button = tv({
15
+ base: 'px-4 py-2 rounded',
16
+ variants: {
17
+ variant: {
18
+ primary: 'bg-blue-500 text-white',
19
+ secondary: 'bg-gray-200 text-gray-800',
20
+ },
21
+ isHovered: { true: '' },
22
+ isPressed: { true: '' },
23
+ },
24
+ compoundVariants: [
25
+ { variant: 'primary', isHovered: true, class: 'bg-blue-600' },
26
+ { variant: 'primary', isPressed: true, class: 'bg-blue-700' },
27
+ { variant: 'secondary', isHovered: true, class: 'bg-gray-300' },
28
+ // ... one entry for every variant × condition combination
29
+ ],
30
+ });
12
31
 
13
- ## Basic Usage
32
+ button({ variant: 'primary', isHovered: true });
33
+ // => 'px-4 py-2 rounded bg-blue-500 text-white bg-blue-600'
34
+ ```
14
35
 
15
- The `ntv` function creates a style function from a nested style definition. Conditions can be nested to express compound states like "primary variant when hovered":
36
+ nestable-tailwind-variants solves this by letting you nest conditions directly inside variants, keeping related logic together:
16
37
 
17
38
  ```tsx
18
39
  import { ntv } from 'nestable-tailwind-variants';
19
40
 
20
- interface ButtonStyleProps {
21
- variant?: 'primary' | 'secondary';
22
- isHovered?: boolean;
41
+ interface ButtonProps {
42
+ variant: 'primary' | 'secondary';
43
+ isHovered: boolean;
44
+ isPressed: boolean;
23
45
  }
24
46
 
25
- const button = ntv<ButtonStyleProps>({
26
- default: 'px-4 py-2 rounded font-medium',
47
+ const button = ntv<ButtonProps>({
48
+ $base: 'px-4 py-2 rounded',
27
49
  variant: {
28
50
  primary: {
29
- default: 'bg-blue-500 text-white',
51
+ $default: 'bg-blue-500 text-white',
30
52
  isHovered: 'bg-blue-600',
53
+ isPressed: 'bg-blue-700',
31
54
  },
32
55
  secondary: {
33
- default: 'bg-gray-200 text-gray-800',
56
+ $default: 'bg-gray-200 text-gray-800',
34
57
  isHovered: 'bg-gray-300',
35
58
  },
36
59
  },
37
60
  });
38
61
 
39
- button({ variant: 'primary' });
40
- // => 'px-4 py-2 rounded font-medium bg-blue-500 text-white'
41
-
42
62
  button({ variant: 'primary', isHovered: true });
43
- // => 'px-4 py-2 rounded font-medium bg-blue-600 text-white'
63
+ // => 'px-4 py-2 rounded bg-blue-600'
64
+ ```
65
+
66
+ ## Installation
67
+
68
+ ```bash
69
+ npm install nestable-tailwind-variants
44
70
  ```
45
71
 
46
- Conditions at the same level are mutually exclusive and ordered—the last matching condition takes precedence. Class conflicts are automatically resolved by [tailwind-merge](https://github.com/dcastil/tailwind-merge).
72
+ ## Core Concepts
47
73
 
48
- ## Why Nested?
74
+ ### `$base` - Always-applied styles
49
75
 
50
- When combining multiple conditions like `variant` + `isHovered`, tailwind-variants requires a flat `compoundVariants` array.
76
+ The `$base` property defines styles that are always included in the output:
51
77
 
52
- **tailwind-variants:**
78
+ ```ts
79
+ interface CardProps {
80
+ variant: 'elevated' | 'flat';
81
+ }
53
82
 
54
- ```tsx
55
- const button = tv({
56
- base: 'rounded font-medium',
57
- variants: {
58
- variant: {
59
- primary: 'bg-blue-500 text-white',
60
- secondary: 'bg-gray-200 text-gray-800',
61
- },
62
- isHovered: { true: '' },
63
- isPressed: { true: '' },
83
+ const card = ntv<CardProps>({
84
+ $base: 'rounded-lg shadow-md p-4',
85
+ variant: {
86
+ elevated: 'shadow-xl',
87
+ flat: 'shadow-none',
64
88
  },
65
- compoundVariants: [
66
- { variant: 'primary', isHovered: true, class: 'bg-blue-600' },
67
- { variant: 'primary', isPressed: true, class: 'bg-blue-700' },
68
- { variant: 'secondary', isHovered: true, class: 'bg-gray-300' },
69
- { variant: 'secondary', isPressed: true, class: 'bg-gray-400' },
70
- ],
71
89
  });
90
+
91
+ card();
92
+ // => 'rounded-lg shadow-md p-4'
93
+
94
+ card({ variant: 'elevated' });
95
+ // => 'rounded-lg shadow-xl p-4'
96
+ // Note: shadow-md is replaced by shadow-xl via tailwind-merge
72
97
  ```
73
98
 
74
- With nestable-tailwind-variants, you can nest conditions directly under each variant.
99
+ ### Variants - String-based selection
75
100
 
76
- **nestable-tailwind-variants:**
101
+ Define variants as objects mapping variant values to class names:
77
102
 
78
- ```tsx
79
- interface ButtonStyleProps {
80
- variant?: 'primary' | 'secondary';
81
- isHovered?: boolean;
82
- isPressed?: boolean;
103
+ ```ts
104
+ interface BadgeProps {
105
+ size: 'sm' | 'md' | 'lg';
106
+ color: 'info' | 'success' | 'warning';
107
+ }
108
+
109
+ const badge = ntv<BadgeProps>({
110
+ $base: 'px-2 py-1 rounded text-sm',
111
+ size: {
112
+ sm: 'text-xs px-1.5',
113
+ md: 'text-sm px-2',
114
+ lg: 'text-base px-3',
115
+ },
116
+ color: {
117
+ info: 'bg-blue-100 text-blue-800',
118
+ success: 'bg-green-100 text-green-800',
119
+ warning: 'bg-yellow-100 text-yellow-800',
120
+ },
121
+ });
122
+
123
+ badge({ size: 'lg', color: 'success' });
124
+ // => 'py-1 rounded text-base px-3 bg-green-100 text-green-800'
125
+ ```
126
+
127
+ ### Boolean Conditions - `is[A-Z]` / `allows[A-Z]` patterns
128
+
129
+ Keys matching `is[A-Z]*` or `allows[A-Z]*` are treated as boolean conditions:
130
+
131
+ ```ts
132
+ interface InputProps {
133
+ isFocused: boolean;
134
+ isDisabled: boolean;
135
+ isInvalid: boolean;
136
+ allowsClearing: boolean;
137
+ }
138
+
139
+ const input = ntv<InputProps>({
140
+ $base: 'border rounded px-3 py-2',
141
+ isFocused: 'ring-2 ring-blue-500 border-blue-500',
142
+ isDisabled: 'bg-gray-100 text-gray-400 cursor-not-allowed',
143
+ isInvalid: 'border-red-500 text-red-600',
144
+ allowsClearing: 'pr-8',
145
+ });
146
+
147
+ input({ isFocused: true });
148
+ // => 'border rounded px-3 py-2 ring-2 ring-blue-500 border-blue-500'
149
+
150
+ input({ isDisabled: true, isInvalid: true });
151
+ // => 'border rounded px-3 py-2 bg-gray-100 text-gray-400 cursor-not-allowed border-red-500 text-red-600'
152
+ ```
153
+
154
+ ### `$default` - Fallback styles
155
+
156
+ Use `$default` for styles applied when no conditions match:
157
+
158
+ ```ts
159
+ interface TextProps {
160
+ variant: 'primary' | 'danger';
83
161
  }
84
162
 
85
- const button = ntv<ButtonStyleProps>({
86
- default: 'rounded font-medium',
163
+ const text = ntv<TextProps>({
164
+ $default: 'text-gray-500',
87
165
  variant: {
166
+ primary: 'text-blue-600',
167
+ danger: 'text-red-600',
168
+ },
169
+ });
170
+
171
+ text();
172
+ // => 'text-gray-500'
173
+
174
+ text({ variant: 'primary' });
175
+ // => 'text-blue-600'
176
+ ```
177
+
178
+ When `$default` is nested inside variants, they accumulate only when no conditions match at each level:
179
+
180
+ ```ts
181
+ interface Props {
182
+ variant: 'primary';
183
+ size: 'large';
184
+ }
185
+
186
+ const styles = ntv<Props>({
187
+ $base: 'base',
188
+ $default: 'root-default',
189
+ variant: {
190
+ $default: 'variant-default',
88
191
  primary: {
89
- default: 'bg-blue-500 text-white',
90
- isHovered: 'bg-blue-600',
91
- isPressed: 'bg-blue-700',
192
+ size: {
193
+ $default: 'size-default',
194
+ large: 'size-large',
195
+ },
92
196
  },
93
- secondary: {
94
- default: 'bg-gray-200 text-gray-800',
95
- isHovered: 'bg-gray-300',
96
- isPressed: 'bg-gray-400',
197
+ },
198
+ });
199
+
200
+ styles();
201
+ // => 'base root-default variant-default'
202
+ // No conditions matched, so $defaults accumulate
203
+
204
+ styles({ variant: 'primary' });
205
+ // => 'base size-default'
206
+ // variant matched, so only nested $default is applied
207
+
208
+ styles({ variant: 'primary', size: 'large' });
209
+ // => 'base size-large'
210
+ // Both matched, so no $defaults are applied
211
+ ```
212
+
213
+ ## Nested Conditions
214
+
215
+ The core feature of nestable-tailwind-variants is the ability to nest conditions inside variants.
216
+
217
+ ### Boolean conditions inside variants
218
+
219
+ ```ts
220
+ interface ChipProps {
221
+ variant: 'filled' | 'outlined';
222
+ isSelected: boolean;
223
+ }
224
+
225
+ const chip = ntv<ChipProps>({
226
+ $base: 'inline-flex items-center rounded-full px-3 py-1',
227
+ variant: {
228
+ filled: {
229
+ $default: 'bg-gray-200 text-gray-800',
230
+ isSelected: 'bg-blue-500 text-white',
231
+ },
232
+ outlined: {
233
+ $default: 'border border-gray-300 text-gray-800',
234
+ isSelected: 'border-blue-500 text-blue-500',
97
235
  },
98
236
  },
99
237
  });
238
+
239
+ chip({ variant: 'filled' });
240
+ // => 'inline-flex items-center rounded-full px-3 py-1 bg-gray-200 text-gray-800'
241
+
242
+ chip({ variant: 'filled', isSelected: true });
243
+ // => 'inline-flex items-center rounded-full px-3 py-1 bg-blue-500 text-white'
244
+
245
+ chip({ variant: 'outlined', isSelected: true });
246
+ // => 'inline-flex items-center rounded-full px-3 py-1 border-blue-500 text-blue-500'
100
247
  ```
101
248
 
102
- Nesting keeps related styles grouped together, making it easier to see which hover/pressed states belong to which variant.
249
+ ### Multi-level nesting
103
250
 
104
- ## With React Aria Components
251
+ You can nest conditions to any depth:
105
252
 
106
- Since `ntv` returns a function, it works directly with [React Aria Components](https://react-aria.adobe.com/)' render props:
253
+ ```ts
254
+ interface ButtonProps {
255
+ variant: 'primary';
256
+ isHovered: boolean;
257
+ isPressed: boolean;
258
+ isDisabled: boolean;
259
+ }
107
260
 
108
- ```tsx
109
- import { Checkbox, type CheckboxRenderProps } from 'react-aria-components';
110
- import { ntv } from 'nestable-tailwind-variants';
261
+ const button = ntv<ButtonProps>({
262
+ $base: 'px-4 py-2 rounded font-medium transition-colors',
263
+ variant: {
264
+ primary: {
265
+ $default: 'bg-blue-500 text-white',
266
+ isHovered: 'bg-blue-600',
267
+ isPressed: 'bg-blue-700',
268
+ isDisabled: {
269
+ $default: 'bg-blue-300 cursor-not-allowed',
270
+ isHovered: 'bg-blue-300', // Prevent hover effect when disabled
271
+ },
272
+ },
273
+ },
274
+ });
111
275
 
112
- <Checkbox
113
- className={ntv<CheckboxRenderProps>({
114
- default: 'bg-gray-100',
115
- isHovered: 'bg-gray-200',
116
- isSelected: 'bg-gray-900',
117
- })}
118
- />;
276
+ button({ variant: 'primary' });
277
+ // => 'px-4 py-2 rounded font-medium transition-colors bg-blue-500 text-white'
278
+
279
+ button({ variant: 'primary', isHovered: true });
280
+ // => 'px-4 py-2 rounded font-medium transition-colors bg-blue-600'
281
+
282
+ button({ variant: 'primary', isDisabled: true });
283
+ // => 'px-4 py-2 rounded font-medium transition-colors bg-blue-300 cursor-not-allowed'
284
+
285
+ button({ variant: 'primary', isDisabled: true, isHovered: true });
286
+ // => 'px-4 py-2 rounded font-medium transition-colors bg-blue-300'
119
287
  ```
120
288
 
121
289
  ## Composing Styles
122
290
 
123
- Combine multiple style functions using `composeNtv`. This is useful for reusing common styles across multiple components:
291
+ Merge multiple style functions into one. Later functions take precedence:
124
292
 
125
- ```tsx
126
- import { ntv, composeNtv } from 'nestable-tailwind-variants';
293
+ ```ts
294
+ import { ntv, mergeNtv } from 'nestable-tailwind-variants';
295
+
296
+ interface BaseButtonProps {
297
+ size: 'sm' | 'md';
298
+ }
127
299
 
128
- const baseButton = ntv<{ size?: 'sm' | 'lg' }>({
129
- default: 'rounded font-medium',
300
+ const baseButton = ntv<BaseButtonProps>({
301
+ $base: 'rounded font-medium transition-colors',
130
302
  size: {
131
- sm: 'px-2 py-1 text-sm',
132
- lg: 'px-4 py-2 text-lg',
303
+ sm: 'px-3 py-1.5 text-sm',
304
+ md: 'px-4 py-2 text-base',
133
305
  },
134
306
  });
135
307
 
136
- const coloredButton = ntv<{ variant?: 'primary' | 'secondary' }>({
308
+ interface ColoredButtonProps {
309
+ variant: 'primary' | 'secondary';
310
+ }
311
+
312
+ const coloredButton = ntv<ColoredButtonProps>({
137
313
  variant: {
138
- primary: 'bg-blue-500 text-white',
139
- secondary: 'bg-gray-200 text-gray-800',
314
+ primary: 'bg-blue-500 text-white hover:bg-blue-600',
315
+ secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
140
316
  },
141
317
  });
142
318
 
143
- const button = composeNtv(baseButton, coloredButton);
319
+ const button = mergeNtv(baseButton, coloredButton);
144
320
 
145
- button({ size: 'lg', variant: 'primary' });
146
- // => 'rounded font-medium px-4 py-2 text-lg bg-blue-500 text-white'
321
+ button({ size: 'md', variant: 'primary' });
322
+ // => 'rounded font-medium transition-colors px-4 py-2 text-base bg-blue-500 text-white hover:bg-blue-600'
147
323
  ```
148
324
 
149
- ## VS Code Integration
325
+ ## Passing Additional Classes
150
326
 
151
- With [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) installed, add the following to your VS Code settings (`settings.json`) to enable autocomplete in `ntv` calls:
327
+ Pass additional classes using `class` or `className`:
152
328
 
153
- ```json
154
- {
155
- "tailwindCSS.classFunctions": ["ntv"]
329
+ ```ts
330
+ interface BoxProps {
331
+ variant: 'primary' | 'secondary';
156
332
  }
333
+
334
+ const box = ntv<BoxProps>({
335
+ $base: 'p-4 rounded',
336
+ variant: {
337
+ primary: 'bg-blue-500',
338
+ secondary: 'bg-gray-200',
339
+ },
340
+ });
341
+
342
+ box({ variant: 'primary', class: 'mt-4 p-8' });
343
+ // => 'rounded bg-blue-500 mt-4 p-8'
344
+ // Note: p-8 overrides p-4 via tailwind-merge
345
+
346
+ box({ variant: 'secondary', className: 'shadow-lg' });
347
+ // => 'p-4 rounded bg-gray-200 shadow-lg'
157
348
  ```
158
349
 
159
- ## ESLint Integration
350
+ ## React Aria Components Integration
160
351
 
161
- To lint Tailwind classes inside `ntv` calls with [eslint-plugin-better-tailwindcss](https://github.com/schoero/eslint-plugin-better-tailwindcss), extend the default callees in your ESLint configuration:
352
+ nestable-tailwind-variants is designed to work seamlessly with [React Aria Components render props](https://react-aria.adobe.com/styling#render-props):
162
353
 
163
- ```js
164
- import { getDefaultCallees } from 'eslint-plugin-better-tailwindcss/defaults';
354
+ ```tsx
355
+ import {
356
+ Button as RACButton,
357
+ composeRenderProps,
358
+ type ButtonProps as RACButtonProps,
359
+ type ButtonRenderProps,
360
+ } from 'react-aria-components';
361
+ import { ntv } from 'nestable-tailwind-variants';
165
362
 
166
- export default [
167
- {
168
- settings: {
169
- 'better-tailwindcss': {
170
- callees: [...getDefaultCallees(), ['ntv', [{ match: 'objectValues' }]]],
171
- },
363
+ interface ButtonStyleProps {
364
+ variant?: 'primary' | 'secondary';
365
+ size?: 'sm' | 'md' | 'lg';
366
+ }
367
+
368
+ const button = ntv<ButtonRenderProps & ButtonStyleProps>({
369
+ $base:
370
+ 'inline-flex items-center justify-center rounded-md px-4 py-2 font-medium transition-colors focus:outline-none',
371
+ variant: {
372
+ primary: {
373
+ $default: 'bg-blue-500 text-white',
374
+ isHovered: 'bg-blue-600',
375
+ isPressed: 'bg-blue-700',
376
+ isFocusVisible: 'ring-2 ring-blue-500 ring-offset-2',
377
+ },
378
+ secondary: {
379
+ $default: 'bg-gray-200 text-gray-800',
380
+ isHovered: 'bg-gray-300',
381
+ isPressed: 'bg-gray-400',
382
+ isFocusVisible: 'ring-2 ring-gray-500 ring-offset-2',
172
383
  },
173
384
  },
174
- ];
385
+ size: {
386
+ sm: 'h-8 px-3 text-sm',
387
+ md: 'h-10 px-4 text-base',
388
+ lg: 'h-12 px-6 text-lg',
389
+ },
390
+ isDisabled: 'opacity-50 cursor-not-allowed',
391
+ });
392
+
393
+ function Button({
394
+ variant = 'primary',
395
+ size = 'md',
396
+ children,
397
+ ...props
398
+ }: RACButtonProps & ButtonStyleProps) {
399
+ return (
400
+ <RACButton
401
+ {...props}
402
+ className={composeRenderProps(props.className, (className, renderProps) =>
403
+ // renderProps includes isHovered, isPressed, isFocusVisible, isDisabled
404
+ button({ ...renderProps, variant, size, className }),
405
+ )}
406
+ >
407
+ {children}
408
+ </RACButton>
409
+ );
410
+ }
175
411
  ```
176
412
 
177
- ## API
413
+ ## Type Safety
178
414
 
179
- ### `ntv<Props>(style)`
415
+ The recommended approach is to define a props type and pass it as a type argument to `ntv`. This provides clear documentation, better IDE support, and ensures type safety:
180
416
 
181
- Creates a style function from a nested style definition.
417
+ ```ts
418
+ interface ButtonProps {
419
+ variant: 'primary' | 'secondary';
420
+ size: 'sm' | 'md' | 'lg';
421
+ isDisabled: boolean;
422
+ }
182
423
 
183
- - `style` - Style definition object
184
- - `default` - Base styles (skipped when other conditions match at the same level)
185
- - `[variantKey]` - Style definitions for each variant value
186
- - `is*` / `allows*` - Styles applied when the boolean prop is `true`
187
- - Returns `(props: Partial<Props>) => string`
424
+ const button = ntv<ButtonProps>({
425
+ $base: 'rounded',
426
+ variant: {
427
+ primary: 'bg-blue-500',
428
+ secondary: 'bg-gray-200',
429
+ },
430
+ size: {
431
+ sm: 'text-sm',
432
+ md: 'text-base',
433
+ lg: 'text-lg',
434
+ },
435
+ isDisabled: 'opacity-50',
436
+ });
188
437
 
189
- ### `composeNtv(...fns)`
438
+ button({ variant: 'primary', size: 'lg' }); // ✅ OK
439
+ button({ variant: 'tertiary' }); // ❌ Error: 'tertiary' is not assignable
440
+ ```
190
441
 
191
- Composes multiple style functions into a single function.
442
+ Omitting the type argument when the scheme has keys disables type checking. Provide explicit type arguments when possible for better type safety.
192
443
 
193
- - `fns` - Style functions to compose
194
- - Returns `(props: Partial<Props>) => string`
444
+ ## Options
195
445
 
196
- ### `createNTV(options)`
446
+ `ntv` accepts an options object as the second argument. For `mergeNtv`, use `mergeNtvWithOptions` to pass options.
197
447
 
198
- Creates a customized `ntv` function.
448
+ ### Disabling tailwind-merge
199
449
 
200
- - `options.twMerge` (`boolean`, default: `true`) - Enable tailwind-merge
201
- - `options.twMergeConfig` (`object`) - Custom tailwind-merge configuration
202
- - Returns customized `ntv` function
450
+ By default, [tailwind-merge](https://github.com/dcastil/tailwind-merge) is used to resolve class conflicts. To disable it, pass `{ twMerge: false }`.
203
451
 
204
- ```tsx
205
- const ntvNoMerge = createNTV({ twMerge: false });
452
+ For `ntv`:
453
+
454
+ ```ts
455
+ const styles = ntv(
456
+ {
457
+ $base: 'p-4',
458
+ },
459
+ { twMerge: false },
460
+ );
461
+
462
+ styles({ class: 'p-8' });
463
+ // => 'p-4 p-8' (no merge, both classes kept)
464
+ ```
465
+
466
+ For `mergeNtvWithOptions`:
206
467
 
207
- const customNTV = createNTV({
468
+ ```ts
469
+ const styles = mergeNtvWithOptions(baseStyles, overrideStyles)({ twMerge: false });
470
+ ```
471
+
472
+ ### Custom tailwind-merge configuration
473
+
474
+ If you've extended Tailwind with custom classes (e.g., custom font sizes via `@theme`), you need to tell tailwind-merge about them so it can resolve conflicts correctly:
475
+
476
+ ```css
477
+ /* app.css */
478
+ @theme {
479
+ --font-size-huge: 4rem;
480
+ }
481
+ ```
482
+
483
+ For `ntv`:
484
+
485
+ ```ts
486
+ const heading = ntv(
487
+ { $base: 'text-huge' },
488
+ {
489
+ twMergeConfig: {
490
+ extend: {
491
+ classGroups: {
492
+ 'font-size': [{ text: ['huge'] }],
493
+ },
494
+ },
495
+ },
496
+ },
497
+ );
498
+
499
+ heading({ class: 'text-6xl' });
500
+ // => 'text-6xl' (text-huge is correctly replaced by text-6xl)
501
+ ```
502
+
503
+ For `mergeNtvWithOptions`:
504
+
505
+ ```ts
506
+ const styles = mergeNtvWithOptions(
507
+ baseStyles,
508
+ overrideStyles,
509
+ )({
208
510
  twMergeConfig: {
209
511
  extend: {
210
- theme: {
211
- shadow: ['100', '200', '300'],
512
+ classGroups: {
513
+ 'font-size': [{ text: ['huge'] }],
212
514
  },
213
515
  },
214
516
  },
215
517
  });
216
518
  ```
217
519
 
218
- ### `createComposeNtv(options)`
520
+ Without this configuration, tailwind-merge wouldn't recognize `text-huge` as a font-size class and would keep both `text-huge` and `text-6xl`.
219
521
 
220
- Creates a customized `composeNtv` function.
522
+ ### Pre-configured factories
221
523
 
222
- - `options.twMerge` (`boolean`, default: `true`) - Enable tailwind-merge
223
- - `options.twMergeConfig` (`object`) - Custom tailwind-merge configuration
224
- - Returns customized `composeNtv` function
524
+ Use `createNtv` or `createMergeNtv` to create functions with shared options:
225
525
 
226
- ```tsx
227
- const composeNtvNoMerge = createComposeNtv({ twMerge: false });
526
+ ```ts
527
+ import { createNtv, createMergeNtv, type TwMergeConfig } from 'nestable-tailwind-variants';
228
528
 
229
- const customComposeNtv = createComposeNtv({
230
- twMergeConfig: {
231
- extend: {
232
- theme: {
233
- shadow: ['100', '200', '300'],
529
+ const twMergeConfig: TwMergeConfig = {
530
+ extend: {
531
+ classGroups: {
532
+ 'font-size': [{ text: ['huge', 'tiny'] }],
533
+ },
534
+ },
535
+ };
536
+
537
+ const ntv = createNtv({ twMergeConfig });
538
+ const mergeNtv = createMergeNtv({ twMergeConfig });
539
+ ```
540
+
541
+ ## Tooling
542
+
543
+ ### Tailwind CSS IntelliSense
544
+
545
+ Add to `.vscode/settings.json` for [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) class autocomplete:
546
+
547
+ ```json
548
+ {
549
+ "tailwindCSS.experimental.classRegex": ["ntv\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
550
+ }
551
+ ```
552
+
553
+ ### prettier-plugin-tailwindcss
554
+
555
+ Add to your Prettier config for [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) automatic class sorting:
556
+
557
+ ```json
558
+ {
559
+ "tailwindFunctions": ["ntv"]
560
+ }
561
+ ```
562
+
563
+ ### eslint-plugin-better-tailwindcss
564
+
565
+ Add to your ESLint config for [eslint-plugin-better-tailwindcss](https://github.com/schoero/eslint-plugin-better-tailwindcss) linting:
566
+
567
+ ```js
568
+ import { getDefaultCallees } from 'eslint-plugin-better-tailwindcss/defaults';
569
+
570
+ export default [
571
+ {
572
+ settings: {
573
+ 'better-tailwindcss': {
574
+ callees: [...getDefaultCallees(), ['ntv', [{ match: 'objectValues' }]]],
234
575
  },
235
576
  },
236
577
  },
237
- });
578
+ ];
238
579
  ```
580
+
581
+ ## API Reference
582
+
583
+ ### Functions
584
+
585
+ | Function | Description |
586
+ | -------------------------------------------- | ------------------------------------------------------- |
587
+ | `ntv(scheme, options?)` | Create a style function from a scheme |
588
+ | `createNtv(options)` | Create a pre-configured `ntv` with default options |
589
+ | `mergeNtv(...styleFns)` | Merge multiple style functions into one |
590
+ | `mergeNtvWithOptions(...styleFns)(options?)` | Merge style functions with custom options |
591
+ | `createMergeNtv(options)` | Create a pre-configured `mergeNtv` with default options |
592
+
593
+ ### Types
594
+
595
+ | Type | Description |
596
+ | --------------- | ------------------------------------------------------------------------------------------- |
597
+ | `ClassValue` | Valid class value (string, array, or object) |
598
+ | `ClassProp` | Props for runtime class override (`{ class?: ClassValue }` or `{ className?: ClassValue }`) |
599
+ | `NtvOptions` | Options for ntv functions (`{ twMerge?: boolean; twMergeConfig?: TwMergeConfig }`) |
600
+ | `TwMergeConfig` | Configuration object for tailwind-merge |
601
+
602
+ ### Scheme Properties
603
+
604
+ | Property | Description |
605
+ | -------------- | ---------------------------------------------------- |
606
+ | `$base` | Classes always applied (top-level only) |
607
+ | `$default` | Fallback classes when no conditions match |
608
+ | `is[A-Z]*` | Boolean condition (e.g., `isSelected`, `isDisabled`) |
609
+ | `allows[A-Z]*` | Boolean condition (e.g., `allowsRemoving`) |
610
+ | `[key]` | Variant object mapping values to classes |
611
+
612
+ ## License
613
+
614
+ MIT