@nationaldesignstudio/react 0.0.19 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nationaldesignstudio/react",
3
- "version": "0.0.19",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "*.css"
@@ -0,0 +1,233 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { page, userEvent } from "vitest/browser";
3
+ import { render } from "vitest-browser-react";
4
+ import { Accordion, AccordionItem } from "./accordion";
5
+
6
+ describe("Accordion", () => {
7
+ describe("Rendering", () => {
8
+ test("renders accordion with items", async () => {
9
+ render(
10
+ <Accordion>
11
+ <AccordionItem id="item-1" title="Question 1">
12
+ Answer 1
13
+ </AccordionItem>
14
+ <AccordionItem id="item-2" title="Question 2">
15
+ Answer 2
16
+ </AccordionItem>
17
+ </Accordion>,
18
+ );
19
+ await expect
20
+ .element(page.getByRole("button", { name: "Question 1" }))
21
+ .toBeInTheDocument();
22
+ await expect
23
+ .element(page.getByRole("button", { name: "Question 2" }))
24
+ .toBeInTheDocument();
25
+ });
26
+
27
+ test("renders with default expanded item", async () => {
28
+ render(
29
+ <Accordion defaultExpanded="item-1">
30
+ <AccordionItem id="item-1" title="Question 1">
31
+ Answer 1
32
+ </AccordionItem>
33
+ <AccordionItem id="item-2" title="Question 2">
34
+ Answer 2
35
+ </AccordionItem>
36
+ </Accordion>,
37
+ );
38
+ // The first item should be expanded (has data-open attribute)
39
+ await expect
40
+ .element(page.getByRole("button", { name: "Question 1" }))
41
+ .toHaveAttribute("data-open");
42
+ });
43
+
44
+ test("renders with multiple default expanded items", async () => {
45
+ render(
46
+ <Accordion allowMultiple defaultExpanded={["item-1", "item-2"]}>
47
+ <AccordionItem id="item-1" title="Question 1">
48
+ Answer 1
49
+ </AccordionItem>
50
+ <AccordionItem id="item-2" title="Question 2">
51
+ Answer 2
52
+ </AccordionItem>
53
+ </Accordion>,
54
+ );
55
+ await expect
56
+ .element(page.getByRole("button", { name: "Question 1" }))
57
+ .toHaveAttribute("data-open");
58
+ await expect
59
+ .element(page.getByRole("button", { name: "Question 2" }))
60
+ .toHaveAttribute("data-open");
61
+ });
62
+ });
63
+
64
+ describe("Keyboard Navigation", () => {
65
+ test("items are focusable via Tab", async () => {
66
+ render(
67
+ <Accordion>
68
+ <AccordionItem id="item-1" title="Question 1">
69
+ Answer 1
70
+ </AccordionItem>
71
+ <AccordionItem id="item-2" title="Question 2">
72
+ Answer 2
73
+ </AccordionItem>
74
+ </Accordion>,
75
+ );
76
+ await userEvent.keyboard("{Tab}");
77
+ await expect
78
+ .element(page.getByRole("button", { name: "Question 1" }))
79
+ .toHaveFocus();
80
+ });
81
+
82
+ test("Enter key expands/collapses item", async () => {
83
+ render(
84
+ <Accordion>
85
+ <AccordionItem id="item-1" title="Question 1">
86
+ Answer 1
87
+ </AccordionItem>
88
+ </Accordion>,
89
+ );
90
+ const trigger = page.getByRole("button", { name: "Question 1" });
91
+ trigger.element().focus();
92
+ await userEvent.keyboard("{Enter}");
93
+ await expect.element(trigger).toHaveAttribute("data-open");
94
+ });
95
+
96
+ test("Space key expands/collapses item", async () => {
97
+ render(
98
+ <Accordion>
99
+ <AccordionItem id="item-1" title="Question 1">
100
+ Answer 1
101
+ </AccordionItem>
102
+ </Accordion>,
103
+ );
104
+ const trigger = page.getByRole("button", { name: "Question 1" });
105
+ trigger.element().focus();
106
+ await userEvent.keyboard(" ");
107
+ await expect.element(trigger).toHaveAttribute("data-open");
108
+ });
109
+ });
110
+
111
+ describe("Click Interactions", () => {
112
+ test("clicking trigger expands item", async () => {
113
+ render(
114
+ <Accordion>
115
+ <AccordionItem id="item-1" title="Question 1">
116
+ Answer 1
117
+ </AccordionItem>
118
+ </Accordion>,
119
+ );
120
+ const trigger = page.getByRole("button", { name: "Question 1" });
121
+ await trigger.click();
122
+ await expect.element(trigger).toHaveAttribute("data-open");
123
+ });
124
+
125
+ test("clicking expanded trigger collapses item", async () => {
126
+ render(
127
+ <Accordion defaultExpanded="item-1">
128
+ <AccordionItem id="item-1" title="Question 1">
129
+ Answer 1
130
+ </AccordionItem>
131
+ </Accordion>,
132
+ );
133
+ const trigger = page.getByRole("button", { name: "Question 1" });
134
+ await expect.element(trigger).toHaveAttribute("data-open");
135
+ await trigger.click();
136
+ await expect.element(trigger).not.toHaveAttribute("data-open");
137
+ });
138
+
139
+ test("single mode collapses other items when opening new one", async () => {
140
+ render(
141
+ <Accordion defaultExpanded="item-1">
142
+ <AccordionItem id="item-1" title="Question 1">
143
+ Answer 1
144
+ </AccordionItem>
145
+ <AccordionItem id="item-2" title="Question 2">
146
+ Answer 2
147
+ </AccordionItem>
148
+ </Accordion>,
149
+ );
150
+ const trigger1 = page.getByRole("button", { name: "Question 1" });
151
+ const trigger2 = page.getByRole("button", { name: "Question 2" });
152
+
153
+ await expect.element(trigger1).toHaveAttribute("data-open");
154
+ await trigger2.click();
155
+ await expect.element(trigger2).toHaveAttribute("data-open");
156
+ await expect.element(trigger1).not.toHaveAttribute("data-open");
157
+ });
158
+
159
+ test("multiple mode allows multiple items open", async () => {
160
+ render(
161
+ <Accordion allowMultiple defaultExpanded="item-1">
162
+ <AccordionItem id="item-1" title="Question 1">
163
+ Answer 1
164
+ </AccordionItem>
165
+ <AccordionItem id="item-2" title="Question 2">
166
+ Answer 2
167
+ </AccordionItem>
168
+ </Accordion>,
169
+ );
170
+ const trigger1 = page.getByRole("button", { name: "Question 1" });
171
+ const trigger2 = page.getByRole("button", { name: "Question 2" });
172
+
173
+ await expect.element(trigger1).toHaveAttribute("data-open");
174
+ await trigger2.click();
175
+ await expect.element(trigger1).toHaveAttribute("data-open");
176
+ await expect.element(trigger2).toHaveAttribute("data-open");
177
+ });
178
+ });
179
+
180
+ describe("Variants", () => {
181
+ test("applies light variant by default", async () => {
182
+ render(
183
+ <Accordion>
184
+ <AccordionItem id="item-1" title="Question 1">
185
+ Answer 1
186
+ </AccordionItem>
187
+ </Accordion>,
188
+ );
189
+ const trigger = page.getByRole("button", { name: "Question 1" });
190
+ // Light variant has gray-800 text
191
+ await expect.element(trigger).toHaveClass(/text-gray-800/);
192
+ });
193
+
194
+ test("applies dark variant classes", async () => {
195
+ render(
196
+ <Accordion variant="dark">
197
+ <AccordionItem id="item-1" title="Question 1">
198
+ Answer 1
199
+ </AccordionItem>
200
+ </Accordion>,
201
+ );
202
+ const trigger = page.getByRole("button", { name: "Question 1" });
203
+ // Dark variant has gray-100 text
204
+ await expect.element(trigger).toHaveClass(/text-gray-100/);
205
+ });
206
+ });
207
+
208
+ describe("Content", () => {
209
+ test("displays title text in trigger", async () => {
210
+ render(
211
+ <Accordion>
212
+ <AccordionItem id="item-1" title="Custom Title">
213
+ Content here
214
+ </AccordionItem>
215
+ </Accordion>,
216
+ );
217
+ await expect.element(page.getByText("Custom Title")).toBeInTheDocument();
218
+ });
219
+
220
+ test("displays content when expanded", async () => {
221
+ render(
222
+ <Accordion defaultExpanded="item-1">
223
+ <AccordionItem id="item-1" title="Question 1">
224
+ Answer content here
225
+ </AccordionItem>
226
+ </Accordion>,
227
+ );
228
+ await expect
229
+ .element(page.getByText("Answer content here"))
230
+ .toBeInTheDocument();
231
+ });
232
+ });
233
+ });
@@ -18,7 +18,7 @@ const accordionVariants = tv({
18
18
  },
19
19
  },
