classname-variants 1.6.0 → 1.7.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/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2022 Felix Gnass
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -12,7 +12,28 @@ Library to create type-safe components that render their class name based on a s
12
12
 
13
13
  ![npm bundle size](https://img.shields.io/bundlephobia/minzip/classname-variants)
14
14
 
15
- # Examples
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install classname-variants
19
+ ```
20
+
21
+ ## Import Paths
22
+
23
+ ```ts
24
+ // Core — vanilla DOM, no framework dependency
25
+ import { variants, classNames, tw } from "classname-variants";
26
+
27
+ // React — styled components, variantProps, type utilities
28
+ import { styled, variantProps, tw } from "classname-variants/react";
29
+ import type { VariantPropsOf } from "classname-variants/react";
30
+
31
+ // Preact — same API, accepts both `class` and `className`
32
+ import { styled, variantProps, tw } from "classname-variants/preact";
33
+ import type { VariantPropsOf } from "classname-variants/preact";
34
+ ```
35
+
36
+ ## Examples
16
37
 
17
38
  Here is an example that uses React and Tailwind CSS:
18
39
 
@@ -36,11 +57,9 @@ function UsageExample() {
36
57
  }
37
58
  ```
38
59
 
39
- [![Edit classname-variants/react](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/classname-variants-react-3bzjl?fontsize=14&hidenavigation=1&theme=dark)
40
-
41
60
  While the library has been designed with tools like Tailwind in mind, it can be also used with custom classes or CSS modules:
42
61
 
43
- ## Preact + CSS modules
62
+ ### Preact + CSS modules
44
63
 
45
64
  ```tsx
46
65
  import { styled } from "classname-variants/preact";
@@ -56,7 +75,7 @@ const Button = styled("button", {
56
75
  });
57
76
  ```
58
77
 
59
- ## Vanilla DOM
78
+ ### Vanilla DOM
60
79
 
61
80
  The core of the library is completely framework-agnostic:
62
81
 
@@ -80,7 +99,7 @@ document.write(`
80
99
  `);
81
100
  ```
82
101
 
83
- # API
102
+ ## API
84
103
 
85
104
  ### Defining variants
86
105
 
@@ -121,6 +140,8 @@ Variants can be typed as `boolean` by using `true` / `false` as key:
121
140
  <Button primary>Click Me!</Button>
122
141
  ```
123
142
 
143
+ ### Compound variants
144
+
124
145
  The `compoundVariants` option can be used to apply class names based on a combination of other variants:
125
146
 
126
147
  ```ts
@@ -188,6 +209,114 @@ import { styled } from "classname-variants/react";
188
209
  const Button = styled("button", "bg-transparent border p-2");
189
210
  ```
190
211
 
212
+ ### Default props
213
+
214
+ If your underlying element (or custom component) expects props that you want to
215
+ provide automatically, you can use the `defaultProps` option. All defaulted
216
+ props become optional in TypeScript – even when you later render the component
217
+ with a polymorphic `as` prop.
218
+
219
+ ```tsx
220
+ const Button = styled("button", {
221
+ base: "inline-flex items-center gap-2",
222
+ defaultProps: {
223
+ type: "button",
224
+ },
225
+ });
226
+
227
+ // `type` is optional but still overridable
228
+ <Button />;
229
+ <Button type="submit" />;
230
+
231
+ // Works together with `as`
232
+ <Button as="a" href="/docs" />;
233
+ ```
234
+
235
+ ### Forwarding props
236
+
237
+ When a variant mirrors an existing prop (such as `disabled` on a button), add
238
+ it to `forwardProps` so the resolved value is passed through to the rendered
239
+ element or custom component.
240
+
241
+ ```tsx
242
+ const Button = styled("button", {
243
+ variants: {
244
+ disabled: {
245
+ true: "cursor-not-allowed",
246
+ },
247
+ },
248
+ forwardProps: ["disabled"],
249
+ });
250
+
251
+ // Renders with both the class name and the DOM `disabled` prop applied.
252
+ <Button disabled />;
253
+ ```
254
+
255
+ ### Chaining additional class names
256
+
257
+ Styled components accept a `className` prop that gets merged with the variant output. This is useful for one-off overrides:
258
+
259
+ ```tsx
260
+ <Button className="mt-4" size="large">Submit</Button>
261
+ ```
262
+
263
+ The Preact adapter accepts both `class` and `className` — use whichever you prefer:
264
+
265
+ ```tsx
266
+ <Button class="mt-4" size="large">Submit</Button>
267
+ ```
268
+
269
+ ### Ref forwarding
270
+
271
+ All `styled()` components support refs via `React.forwardRef`:
272
+
273
+ ```tsx
274
+ const Input = styled("input", {
275
+ base: "border rounded px-2",
276
+ variants: { ... },
277
+ });
278
+
279
+ const ref = useRef<HTMLInputElement>(null);
280
+ <Input ref={ref} />;
281
+ ```
282
+
283
+ ### `variantProps()`
284
+
285
+ The lower-level `variantProps()` function lets you separate variant logic from rendering. This is useful for headless components or when you need more control:
286
+
287
+ ```tsx
288
+ import { variantProps } from "classname-variants/react";
289
+
290
+ const buttonProps = variantProps({
291
+ base: "rounded px-4 py-2",
292
+ variants: {
293
+ intent: {
294
+ primary: "bg-teal-500 text-white",
295
+ secondary: "bg-slate-200",
296
+ },
297
+ },
298
+ });
299
+
300
+ function Button(props) {
301
+ // Extracts variant props, returns { className, ...rest }
302
+ const { className, ...rest } = buttonProps(props);
303
+ return <button className={className} {...rest} />;
304
+ }
305
+ ```
306
+
307
+ ### `VariantPropsOf<T>`
308
+
309
+ Use this utility type to extract the variant props accepted by a `variantProps` function — helpful when building wrapper components:
310
+
311
+ ```tsx
312
+ import { variantProps, type VariantPropsOf } from "classname-variants/react";
313
+
314
+ const buttonProps = variantProps({ ... });
315
+
316
+ type ButtonProps = VariantPropsOf<typeof buttonProps>;
317
+ // { intent: "primary" | "secondary"; className?: string }
318
+ ```
319
+
191
320
  ### Styling custom components
192
321
 
193
322
  You can style any custom React/Preact component as long as they accept a `className` prop (or `class` in case of Preact).
@@ -250,7 +379,24 @@ import { twMerge } from "tailwind-merge";
250
379
  classNames.combine = twMerge;
251
380
  ```
252
381
 
253
- # Tailwind IntelliSense
382
+ ## Why classname-variants?
383
+
384
+ ### vs clsx / classnames
385
+
386
+ - **Type safety** — full TypeScript inference for variant props instead of manual conditional logic
387
+ - **Variant system** — built-in support for default values, compound variants, and boolean variants
388
+ - **Framework bindings** — `styled()` creates ready-to-use React/Preact components
389
+
390
+ ### vs class-variance-authority (cva)
391
+
392
+ - **Zero dependencies** — no external runtime dependencies
393
+ - **Framework integration** — built-in `styled()` API with polymorphic `as` prop, ref forwarding, and `defaultProps`
394
+ - **Prop forwarding** — `forwardProps` maps variant values to DOM attributes (e.g. `disabled`)
395
+ - **TypeScript-first** — designed around type inference rather than requiring manual `VariantProps` extraction
396
+
397
+ If you're coming from cva: `cva()` maps to `variants()`, and `VariantProps<typeof x>` maps to `VariantPropsOf<typeof x>`.
398
+
399
+ ## Tailwind IntelliSense
254
400
 
255
401
  In order to get auto-completion for the CSS classes themselves, you can use the [Tailwind CSS IntelliSense](https://github.com/tailwindlabs/tailwindcss-intellisense) plugin for VS Code. In order to make it recognize the strings inside your variants-config, you have to somehow mark them and configure the plugin accordingly.
256
402
 
@@ -270,21 +416,17 @@ const button = variants({
270
416
  });
271
417
  ```
272
418
 
273
- You can then add the following line to your `settings.json`:
274
-
275
- ```
276
- "tailwindCSS.experimental.classRegex": ["tw`(.+?)`"]
277
- ```
419
+ You can then set the _Tailwind CSS: Class Functions_ option to `tw`.
278
420
 
279
421
  > [!NOTE]
280
422
  > The `tw` helper function is just an alias for [`String.raw()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw) which has the nice side effect backslashes are not treated as [escape character in JSX](https://tailwindcss.com/docs/adding-custom-styles#handling-whitespace).
281
423
 
282
424
  In order to get type coverage even for your Tailwind classes, you can use a tool like [tailwind-ts](https://github.com/mathieutu/tailwind-ts).
283
425
 
284
- # For AI Assistants
426
+ ## For AI Assistants
285
427
 
286
428
  For comprehensive technical documentation optimized for LLMs, see [`llms.txt`](./llms.txt).
287
429
 
288
- # License
430
+ ## License
289
431
 
290
432
  MIT
package/biome.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "formatter": {
9
+ "enabled": true,
10
+ "indentStyle": "space",
11
+ "indentWidth": 2
12
+ },
13
+ "linter": {
14
+ "enabled": true,
15
+ "rules": {
16
+ "recommended": true,
17
+ "correctness": {
18
+ "noUnusedImports": {
19
+ "options": {},
20
+ "level": "warn",
21
+ "fix": "safe"
22
+ }
23
+ },
24
+ "complexity": {
25
+ "noBannedTypes": "off"
26
+ },
27
+ "style": {
28
+ "noNonNullAssertion": "off"
29
+ },
30
+ "suspicious": {
31
+ "noExplicitAny": "off"
32
+ }
33
+ }
34
+ },
35
+ "javascript": {
36
+ "formatter": {
37
+ "quoteStyle": "double"
38
+ }
39
+ },
40
+ "assist": {
41
+ "enabled": true,
42
+ "actions": {
43
+ "source": {
44
+ "organizeImports": "on",
45
+ "useSortedAttributes": "on"
46
+ }
47
+ }
48
+ }
49
+ }
@@ -1,8 +1,8 @@
1
+ import { h, render } from "preact";
1
2
  import { createElement } from "react";
2
3
  import { createRoot } from "react-dom/client";
3
- import { render, h } from "preact";
4
- import { ReactApp } from "./react";
5
4
  import { PreactApp } from "./preact";
5
+ import { ReactApp } from "./react";
6
6
  const root = createRoot(document.getElementById("react-root"));
7
7
  root.render(createElement(ReactApp));
8
8
  render(h(PreactApp, {}), document.getElementById("preact-root"));
@@ -1,11 +1,17 @@
1
1
  /** @jsx h */
2
- import { styled } from "../preact";
2
+ // biome-ignore lint/correctness/noUnusedImports: Preact JSX pragma requires importing h
3
3
  import { h } from "preact";
4
+ import { styled } from "../preact";
4
5
  function CustomComponent({ title, ...props }) {
5
6
  return h("div", { ...props }, title);
6
7
  }
7
8
  const Card = styled("div", "bg-white p-4 border-2 rounded-lg");
8
- const TitleCard = styled(CustomComponent, "bg-white p-4 border-2 rounded-lg");
9
+ const TitleCard = styled(CustomComponent, {
10
+ base: "bg-white p-4 border-2 rounded-lg",
11
+ defaultProps: {
12
+ title: "Default Title",
13
+ },
14
+ });
9
15
  const Button = styled("button", {
10
16
  base: "px-5 py-2 text-white disabled:bg-gray-400 disabled:text-gray-300",
11
17
  variants: {
@@ -24,6 +30,9 @@ const Button = styled("button", {
24
30
  true: "rounded-full",
25
31
  false: "rounded-sm",
26
32
  },
33
+ disabled: {
34
+ true: "cursor-not-allowed",
35
+ },
27
36
  },
28
37
  compoundVariants: [
29
38
  {
@@ -34,6 +43,10 @@ const Button = styled("button", {
34
43
  defaultVariants: {
35
44
  color: "neutral",
36
45
  },
46
+ defaultProps: {
47
+ type: "button",
48
+ },
49
+ forwardProps: ["disabled"],
37
50
  });
38
51
  export const ExpectErrors = styled("div", {
39
52
  variants: {
@@ -53,20 +66,25 @@ export const ExpectErrors = styled("div", {
53
66
  //@ts-expect-error
54
67
  outlined: true,
55
68
  },
69
+ forwardProps: [
70
+ //@ts-expect-error
71
+ "outlined",
72
+ ],
56
73
  });
57
74
  export function WithErrors() {
58
75
  return (h("div", null,
59
76
  h(Button, { foo: true, size: "medium" }, "unknown property"),
60
77
  h(Card, { foo: true }, "Unknown property"),
61
- h(Button, { size: "medium", color: "foo" }, "Invalid variant"),
78
+ h(Button, { color: "foo", size: "medium" }, "Invalid variant"),
62
79
  h(Button, null, "Missing size")));
63
80
  }
64
81
  export function PreactApp() {
65
82
  return (h("div", { className: "flex justify-center items-center pt-8 gap-4 flex-wrap" },
66
- h(Button, { size: "medium", onClick: console.log }, "Neutral"),
67
- h(Button, { size: "medium", rounded: true }, "Neutral + Rounded"),
68
- h(Button, { size: "medium", color: "accent", outlined: true }, "Accent + Outlined"),
69
- h(Button, { size: "medium", color: "accent", disabled: true }, "Disabled"),
83
+ h(Button, { onClick: console.log, size: "medium" }, "Neutral"),
84
+ h(Button, { rounded: true, size: "medium" }, "Neutral + Rounded"),
85
+ h(Button, { color: "accent", outlined: true, size: "medium" }, "Accent + Outlined"),
86
+ h(Button, { color: "accent", disabled: true, size: "medium" }, "Disabled"),
87
+ h(TitleCard, null),
70
88
  h(TitleCard, { title: "Hello" }),
71
89
  h(Card, null,
72
90
  h("h1", null, "Hello"),
@@ -1,14 +1,14 @@
1
1
  import React from "react";
2
- export declare const StyledWithoutVariants: <As extends React.ElementType<any> = "div">(props: {
2
+ export declare const StyledWithoutVariants: <As extends React.ElementType<any, keyof React.JSX.IntrinsicElements> = "div">(props: {
3
3
  as?: As | undefined;
4
4
  } & Omit<React.ComponentProps<As>, "as"> & {} & {}) => React.ReactElement<any, string | React.JSXElementConstructor<any>> | null;
5
- export declare const TestBaseOnly: <As extends React.ElementType<any> = "div">(props: {
5
+ export declare const TestBaseOnly: <As extends React.ElementType<any, keyof React.JSX.IntrinsicElements> = "div">(props: {
6
6
  as?: As | undefined;
7
7
  } & Omit<React.ComponentProps<As>, "as"> & {} & {}) => React.ReactElement<any, string | React.JSXElementConstructor<any>> | null;
8
- export declare const ExpectErrors: <As extends React.ElementType<any> = "div">(props: {
8
+ export declare const ExpectErrors: <As extends React.ElementType<any, keyof React.JSX.IntrinsicElements> = "div">(props: {
9
9
  as?: As | undefined;
10
10
  } & Omit<React.ComponentProps<As>, "as" | "color"> & {
11
11
  color: "neutral" | "accent";
12
12
  } & {}) => React.ReactElement<any, string | React.JSXElementConstructor<any>> | null;
13
- export declare function WithErrors(): JSX.Element;
14
- export declare function ReactApp(): JSX.Element;
13
+ export declare function WithErrors(): React.JSX.Element;
14
+ export declare function ReactApp(): React.JSX.Element;
@@ -1,10 +1,17 @@
1
+ // biome-ignore lint/correctness/noUnusedImports: React import required for JSX runtime configuration
1
2
  import React from "react";
2
3
  import { styled } from "../react";
3
4
  function CustomComponent({ title, ...props }) {
4
5
  return React.createElement("div", { ...props }, title);
5
6
  }
6
7
  const Card = styled("div", "bg-white p-4 border-2 rounded-lg");
7
- const TitleCard = styled(CustomComponent, "bg-white p-4 border-2 rounded-lg");
8
+ const _cardAsShouldBeGeneric = "a";
9
+ const TitleCard = styled(CustomComponent, {
10
+ base: "bg-white p-4 border-2 rounded-lg",
11
+ defaultProps: {
12
+ title: "Default Title",
13
+ },
14
+ });
8
15
  const Button = styled("button", {
9
16
  base: "px-5 py-2 text-white disabled:bg-gray-400 disabled:text-gray-300",
10
17
  variants: {
@@ -23,6 +30,9 @@ const Button = styled("button", {
23
30
  true: "rounded-full",
24
31
  false: "rounded-sm",
25
32
  },
33
+ disabled: {
34
+ true: "cursor-not-allowed",
35
+ },
26
36
  },
27
37
  compoundVariants: [
28
38
  {
@@ -33,6 +43,10 @@ const Button = styled("button", {
33
43
  defaultVariants: {
34
44
  color: "neutral",
35
45
  },
46
+ defaultProps: {
47
+ type: "button",
48
+ },
49
+ forwardProps: ["disabled"],
36
50
  });
37
51
  export const StyledWithoutVariants = styled("div", {
38
52
  base: "bg-white",
@@ -58,21 +72,26 @@ export const ExpectErrors = styled("div", {
58
72
  //@ts-expect-error
59
73
  outlined: true,
60
74
  },
75
+ forwardProps: [
76
+ //@ts-expect-error
77
+ "outlined",
78
+ ],
61
79
  });
62
80
  export function WithErrors() {
63
81
  return (React.createElement("div", null,
64
82
  React.createElement(Button, { foo: true, size: "medium" }, "unknown property"),
65
83
  React.createElement(Card, { foo: true }, "Unknown property"),
66
- React.createElement(Button, { size: "medium", color: "foo" }, "Invalid variant"),
84
+ React.createElement(Button, { color: "foo", size: "medium" }, "Invalid variant"),
67
85
  React.createElement(Button, null, "Missing size"),
68
86
  React.createElement(Card, { as: "b", href: "https://example.com" }, "B tags don't have a href attribute")));
69
87
  }
70
88
  export function ReactApp() {
71
89
  return (React.createElement("div", { className: "flex justify-center items-center pt-8 gap-4 flex-wrap" },
72
- React.createElement(Button, { size: "medium", onClick: console.log }, "Neutral"),
73
- React.createElement(Button, { size: "medium", rounded: true }, "Neutral + Rounded"),
74
- React.createElement(Button, { size: "medium", color: "accent", outlined: true }, "Accent + Outlined"),
75
- React.createElement(Button, { size: "medium", color: "accent", disabled: true }, "Disabled"),
90
+ React.createElement(Button, { onClick: console.log, size: "medium" }, "Neutral"),
91
+ React.createElement(Button, { rounded: true, size: "medium" }, "Neutral + Rounded"),
92
+ React.createElement(Button, { color: "accent", outlined: true, size: "medium" }, "Accent + Outlined"),
93
+ React.createElement(Button, { color: "accent", disabled: true, size: "medium" }, "Disabled"),
94
+ React.createElement(TitleCard, null),
76
95
  React.createElement(TitleCard, { title: "Hello" }),
77
96
  React.createElement(Card, null,
78
97
  React.createElement("h1", null, "Hello"),
package/lib/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export interface VariantsConfig<V extends Variants = {}> {
21
21
  variants: V;
22
22
  compoundVariants?: CompoundVariant<V>[];
23
23
  defaultVariants?: Partial<OptionsOf<V>>;
24
+ forwardProps?: ReadonlyArray<Extract<keyof V, string>>;
24
25
  }
25
26
  /**
26
27
  * Rules for class names that are applied for certain variant combinations.
package/lib/index.js CHANGED
@@ -14,12 +14,12 @@ export function variants(config) {
14
14
  var _a, _b;
15
15
  return (_b = (_a = props[name]) !== null && _a !== void 0 ? _a : defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[name]) !== null && _b !== void 0 ? _b : (isBooleanVariant(name) ? false : undefined);
16
16
  };
17
- for (let name in variants) {
17
+ for (const name in variants) {
18
18
  const selected = getSelected(name);
19
19
  if (selected !== undefined)
20
20
  classes.push((_a = variants[name]) === null || _a === void 0 ? void 0 : _a[selected]);
21
21
  }
22
- for (let { variants, className } of compoundVariants !== null && compoundVariants !== void 0 ? compoundVariants : []) {
22
+ for (const { variants, className } of compoundVariants !== null && compoundVariants !== void 0 ? compoundVariants : []) {
23
23
  const isSelected = (name) => getSelected(name) === variants[name];
24
24
  if (Object.keys(variants).every(isSelected)) {
25
25
  classes.push(className);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { classNames, tw, variants } from "./index.js";
3
+ describe("classNames.combine", () => {
4
+ it("joins truthy values with spaces", () => {
5
+ expect(classNames.combine("foo", undefined, "bar", false, "baz")).toBe("foo bar baz");
6
+ });
7
+ it("returns empty string for falsy input", () => {
8
+ expect(classNames.combine(undefined, null, false, 0)).toBe("");
9
+ });
10
+ });
11
+ describe("tw helper", () => {
12
+ it("returns raw template strings", () => {
13
+ expect(tw `foo`).toBe("foo");
14
+ const dynamic = "bar";
15
+ expect(tw `foo-${dynamic}`).toBe("foo-bar");
16
+ });
17
+ });
18
+ describe("variants factory", () => {
19
+ const config = {
20
+ base: "btn",
21
+ variants: {
22
+ tone: {
23
+ neutral: "text-neutral",
24
+ accent: "text-accent",
25
+ },
26
+ size: {
27
+ small: "text-sm",
28
+ large: "text-lg",
29
+ },
30
+ disabled: {
31
+ true: "opacity-50",
32
+ false: "opacity-100",
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ tone: "neutral",
37
+ size: "small",
38
+ },
39
+ compoundVariants: [
40
+ {
41
+ variants: { tone: "accent", size: "large" },
42
+ className: "text-accent-large",
43
+ },
44
+ {
45
+ variants: { disabled: true },
46
+ className: "pointer-events-none",
47
+ },
48
+ ],
49
+ };
50
+ const button = variants(config);
51
+ it("includes base class", () => {
52
+ expect(button({ tone: "neutral", size: "small" })).toContain("btn");
53
+ });
54
+ it("uses default variants when omitted", () => {
55
+ const result = button({});
56
+ expect(result).toContain("text-neutral");
57
+ expect(result).toContain("text-sm");
58
+ expect(result).not.toContain("text-lg");
59
+ });
60
+ it("applies boolean variants with false default", () => {
61
+ const result = button({});
62
+ expect(result).toContain("opacity-100");
63
+ });
64
+ it("prefers explicit props over defaults", () => {
65
+ const result = button({ tone: "accent", size: "large" });
66
+ expect(result).toContain("text-accent");
67
+ expect(result).toContain("text-lg");
68
+ expect(result).not.toContain("text-neutral");
69
+ expect(result).not.toContain("text-sm");
70
+ });
71
+ it("handles boolean true variants", () => {
72
+ const result = button({ disabled: true });
73
+ expect(result).toContain("opacity-50");
74
+ expect(result).toContain("pointer-events-none");
75
+ });
76
+ it("runs compound variants only when all predicates match", () => {
77
+ const accentLarge = button({ tone: "accent", size: "large" });
78
+ expect(accentLarge).toContain("text-accent-large");
79
+ const accentSmall = button({ tone: "accent", size: "small" });
80
+ expect(accentSmall).not.toContain("text-accent-large");
81
+ });
82
+ it("accepts boolean variants via explicit false", () => {
83
+ const result = button({ disabled: false });
84
+ expect(result).toContain("opacity-100");
85
+ expect(result).not.toContain("pointer-events-none");
86
+ });
87
+ it("keeps default class when variant explicitly undefined", () => {
88
+ const result = button({ tone: undefined, size: "large" });
89
+ expect(result).toContain("text-lg");
90
+ expect(result).toContain("text-neutral");
91
+ expect(result).not.toContain("text-accent");
92
+ });
93
+ });
package/lib/preact.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { JSX, ComponentType, ComponentProps, VNode } from "preact";
1
+ import type { ComponentProps, ComponentType, JSX, VNode } from "preact";
2
2
  type ElementType<P = any> = keyof JSX.IntrinsicElements | ComponentType<P>;
3
- import { Variants, VariantsConfig, VariantOptions, Simplify } from "./index.js";
3
+ import { type Simplify, type VariantOptions, type Variants, type VariantsConfig } from "./index.js";
4
4
  /**
5
5
  * Utility type to infer the first argument of a variantProps function.
6
6
  */
@@ -12,22 +12,40 @@ type VariantProps<C extends VariantsConfig<V>, V extends Variants = C["variants"
12
12
  class?: string;
13
13
  className?: string;
14
14
  };
15
+ type ForwardedVariantKeys<C> = C extends {
16
+ forwardProps?: ReadonlyArray<infer K>;
17
+ variants: infer V;
18
+ } ? Extract<K, keyof V> : never;
19
+ type VariantPropKeys<C> = C extends {
20
+ variants: infer V;
21
+ } ? keyof V : never;
22
+ type ForwardedVariantProps<C extends VariantsConfig<any>> = Pick<VariantOptions<C>, ForwardedVariantKeys<C>>;
15
23
  export declare function variantProps<C extends VariantsConfig<V>, V extends Variants = C["variants"]>(config: Simplify<C>): <P extends VariantProps<C, C["variants"]>>(props: P) => {
16
24
  class: string;
17
- } & Omit<P, keyof C["variants"]>;
18
- type VariantsOf<T, V> = T extends VariantsConfig ? V : {};
25
+ } & Omit<P, VariantPropKeys<C>> & ForwardedVariantProps<C>;
26
+ type VariantsOf<T> = T extends {
27
+ variants: infer TV;
28
+ } ? TV extends Variants ? TV : {} : {};
19
29
  type AsProps<T extends ElementType = ElementType> = {
20
30
  as?: T;
21
31
  };
32
+ type CleanDefaults<Defaults> = Exclude<Defaults, undefined>;
33
+ type DefaultedKeys<Props, Defaults> = Extract<keyof CleanDefaults<Defaults>, keyof Props>;
34
+ type WithDefaultProps<Props, Defaults> = [CleanDefaults<Defaults>] extends [
35
+ never
36
+ ] ? Props : Omit<Props, DefaultedKeys<Props, Defaults>> & Partial<Pick<Props, DefaultedKeys<Props, Defaults>>>;
22
37
  type PolymorphicComponentProps<V, T extends ElementType> = AsProps<T> & Omit<ComponentProps<T>, "as" | keyof V> & V;
23
- export declare function styled<T extends ElementType, C extends VariantsConfig<V>, V extends Variants = VariantsOf<C, C["variants"]>>(type: T, config: string | {
38
+ export declare function styled<T extends ElementType, C extends VariantsConfig<V>, V extends Variants = VariantsOf<C>, Defaults extends Partial<ComponentProps<T>> | undefined = undefined>(type: T, config: string | {
24
39
  base: string;
25
- } | Simplify<C>): <As extends ElementType = T>(props: PolymorphicComponentProps<typeof config extends string ? {} : typeof config extends {
40
+ defaultProps?: Defaults;
41
+ } | (Simplify<C> & {
42
+ defaultProps?: Defaults;
43
+ })): <As extends ElementType = T>(props: WithDefaultProps<PolymorphicComponentProps<typeof config extends string ? {} : typeof config extends {
26
44
  base: string;
27
45
  variants?: undefined;
28
46
  compoundVariants?: undefined;
29
47
  defaultVariants?: undefined;
30
- } ? {} : VariantOptions<C>, As>) => VNode | null;
48
+ } ? {} : VariantOptions<C>, As>, Defaults>) => VNode | null;
31
49
  /**
32
50
  * No-op function to mark template literals as tailwind strings.
33
51
  */
package/lib/preact.js CHANGED
@@ -1,16 +1,31 @@
1
1
  import { h } from "preact";
2
2
  import { forwardRef } from "preact/compat";
3
- import { variants, classNames, } from "./index.js";
3
+ import { classNames, variants, } from "./index.js";
4
4
  export function variantProps(config) {
5
5
  const variantClassName = variants(config);
6
6
  return (props) => {
7
7
  const result = {};
8
8
  // Pass-through all unrelated props
9
- for (let prop in props) {
9
+ for (const prop in props) {
10
10
  if (config.variants && !(prop in config.variants)) {
11
11
  result[prop] = props[prop];
12
12
  }
13
13
  }
14
+ if (config.forwardProps) {
15
+ for (const name of config.forwardProps) {
16
+ if (!config.variants || !(name in config.variants))
17
+ continue;
18
+ if (Object.hasOwn(props, name)) {
19
+ result[name] = props[name];
20
+ continue;
21
+ }
22
+ if (config.defaultVariants &&
23
+ Object.hasOwn(config.defaultVariants, name)) {
24
+ result[name] =
25
+ config.defaultVariants[name];
26
+ }
27
+ }
28
+ }
14
29
  // Add the optionally passed class/className prop for chaining
15
30
  result.class = classNames.combine(variantClassName(props), props.class, props.className);
16
31
  return result;
@@ -21,9 +36,26 @@ export function styled(type, config) {
21
36
  ? variantProps({ base: config, variants: {} })
22
37
  : "variants" in config
23
38
  ? variantProps(config)
24
- : variantProps({ base: config.base, variants: {} });
39
+ : variantProps({
40
+ base: config
41
+ .base,
42
+ variants: {},
43
+ });
44
+ const defaultProps = typeof config === "string" ? undefined : config.defaultProps;
45
+ const toRecord = (value) => value && typeof value === "object"
46
+ ? value
47
+ : {};
48
+ const readClass = (value) => typeof value === "string" ? value : undefined;
25
49
  const Component = forwardRef(({ as, ...props }, ref) => {
26
- return h(as !== null && as !== void 0 ? as : type, { ...styledProps(props), ref });
50
+ const defaultsRecord = toRecord(defaultProps);
51
+ const propsRecord = props;
52
+ const merged = {
53
+ ...defaultsRecord,
54
+ ...propsRecord,
55
+ };
56
+ merged.class = classNames.combine(readClass(defaultsRecord.class), readClass(propsRecord.class));
57
+ merged.className = classNames.combine(readClass(defaultsRecord.className), readClass(propsRecord.className));
58
+ return h(as !== null && as !== void 0 ? as : type, { ...styledProps(merged), ref });
27
59
  });
28
60
  return Component;
29
61
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { variantProps } from "./preact.js";
3
+ describe("preact adapter forwardProps", () => {
4
+ it("forwards explicit variant values", () => {
5
+ const config = {
6
+ base: "btn",
7
+ variants: {
8
+ disabled: {
9
+ true: "opacity-50",
10
+ },
11
+ size: {
12
+ small: "text-sm",
13
+ large: "text-lg",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ size: "small",
18
+ },
19
+ forwardProps: ["disabled"],
20
+ };
21
+ const mapProps = variantProps(config);
22
+ const result = mapProps({
23
+ disabled: true,
24
+ size: "large",
25
+ class: "custom",
26
+ });
27
+ expect(result.disabled).toBe(true);
28
+ expect(result.class).toContain("btn");
29
+ expect(result.class).toContain("opacity-50");
30
+ expect(result.class).toContain("text-lg");
31
+ expect(result.class).toContain("custom");
32
+ expect("size" in result).toBe(false);
33
+ });
34
+ it("uses default variant values when forwarding", () => {
35
+ const config = {
36
+ base: "btn",
37
+ variants: {
38
+ disabled: {
39
+ true: "opacity-50",
40
+ },
41
+ },
42
+ defaultVariants: {
43
+ disabled: true,
44
+ },
45
+ forwardProps: ["disabled"],
46
+ };
47
+ const mapProps = variantProps(config);
48
+ const result = mapProps({});
49
+ expect(result.disabled).toBe(true);
50
+ expect(result.class).toContain("opacity-50");
51
+ });
52
+ });
package/lib/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { ComponentProps, ElementType, ReactElement } from "react";
2
- import { Variants, VariantsConfig, VariantOptions, Simplify } from "./index.js";
1
+ import { type ComponentProps, type ElementType, type ReactElement } from "react";
2
+ import { type Simplify, type VariantOptions, type Variants, type VariantsConfig } from "./index.js";
3
3
  /**
4
4
  * Utility type to infer the first argument of a variantProps function.
5
5
  */
@@ -10,22 +10,40 @@ export type VariantPropsOf<T> = T extends (props: infer P) => any ? P : never;
10
10
  type VariantProps<C extends VariantsConfig<V>, V extends Variants = C["variants"]> = VariantOptions<C> & {
11
11
  className?: string;
12
12
  };
13
+ type ForwardedVariantKeys<C> = C extends {
14
+ forwardProps?: ReadonlyArray<infer K>;
15
+ variants: infer V;
16
+ } ? Extract<K, keyof V> : never;
17
+ type VariantPropKeys<C> = C extends {
18
+ variants: infer V;
19
+ } ? keyof V : never;
20
+ type ForwardedVariantProps<C extends VariantsConfig<any>> = Pick<VariantOptions<C>, ForwardedVariantKeys<C>>;
13
21
  export declare function variantProps<C extends VariantsConfig<V>, V extends Variants = C["variants"]>(config: Simplify<C>): <P extends VariantProps<C, C["variants"]>>(props: P) => {
14
22
  className: string;
15
- } & Omit<P, keyof C["variants"]>;
16
- type VariantsOf<T, V> = T extends VariantsConfig ? V : {};
23
+ } & Omit<P, VariantPropKeys<C>> & ForwardedVariantProps<C>;
24
+ type VariantsOf<T> = T extends {
25
+ variants: infer TV;
26
+ } ? TV extends Variants ? TV : {} : {};
17
27
  type AsProps<T extends ElementType = ElementType> = {
18
28
  as?: T;
19
29
  };
30
+ type CleanDefaults<Defaults> = Exclude<Defaults, undefined>;
31
+ type DefaultedKeys<Props, Defaults> = Extract<keyof CleanDefaults<Defaults>, keyof Props>;
32
+ type WithDefaultProps<Props, Defaults> = [CleanDefaults<Defaults>] extends [
33
+ never
34
+ ] ? Props : Omit<Props, DefaultedKeys<Props, Defaults>> & Partial<Pick<Props, DefaultedKeys<Props, Defaults>>>;
20
35
  type PolymorphicComponentProps<V, T extends ElementType> = AsProps<T> & Omit<ComponentProps<T>, "as" | keyof V> & V;
21
- export declare function styled<T extends ElementType, C extends VariantsConfig<V>, V extends Variants = VariantsOf<C, C["variants"]>>(type: T, config: string | {
36
+ export declare function styled<T extends ElementType, C extends VariantsConfig<V>, V extends Variants = VariantsOf<C>, Defaults extends Partial<ComponentProps<T>> | undefined = undefined>(type: T, config: string | {
22
37
  base: string;
23
- } | Simplify<C>): <As extends ElementType = T>(props: PolymorphicComponentProps<typeof config extends string ? {} : typeof config extends {
38
+ defaultProps?: Defaults;
39
+ } | (Simplify<C> & {
40
+ defaultProps?: Defaults;
41
+ })): <As extends ElementType = T>(props: WithDefaultProps<PolymorphicComponentProps<typeof config extends string ? {} : typeof config extends {
24
42
  base: string;
25
43
  variants?: undefined;
26
44
  compoundVariants?: undefined;
27
45
  defaultVariants?: undefined;
28
- } ? {} : VariantOptions<C>, As>) => ReactElement | null;
46
+ } ? {} : VariantOptions<C>, As>, Defaults>) => ReactElement | null;
29
47
  /**
30
48
  * No-op function to mark template literals as tailwind strings.
31
49
  */
package/lib/react.js CHANGED
@@ -1,15 +1,30 @@
1
1
  import { createElement, forwardRef, } from "react";
2
- import { variants, classNames, } from "./index.js";
2
+ import { classNames, variants, } from "./index.js";
3
3
  export function variantProps(config) {
4
4
  const variantClassName = variants(config);
5
5
  return (props) => {
6
6
  const result = {};
7
7
  // Pass-through all unrelated props
8
- for (let prop in props) {
8
+ for (const prop in props) {
9
9
  if (config.variants && !(prop in config.variants)) {
10
10
  result[prop] = props[prop];
11
11
  }
12
12
  }
13
+ if (config.forwardProps) {
14
+ for (const name of config.forwardProps) {
15
+ if (!config.variants || !(name in config.variants))
16
+ continue;
17
+ if (Object.hasOwn(props, name)) {
18
+ result[name] = props[name];
19
+ continue;
20
+ }
21
+ if (config.defaultVariants &&
22
+ Object.hasOwn(config.defaultVariants, name)) {
23
+ result[name] =
24
+ config.defaultVariants[name];
25
+ }
26
+ }
27
+ }
13
28
  // Add the optionally passed className prop for chaining
14
29
  result.className = classNames.combine(variantClassName(props), props.className);
15
30
  return result;
@@ -20,9 +35,25 @@ export function styled(type, config) {
20
35
  ? variantProps({ base: config, variants: {} })
21
36
  : "variants" in config
22
37
  ? variantProps(config)
23
- : variantProps({ base: config.base, variants: {} });
38
+ : variantProps({
39
+ base: config
40
+ .base,
41
+ variants: {},
42
+ });
43
+ const defaultProps = typeof config === "string" ? undefined : config.defaultProps;
44
+ const toRecord = (value) => value && typeof value === "object"
45
+ ? value
46
+ : {};
47
+ const readClassName = (value) => typeof value === "string" ? value : undefined;
24
48
  const Component = forwardRef(({ as, ...props }, ref) => {
25
- return createElement(as !== null && as !== void 0 ? as : type, { ...styledProps(props), ref });
49
+ const defaultsRecord = toRecord(defaultProps);
50
+ const propsRecord = props;
51
+ const merged = {
52
+ ...defaultsRecord,
53
+ ...propsRecord,
54
+ };
55
+ merged.className = classNames.combine(readClassName(defaultsRecord.className), readClassName(propsRecord.className));
56
+ return createElement(as !== null && as !== void 0 ? as : type, { ...styledProps(merged), ref });
26
57
  });
27
58
  return Component;
28
59
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ import * as React from "react";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { describe, expect, it } from "vitest";
4
+ import { styled, variantProps } from "./react.js";
5
+ describe("react adapter forwardProps", () => {
6
+ it("forwards explicit variant values", () => {
7
+ const config = {
8
+ base: "btn",
9
+ variants: {
10
+ disabled: {
11
+ true: "opacity-50",
12
+ },
13
+ tone: {
14
+ neutral: "text-slate-700",
15
+ accent: "text-teal-600",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ tone: "neutral",
20
+ },
21
+ forwardProps: ["disabled"],
22
+ };
23
+ const mapProps = variantProps(config);
24
+ const result = mapProps({
25
+ disabled: true,
26
+ tone: "accent",
27
+ className: "custom",
28
+ });
29
+ expect(result.disabled).toBe(true);
30
+ expect(result.className).toContain("btn");
31
+ expect(result.className).toContain("opacity-50");
32
+ expect(result.className).toContain("text-teal-600");
33
+ expect(result.className).toContain("custom");
34
+ expect("tone" in result).toBe(false);
35
+ });
36
+ it("uses default variant values when forwarding", () => {
37
+ const config = {
38
+ base: "btn",
39
+ variants: {
40
+ disabled: {
41
+ true: "opacity-50",
42
+ },
43
+ },
44
+ defaultVariants: {
45
+ disabled: true,
46
+ },
47
+ forwardProps: ["disabled"],
48
+ };
49
+ const mapProps = variantProps(config);
50
+ const result = mapProps({});
51
+ expect(result.disabled).toBe(true);
52
+ expect(result.className).toContain("opacity-50");
53
+ });
54
+ it("renders forwarded props through styled components", () => {
55
+ const buttonConfig = {
56
+ base: "btn",
57
+ variants: {
58
+ disabled: {
59
+ true: "opacity-50",
60
+ },
61
+ },
62
+ forwardProps: ["disabled"],
63
+ };
64
+ const Button = styled("button", buttonConfig);
65
+ const markup = renderToStaticMarkup(React.createElement(Button, {
66
+ className: "custom",
67
+ disabled: true,
68
+ }));
69
+ const host = document.createElement("div");
70
+ host.innerHTML = markup;
71
+ const button = host.querySelector("button");
72
+ expect(button).not.toBeNull();
73
+ expect(button.hasAttribute("disabled")).toBe(true);
74
+ expect(button.className).toContain("btn");
75
+ expect(button.className).toContain("opacity-50");
76
+ expect(button.className).toContain("custom");
77
+ });
78
+ });
package/llms.txt CHANGED
@@ -15,17 +15,28 @@ This library provides a variant API for generating class names based on componen
15
15
  - `tw` - Tagged template literal helper for Tailwind CSS IntelliSense
16
16
 
17
17
  ### Framework Integrations
18
- - `src/react.ts` - React-specific bindings with `styled()` and `variantProps()` functions
19
- - `src/preact.ts` - Preact-specific bindings (similar to React but uses Preact's JSX)
18
+ - `src/react.ts` - React-specific bindings with `styled()` and `variantProps()` functions, including runtime merging and type inference for `defaultProps`
19
+ - `src/preact.ts` - Preact-specific bindings (similar to React but uses Preact's JSX with `class` prop) with equivalent `defaultProps` support
20
+
21
+ ### Class Application Order
22
+ When generating class names, the library combines them in this order:
23
+ 1. `base` classes (always applied)
24
+ 2. Variant classes (based on selected or default variant values)
25
+ 3. Compound variant classes (when all conditions match)
26
+ 4. User-supplied `className` prop (React/Preact only, via chaining)
20
27
 
21
28
  ### Key Features
22
29
  - Type-safe variant definitions with full TypeScript inference
23
30
  - Support for required and optional variants
24
31
  - Boolean variants (true/false)
25
- - Default variants
32
+ - Default variants via `defaultVariants` (note: NOT "defaults")
26
33
  - Compound variants (combinations of multiple variant states)
34
+ - Forwarding variant values via `forwardProps`
27
35
  - Polymorphic components with "as" prop
28
- - Custom class name combination strategies
36
+ - Optional `defaultProps` with strong typing (defaulted props become optional while respecting polymorphic `as`)
37
+ - `className` prop chaining on styled components (user classes merged with variant output)
38
+ - Ref forwarding — all `styled()` components use `forwardRef` internally
39
+ - Works with any CSS framework (Tailwind, CSS modules, etc.) without additional dependencies
29
40
 
30
41
  ## File Structure
31
42
 
@@ -57,8 +68,10 @@ src/
57
68
  [optionName]: "class-names"
58
69
  }
59
70
  },
60
- defaultVariants?: {...}, // Default values
71
+ defaultVariants?: {...}, // IMPORTANT: Use "defaultVariants" for default variant values
72
+ defaultProps?: {...}, // DIFFERENT: Use "defaultProps" for non-variant props (e.g. { type: "button" })
61
73
  compoundVariants?: [...] // Conditional class combinations
74
+ forwardProps?: ["disabled", ...] // Variant keys to pass through as real props
62
75
  }
63
76
  ```
64
77
 
@@ -67,7 +80,9 @@ The library uses advanced TypeScript features to:
67
80
  - Infer variant prop types from configuration
68
81
  - Make variants required unless they have defaults or are boolean
69
82
  - Convert "true"/"false" keys to boolean props
83
+ - Infer optionality for props supplied via `defaultProps` while preserving `as` polymorphism
70
84
  - Provide full autocomplete and type checking
85
+ - Preserve variant prop ergonomics when using `forwardProps`; forwarded keys remain strongly typed and emitted alongside the generated className
71
86
 
72
87
  ## Usage Examples
73
88
 
@@ -86,9 +101,9 @@ import { styled, tw } from "classname-variants/react"
86
101
  // Preact
87
102
  import { styled, tw } from "classname-variants/preact"
88
103
 
89
- // Low-level API (rarely used directly)
90
- import { variantProps } from "classname-variants/react"
91
- import { variantProps } from "classname-variants/preact"
104
+ // Low-level API and type utilities
105
+ import { variantProps, type VariantPropsOf } from "classname-variants/react"
106
+ import { variantProps, type VariantPropsOf } from "classname-variants/preact"
92
107
  ```
93
108
 
94
109
  ### Styled Components Without Variants
@@ -99,6 +114,78 @@ const Card = styled("div", "bg-white p-4 border-2 rounded-lg");
99
114
  const CustomCard = styled(CustomComponent, "bg-white p-4 border-2 rounded-lg");
100
115
  ```
101
116
 
117
+ ### Default Props in Styled Components
118
+ Provide defaults for non-variant props while keeping TypeScript happy:
119
+
120
+ ```tsx
121
+ const Button = styled("button", {
122
+ base: "inline-flex items-center gap-2",
123
+ defaultProps: {
124
+ type: "button",
125
+ },
126
+ });
127
+
128
+ <Button />; // `type` inferred as optional
129
+ <Button type="submit" />; // Still overridable
130
+ <Button as="a" href="/docs" />; // Works with polymorphic `as`
131
+ ```
132
+
133
+ ### Forwarding Variant Values
134
+ Mirror a variant onto the rendered element to toggle native behaviour:
135
+
136
+ ```tsx
137
+ const Button = styled("button", {
138
+ base: "inline-flex items-center gap-2",
139
+ variants: {
140
+ disabled: {
141
+ true: "opacity-50",
142
+ },
143
+ },
144
+ forwardProps: ["disabled"],
145
+ });
146
+
147
+ // Applies the class names *and* the DOM `disabled` prop
148
+ <Button disabled />;
149
+ ```
150
+
151
+ ### className Prop Chaining
152
+ Styled components accept an additional `className` prop that gets merged with the variant output:
153
+
154
+ ```tsx
155
+ <Button className="mt-4" size="large">Submit</Button>
156
+ ```
157
+
158
+ The Preact adapter accepts both `class` and `className` — they are merged together, so either works:
159
+
160
+ ```tsx
161
+ <Button class="mt-4" size="large">Submit</Button>
162
+ ```
163
+
164
+ ### variantProps() for Headless Components
165
+ The lower-level `variantProps()` function separates variant logic from rendering, useful for headless components or custom wrappers:
166
+
167
+ ```tsx
168
+ import { variantProps, type VariantPropsOf } from "classname-variants/react";
169
+
170
+ const buttonProps = variantProps({
171
+ base: "rounded px-4 py-2",
172
+ variants: {
173
+ intent: {
174
+ primary: "bg-teal-500 text-white",
175
+ secondary: "bg-slate-200",
176
+ },
177
+ },
178
+ });
179
+
180
+ // Extract the variant prop types for the wrapper component
181
+ type ButtonProps = VariantPropsOf<typeof buttonProps>;
182
+
183
+ function Button(props: ButtonProps) {
184
+ const { className, ...rest } = buttonProps(props);
185
+ return <button className={className} {...rest} />;
186
+ }
187
+ ```
188
+
102
189
  ### Polymorphic Components with "as" Prop
103
190
  Change the underlying element/component while keeping the same styles:
104
191
 
@@ -113,8 +200,13 @@ const Button = styled("button", { variants: {...} });
113
200
 
114
201
  ### Tailwind CSS Integration
115
202
 
116
- #### Using tailwind-merge for Conflict Resolution
203
+ The library works with Tailwind CSS out of the box without any additional dependencies.
204
+
205
+ #### OPTIONAL: Using tailwind-merge for Class Conflict Resolution
206
+ **This is completely optional.** Only consider using tailwind-merge if you need to resolve conflicting Tailwind classes (e.g., when allowing className overrides). Most use cases don't need this - design your variants to avoid conflicts instead to reduce complexity and overhead.
207
+
117
208
  ```ts
209
+ // Only if you really need class merging:
118
210
  import { classNames } from "classname-variants";
119
211
  import { twMerge } from "tailwind-merge";
120
212
 
@@ -160,4 +252,4 @@ Add to VS Code settings.json:
160
252
 
161
253
  - Zero runtime dependencies
162
254
  - Development dependencies: React, Preact, TypeScript
163
- - Designed to work with any CSS framework (Tailwind, CSS modules, etc.)
255
+ - Designed to work with any CSS framework (Tailwind, CSS modules, etc.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "classname-variants",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "Variant API for plain class names",
5
5
  "author": "Felix Gnass <fgnass@gmail.com>",
6
6
  "license": "MIT",
@@ -37,8 +37,11 @@
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsc",
40
+ "lint": "biome check .",
41
+ "lint:fix": "biome check --write --unsafe .",
40
42
  "start": "npx vite",
41
- "prepublishOnly": "npm run build"
43
+ "test": "vitest",
44
+ "prepublishOnly": "npm run build && vitest --run"
42
45
  },
43
46
  "keywords": [
44
47
  "tailwind",
@@ -49,11 +52,14 @@
49
52
  "preact"
50
53
  ],
51
54
  "devDependencies": {
55
+ "@biomejs/biome": "^2.2.6",
52
56
  "@types/react": "^18.0.26",
53
57
  "@types/react-dom": "^18.0.9",
58
+ "happy-dom": "^20.0.5",
54
59
  "preact": "^10.20.2",
55
60
  "react": "^18.2.0",
56
61
  "react-dom": "^18.2.0",
57
- "typescript": "^4.9.4"
62
+ "typescript": "^4.9.4",
63
+ "vitest": "^3.2.4"
58
64
  }
59
65
  }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "happy-dom",
6
+ globals: true,
7
+ include: ["src/**/*.{test,spec}.{ts,tsx}"],
8
+ },
9
+ });
@@ -1,12 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm run build:*)",
5
- "Bash(npx tsc:*)",
6
- "Bash(rm:*)",
7
- "Bash(node:*)"
8
- ],
9
- "deny": [],
10
- "ask": []
11
- }
12
- }