20
20
  defaultVariants: {
21
- variant: "dark",
21
+ variant: "light",
22
22
  },
23
23
  });
24
24
 
@@ -31,7 +31,7 @@ const accordionItemVariants = tv({
31
31
  },
32
32
  },
33
33
  defaultVariants: {
34
- variant: "dark",
34
+ variant: "light",
35
35
  },
36
36
  });
37
37
 
@@ -48,7 +48,7 @@ const accordionTriggerVariants = tv({
48
48
  },
49
49
  },
50
50
  defaultVariants: {
51
- variant: "dark",
51
+ variant: "light",
52
52
  },
53
53
  });
54
54
 
@@ -62,7 +62,7 @@ const accordionPanelVariants = tv({
62
62
  },
63
63
  },
64
64
  defaultVariants: {
65
- variant: "dark",
65
+ variant: "light",
66
66
  },
67
67
  });
68
68
 
@@ -73,7 +73,7 @@ const accordionPanelVariants = tv({
73
73
  const AccordionContext = React.createContext<{
74
74
  variant: "dark" | "light";
75
75
  }>({
76
- variant: "dark",
76
+ variant: "light",
77
77
  });
78
78
 
79
79
  // =============================================================================
@@ -100,8 +100,8 @@ export interface AccordionProps
100
100
  * Built on Base UI's Accordion primitive.
101
101
  *
102
102
  * Variants:
103
- * - dark: Dark theme styling (default)
104
- * - light: Light theme styling
103
+ * - light: Light theme styling (default)
104
+ * - dark: Dark theme styling
105
105
  *
106
106
  * @example
107
107
  * ```tsx
@@ -134,7 +134,7 @@ const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
134
134
  }, [defaultExpanded]);
135
135
 
136
136
  return (
137
- <AccordionContext.Provider value={{ variant: variant ?? "dark" }}>
137
+ <AccordionContext.Provider value={{ variant }}>
138
138
  <BaseAccordion.Root
139
139
  ref={ref}
140
140
  className={accordionVariants({ variant, class: className })}
@@ -0,0 +1,213 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { page } from "vitest/browser";
3
+ import { render } from "vitest-browser-react";
4
+ import {
5
+ Background,
6
+ BackgroundGradient,
7
+ BackgroundImage,
8
+ BackgroundOverlay,
9
+ BackgroundVideo,
10
+ } from "./background";
11
+
12
+ describe("Background", () => {
13
+ describe("Background Container", () => {
14
+ test("renders as a div with absolute positioning", async () => {
15
+ render(<Background data-testid="bg-container">Content</Background>);
16
+ const container = page.getByTestId("bg-container");
17
+ await expect.element(container).toBeInTheDocument();
18
+ await expect.element(container).toHaveClass(/absolute/);
19
+ await expect.element(container).toHaveClass(/inset-0/);
20
+ });
21
+
22
+ test("has aria-hidden attribute", async () => {
23
+ render(<Background data-testid="bg-container" />);
24
+ await expect
25
+ .element(page.getByTestId("bg-container"))
26
+ .toHaveAttribute("aria-hidden", "true");
27
+ });
28
+
29
+ test("accepts custom className", async () => {
30
+ render(<Background data-testid="bg" className="custom-class" />);
31
+ await expect.element(page.getByTestId("bg")).toHaveClass(/custom-class/);
32
+ });
33
+ });
34
+
35
+ describe("BackgroundImage", () => {
36
+ test("renders an img element with src", async () => {
37
+ render(<BackgroundImage src="/test-image.jpg" data-testid="bg-img" />);
38
+ const img = page.getByTestId("bg-img");
39
+ await expect.element(img).toBeInTheDocument();
40
+ await expect.element(img).toHaveAttribute("src", "/test-image.jpg");
41
+ });
42
+
43
+ test("has object-cover class for proper fitting", async () => {
44
+ render(<BackgroundImage src="/test.jpg" data-testid="bg-img" />);
45
+ await expect
46
+ .element(page.getByTestId("bg-img"))
47
+ .toHaveClass(/object-cover/);
48
+ });
49
+
50
+ test("sets default alt to empty string for decorative images", async () => {
51
+ render(<BackgroundImage src="/test.jpg" data-testid="bg-img" />);
52
+ await expect
53
+ .element(page.getByTestId("bg-img"))
54
+ .toHaveAttribute("alt", "");
55
+ });
56
+
57
+ test("accepts custom alt text", async () => {
58
+ render(
59
+ <BackgroundImage
60
+ src="/test.jpg"
61
+ alt="Custom alt text"
62
+ data-testid="bg-img"
63
+ />,
64
+ );
65
+ await expect
66
+ .element(page.getByTestId("bg-img"))
67
+ .toHaveAttribute("alt", "Custom alt text");
68
+ });
69
+
70
+ test("applies custom position via style", async () => {
71
+ render(
72
+ <BackgroundImage
73
+ src="/test.jpg"
74
+ position="top left"
75
+ data-testid="bg-img"
76
+ />,
77
+ );
78
+ const img = page.getByTestId("bg-img");
79
+ // Check the style attribute contains the position
80
+ await expect.element(img).toHaveStyle({ objectPosition: "top left" });
81
+ });
82
+
83
+ test("supports render prop for custom element", async () => {
84
+ render(
85
+ <BackgroundImage
86
+ src="/test.jpg"
87
+ // biome-ignore lint/a11y/useAltText: Test case for custom element
88
+ render={<img className="custom-img-class" />}
89
+ data-testid="bg-img"
90
+ />,
91
+ );
92
+ await expect
93
+ .element(page.getByTestId("bg-img"))
94
+ .toHaveClass(/custom-img-class/);
95
+ });
96
+ });
97
+
98
+ describe("BackgroundVideo", () => {
99
+ test("renders a video element", async () => {
100
+ render(<BackgroundVideo src="/test-video.mp4" data-testid="bg-video" />);
101
+ const video = page.getByTestId("bg-video");
102
+ await expect.element(video).toBeInTheDocument();
103
+ });
104
+
105
+ test("has autoplay, loop, and muted by default", async () => {
106
+ render(<BackgroundVideo src="/test.mp4" data-testid="bg-video" />);
107
+ const video = page.getByTestId("bg-video");
108
+ await expect.element(video).toHaveAttribute("autoplay");
109
+ await expect.element(video).toHaveAttribute("loop");
110
+ await expect.element(video).toHaveAttribute("muted");
111
+ });
112
+
113
+ test("has object-cover class", async () => {
114
+ render(<BackgroundVideo src="/test.mp4" data-testid="bg-video" />);
115
+ await expect
116
+ .element(page.getByTestId("bg-video"))
117
+ .toHaveClass(/object-cover/);
118
+ });
119
+
120
+ test("accepts poster prop", async () => {
121
+ render(
122
+ <BackgroundVideo
123
+ src="/test.mp4"
124
+ poster="/poster.jpg"
125
+ data-testid="bg-video"
126
+ />,
127
+ );
128
+ await expect
129
+ .element(page.getByTestId("bg-video"))
130
+ .toHaveAttribute("poster", "/poster.jpg");
131
+ });
132
+ });
133
+
134
+ describe("BackgroundOverlay", () => {
135
+ test("renders with default opacity", async () => {
136
+ render(<BackgroundOverlay data-testid="bg-overlay" />);
137
+ const overlay = page.getByTestId("bg-overlay");
138
+ await expect.element(overlay).toBeInTheDocument();
139
+ await expect.element(overlay).toHaveStyle({ opacity: "0.4" });
140
+ });
141
+
142
+ test("accepts custom opacity", async () => {
143
+ render(<BackgroundOverlay opacity={0.7} data-testid="bg-overlay" />);
144
+ await expect
145
+ .element(page.getByTestId("bg-overlay"))
146
+ .toHaveStyle({ opacity: "0.7" });
147
+ });
148
+
149
+ test("has aria-hidden attribute", async () => {
150
+ render(<BackgroundOverlay data-testid="bg-overlay" />);
151
+ await expect
152
+ .element(page.getByTestId("bg-overlay"))
153
+ .toHaveAttribute("aria-hidden", "true");
154
+ });
155
+ });
156
+
157
+ describe("BackgroundGradient", () => {
158
+ test("renders with default gradient direction", async () => {
159
+ render(<BackgroundGradient data-testid="bg-gradient" />);
160
+ const gradient = page.getByTestId("bg-gradient");
161
+ await expect.element(gradient).toBeInTheDocument();
162
+ });
163
+
164
+ test("has aria-hidden attribute", async () => {
165
+ render(<BackgroundGradient data-testid="bg-gradient" />);
166
+ await expect
167
+ .element(page.getByTestId("bg-gradient"))
168
+ .toHaveAttribute("aria-hidden", "true");
169
+ });
170
+
171
+ test("accepts custom gradient via gradient prop", async () => {
172
+ render(
173
+ <BackgroundGradient
174
+ gradient="linear-gradient(45deg, red, blue)"
175
+ data-testid="bg-gradient"
176
+ />,
177
+ );
178
+ await expect
179
+ .element(page.getByTestId("bg-gradient"))
180
+ .toHaveStyle({ backgroundImage: "linear-gradient(45deg, red, blue)" });
181
+ });
182
+ });
183
+
184
+ describe("Compound Component", () => {
185
+ test("Background.Image is accessible via dot notation", async () => {
186
+ render(
187
+ <Background data-testid="bg-container">
188
+ <Background.Image src="/test.jpg" data-testid="bg-img" />
189
+ </Background>,
190
+ );
191
+ await expect
192
+ .element(page.getByTestId("bg-container"))
193
+ .toBeInTheDocument();
194
+ await expect.element(page.getByTestId("bg-img")).toBeInTheDocument();
195
+ });
196
+
197
+ test("can compose multiple background layers", async () => {
198
+ render(
199
+ <Background data-testid="bg-container">
200
+ <Background.Image src="/test.jpg" data-testid="bg-img" />
201
+ <Background.Overlay data-testid="bg-overlay" />
202
+ <Background.Gradient data-testid="bg-gradient" />
203
+ </Background>,
204
+ );
205
+ await expect
206
+ .element(page.getByTestId("bg-container"))
207
+ .toBeInTheDocument();
208
+ await expect.element(page.getByTestId("bg-img")).toBeInTheDocument();
209
+ await expect.element(page.getByTestId("bg-overlay")).toBeInTheDocument();
210
+ await expect.element(page.getByTestId("bg-gradient")).toBeInTheDocument();
211
+ });
212
+ });
213
+ });
@@ -1,3 +1,6 @@
1
+ "use client";
2
+
3
+ import { useRender } from "@base-ui-components/react/use-render";
1
4
  import * as React from "react";
2
5
  import { tv } from "tailwind-variants";
3
6
 
@@ -56,28 +59,59 @@ export interface BackgroundImageProps
56
59
  * Object position (default: "center")
57
60
  */
58
61
  position?: string;
62
+ /**
63
+ * Custom render prop for element composition.
64
+ * Accepts a React element or render function.
65
+ * @example
66
+ * ```tsx
67
+ * // Element pattern
68
+ * <BackgroundImage render={<img className="custom" />} src="/bg.jpg" />
69
+ *
70
+ * // Callback pattern
71
+ * <BackgroundImage render={(props) => <img {...props} />} src="/bg.jpg" />
72
+ * ```
73
+ */
74
+ render?:
75
+ | React.ReactElement
76
+ | ((
77
+ props: React.ImgHTMLAttributes<HTMLImageElement>,
78
+ ) => React.ReactElement);
59
79
  }
60
80
 
61
81
  /**
62
82
  * Background image layer using an actual img element with object-cover.
63
83
  * Supports native lazy loading, srcset, and better accessibility.
84
+ * Supports render prop for element composition.
64
85
  */
65
86
  const BackgroundImage = React.forwardRef<
66
87
  HTMLImageElement,
67
88
  BackgroundImageProps
68
- >(({ className, src, position = "center", alt = "", style, ...props }, ref) => (
69
- <img
70
- ref={ref}
71
- src={src}
72
- alt={alt}
73
- className={backgroundImageVariants({ class: className })}
74
- style={{
75
- objectPosition: position,
76
- ...style,
77
- }}
78
- {...props}
79
- />
80
- ));
89
+ >(
90
+ (
91
+ { className, src, position = "center", alt = "", style, render, ...props },
92
+ ref,
93
+ ) => {
94
+ const mergedProps = {
95
+ src,
96
+ alt,
97
+ className: backgroundImageVariants({ class: className }),
98
+ style: {
99
+ objectPosition: position,
100
+ ...style,
101
+ },
102
+ ...props,
103
+ };
104
+
105
+ const element = useRender({
106
+ // biome-ignore lint/a11y/useAltText: alt is provided via mergedProps
107
+ render: render ?? <img />,
108
+ ref,
109
+ props: mergedProps,
110
+ });
111
+
112
+ return element;
113
+ },
114
+ );
81
115
  BackgroundImage.displayName = "Background.Image";
82
116
 
83
117
  // =============================================================================
@@ -102,10 +136,23 @@ export interface BackgroundVideoProps
102
136
  * Poster image URL shown before video loads
103
137
  */
104
138
  poster?: string;
139
+ /**
140
+ * Custom render prop for element composition.
141
+ * @example
142
+ * ```tsx
143
+ * <BackgroundVideo render={<video className="custom" />} src="/bg.mp4" />
144
+ * ```
145
+ */
146
+ render?:
147
+ | React.ReactElement
148
+ | ((
149
+ props: React.VideoHTMLAttributes<HTMLVideoElement>,
150
+ ) => React.ReactElement);
105
151
  }
106
152
 
107
153
  /**
108
154
  * Background video layer using HTML5 video element.
155
+ * Supports render prop for element composition.
109
156
  */
110
157
  const BackgroundVideo = React.forwardRef<
111
158
  HTMLVideoElement,
@@ -121,23 +168,34 @@ const BackgroundVideo = React.forwardRef<
121
168
  loop = true,
122
169
  muted = true,
123
170
  playsInline = true,
171
+ render,
124
172
  ...props
125
173
  },
126
174
  ref,
127
- ) => (
128
- <video
129
- ref={ref}
130
- autoPlay={autoPlay}
131
- loop={loop}
132
- muted={muted}
133
- playsInline={playsInline}
134
- poster={poster}
135
- className={backgroundVideoVariants({ class: className })}
136
- {...props}
137
- >
138
- <source src={src} type={type} />
139
- </video>
140
- ),
175
+ ) => {
176
+ const mergedProps = {
177
+ autoPlay,
178
+ loop,
179
+ muted,
180
+ playsInline,
181
+ poster,
182
+ className: backgroundVideoVariants({ class: className }),
183
+ ...props,
184
+ };
185
+
186
+ const element = useRender({
187
+ render: render ?? (
188
+ // biome-ignore lint/a11y/useMediaCaption: Background videos are decorative and shouldn't have captions
189
+ <video>
190
+ <source src={src} type={type} />
191
+ </video>
192
+ ),
193
+ ref,
194
+ props: mergedProps,
195
+ });
196
+
197
+ return element;
198
+ },
141
199
  );
142
200
  BackgroundVideo.displayName = "Background.Video";
143
201
 
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  Button as BaseButton,
3
5
  type ButtonProps as BaseButtonProps,
@@ -159,6 +161,11 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
159
161
  const themeStyles = buttonThemeToStyleVars(theme);
160
162
  const combinedStyles = hasTheme ? { ...themeStyles, ...style } : style;
161
163
 
164
+ // Resolve actual values for data attributes
165
+ const resolvedVariant = effectiveVariant ?? "solid";
166
+ const resolvedColorScheme = colorScheme ?? "dark";
167
+ const resolvedSize = size ?? "default";
168
+
162
169
  return (
163
170
  <BaseButton
164
171
  className={buttonVariants({
@@ -171,6 +178,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
171
178
  render={render}
172
179
  nativeButton={isNativeButton}
173
180
  style={combinedStyles}
181
+ data-variant={resolvedVariant}
182
+ data-color-scheme={resolvedColorScheme}
183
+ data-size={resolvedSize}
174
184
  {...props}
175
185
  />
176
186
  );