├── .github
└── FUNDING.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmignore
├── LICENSE
├── README.md
├── biome.json
├── demo
├── .eslintrc.json
├── .gitignore
├── components
│ ├── accordion.tsx
│ ├── alert-dialog.tsx
│ ├── aspect-ratio.tsx
│ ├── avatar.tsx
│ ├── checkbox.tsx
│ ├── collapsible.tsx
│ ├── context-menu.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── hover-card.tsx
│ ├── menubar.tsx
│ ├── navigation-menu.tsx
│ ├── popover.tsx
│ ├── progress.tsx
│ ├── radio-group.tsx
│ ├── select.tsx
│ ├── shared
│ │ ├── button.tsx
│ │ ├── command-menu.tsx
│ │ ├── demo-card.tsx
│ │ └── theme-switcher.tsx
│ ├── slider.tsx
│ ├── switch.tsx
│ ├── tabs.tsx
│ ├── toast.tsx
│ ├── toggle-group.tsx
│ ├── toggle.tsx
│ ├── toolbar.tsx
│ └── tooltip.tsx
├── css
│ └── tailwind.css
├── hooks
│ ├── use-dark-mode.ts
│ └── use-media-query.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ └── index.tsx
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.js
├── public
│ ├── favicon.ico
│ └── static
│ │ ├── og.png
│ │ ├── og.webp
│ │ └── theme.js
├── tailwind.config.js
├── tsconfig.json
├── utils
│ ├── math.ts
│ └── random.ts
└── vercel.json
├── package.json
├── pnpm-lock.yaml
├── src
├── __snapshots__
│ └── index.test.ts.snap
├── index.test.ts
└── index.ts
└── tsconfig.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: ecklf
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm test
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | demo
3 | types
4 | .github
5 | .vscode
6 | .gitignore
7 | tsconfig.json
8 | .scratch
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 ecklf
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Utilities and variants for styling Radix state
12 |
13 |
14 |
21 |
22 | ## What is this?
23 |
24 | The main purpose of this library is adding classnames for accessing Radix data attributes, which gains you the benefit of auto-completion compared to using `data-*` variants.
25 |
26 | **TL;DR** It's [@headlessui-tailwindcss](https://github.com/tailwindlabs/headlessui/tree/main/packages/@headlessui-tailwindcss) for Radix.
27 |
28 | ## Installation
29 |
30 | ```sh
31 | # For v3 compatibility
32 | pnpm add tailwindcss-radix@3
33 | # For v4 compatibility
34 | pnpm add tailwindcss-radix
35 | ```
36 |
37 | ## Demo
38 |
39 | Click on the banner to check out the demo components. You can find the code inside the [demo](https://github.com/ecklf/tailwindcss-radix/tree/main/demo) folder.
40 |
41 | ## Usage
42 |
43 | ### With [@plugin directive](https://tailwindcss.com/docs/functions-and-directives#plugin-directive) (recommended)
44 |
45 | **Default prefix**
46 | ```css
47 | /* Generates `radix-[state/side/orientation]-*` utilities for `data-[state/side/orientation]="*"` */
48 | @plugin "tailwindcss-radix";
49 | ```
50 |
51 | **Custom prefix**
52 | ```css
53 | /* Generates `rdx-[state/side/orientation]-*` utilities for `data-[state/side/orientation]="*"` */
54 | @plugin "tailwindcss-radix" {
55 | variantPrefix: rdx;
56 | }
57 | ```
58 |
59 | ### With [@config directive](https://tailwindcss.com/docs/functions-and-directives#config-directive)
60 |
61 | **Default prefix**
62 | ```js
63 | module.exports = {
64 | // --snip --
65 | plugins: [
66 | // Generates `radix-[state/side/orientation]-*` utilities for `data-[state/side/orientation]="*"`
67 | require("tailwindcss-radix")(),
68 | ],
69 | };
70 | ```
71 |
72 | **Custom prefix**
73 | ```js
74 | module.exports = {
75 | // --snip --
76 | plugins: [
77 | // Generates `rdx-[state/side/orientation]-*` utilities for `data-[state/side/orientation]="*"`
78 | require("tailwindcss-radix")({
79 | variantPrefix: "rdx",
80 | }),
81 | ],
82 | };
83 | ```
84 |
85 | **Load configuration**
86 | ```css
87 | @config "../../tailwind.config.js";
88 | ```
89 |
90 | ### Styling state
91 |
92 | #### Basic usage
93 |
94 | This plugin works with CSS attribute selectors. Use the variants based on the `data-*` attribute added by Radix.
95 |
96 | ```tsx
97 | import React from "react";
98 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
99 |
100 | const App = () => {
101 | return (
102 |
103 |
104 | Trigger
105 |
106 |
107 | Item
108 |
109 |
110 | );
111 | };
112 |
113 | export default App;
114 | ```
115 |
116 | #### Accessing parent state
117 |
118 | When you need to style an element based on the state of a parent element, mark the parent with the `group` class and style the target with `group-radix-*` modifiers.
119 |
120 | Example usage of a conditional transform for a Radix `Accordion`:
121 |
122 | ```tsx
123 | import React from "react";
124 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
125 | import { ChevronDownIcon } from "@radix-ui/react-icons";
126 |
127 | const Accordion = () => {
128 | return (
129 |
130 |
131 |
132 |
133 |
134 | Item 1
135 |
136 |
137 |
138 |
139 | Content 1
140 |
141 |
142 |
143 |
144 |
145 | Item 2
146 |
147 |
148 |
149 |
150 | Content 2
151 |
152 |
153 | );
154 | };
155 |
156 | export default App;
157 | ```
158 |
159 | #### Accessing sibling state
160 |
161 | When you need to style an element based on the state of a sibling element, mark the sibling with the `peer` class and style the target with `peer-radix-*` modifiers.
162 |
163 | Example usage of a conditional icon color for a sibling of a Radix `Checkbox`:
164 |
165 | ```tsx
166 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
167 | import { CheckIcon, TargetIcon } from "@radix-ui/react-icons";
168 | import React from "react";
169 |
170 | interface Props {}
171 |
172 | const App = (props: Props) => {
173 | return (
174 | <>
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | >
183 | );
184 | };
185 |
186 | export default App;
187 | ```
188 |
189 | #### Disabled state
190 |
191 | Use the generated `disabled` variant.
192 |
193 | ```tsx
194 | import React from "react";
195 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
196 |
197 | const ContextMenu = () => {
198 | return (
199 | // --snip--
200 |
204 | Item
205 |
206 | // --snip--
207 | );
208 | };
209 | ```
210 |
211 | ### CSS Variable Utilities
212 |
213 | #### Origin position
214 |
215 | ```css
216 | .origin-radix-context-menu {
217 | transform-origin: var(--radix-context-menu-content-transform-origin);
218 | }
219 | .origin-radix-dropdown-menu {
220 | transform-origin: var(--radix-dropdown-menu-content-transform-origin);
221 | }
222 | .origin-radix-hover-card {
223 | transform-origin: var(--radix-hover-card-content-transform-origin);
224 | }
225 | .origin-radix-menubar {
226 | transform-origin: var(--radix-menubar-content-transform-origin);
227 | }
228 | .origin-radix-popover {
229 | transform-origin: var(--radix-popover-content-transform-origin);
230 | }
231 | .origin-radix-select {
232 | transform-origin: var(--radix-select-content-transform-origin);
233 | }
234 | .origin-radix-tooltip {
235 | transform-origin: var(--radix-tooltip-content-transform-origin);
236 | }
237 | ```
238 |
239 | #### Content / Viewport Width / Height
240 |
241 | ```css
242 | .w-radix-accordion-content {
243 | width: var(--radix-accordion-content-width);
244 | }
245 | .h-radix-accordion-content {
246 | height: var(--radix-accordion-content-height);
247 | }
248 | .w-radix-collapsible-content {
249 | width: var(--radix-collapsible-content-width);
250 | }
251 | .h-radix-collapsible-content {
252 | height: var(--radix-collapsible-content-height);
253 | }
254 | .w-radix-navigation-menu-viewport {
255 | width: var(--radix-navigation-menu-viewport-width);
256 | }
257 | .h-radix-navigation-menu-viewport {
258 | height: var(--radix-navigation-menu-viewport-height);
259 | }
260 | ```
261 |
262 | #### Content Available Width / Height
263 |
264 | ```css
265 | .w-radix-context-menu-content-available {
266 | width: var(--radix-context-menu-content-available-width);
267 | }
268 | .max-w-radix-context-menu-content-available {
269 | max-width: var(--radix-context-menu-content-available-width);
270 | }
271 | .h-radix-context-menu-content-available {
272 | height: var(--radix-context-menu-content-available-height);
273 | }
274 | .max-h-radix-context-menu-content-available {
275 | max-height: var(--radix-context-menu-content-available-height);
276 | }
277 | .w-radix-dropdown-menu-content-available {
278 | width: var(--radix-dropdown-menu-content-available-width);
279 | }
280 | .max-w-radix-dropdown-menu-content-available {
281 | max-width: var(--radix-dropdown-menu-content-available-width);
282 | }
283 | .h-radix-dropdown-menu-content-available {
284 | height: var(--radix-dropdown-menu-content-available-height);
285 | }
286 | .max-h-radix-dropdown-menu-content-available {
287 | max-height: var(--radix-dropdown-menu-content-available-height);
288 | }
289 | .w-radix-hover-card-content-available {
290 | width: var(--radix-hover-card-content-available-width);
291 | }
292 | .max-w-radix-hover-card-content-available {
293 | max-width: var(--radix-hover-card-content-available-width);
294 | }
295 | .h-radix-hover-card-content-available {
296 | height: var(--radix-hover-card-content-available-height);
297 | }
298 | .max-h-radix-hover-card-content-available {
299 | max-height: var(--radix-hover-card-content-available-height);
300 | }
301 | .w-radix-menubar-content-available {
302 | width: var(--radix-menubar-content-available-width);
303 | }
304 | .max-w-radix-menubar-content-available {
305 | max-width: var(--radix-menubar-content-available-width);
306 | }
307 | .h-radix-menubar-content-available {
308 | height: var(--radix-menubar-content-available-height);
309 | }
310 | .max-h-radix-menubar-content-available {
311 | max-height: var(--radix-menubar-content-available-height);
312 | }
313 | .w-radix-popover-content-available {
314 | width: var(--radix-popover-content-available-width);
315 | }
316 | .max-w-radix-popover-content-available {
317 | max-width: var(--radix-popover-content-available-width);
318 | }
319 | .h-radix-popover-content-available {
320 | height: var(--radix-popover-content-available-height);
321 | }
322 | .max-h-radix-popover-content-available {
323 | max-height: var(--radix-popover-content-available-height);
324 | }
325 | .w-radix-select-content-available {
326 | width: var(--radix-select-content-available-width);
327 | }
328 | .max-w-radix-select-content-available {
329 | max-width: var(--radix-select-content-available-width);
330 | }
331 | .h-radix-select-content-available {
332 | height: var(--radix-select-content-available-height);
333 | }
334 | .max-h-radix-select-content-available {
335 | max-height: var(--radix-select-content-available-height);
336 | }
337 | .w-radix-tooltip-content-available {
338 | width: var(--radix-tooltip-content-available-width);
339 | }
340 | .max-w-radix-tooltip-content-available {
341 | max-width: var(--radix-tooltip-content-available-width);
342 | }
343 | .h-radix-tooltip-content-available {
344 | height: var(--radix-tooltip-content-available-height);
345 | }
346 | .max-h-radix-tooltip-content-available {
347 | max-height: var(--radix-tooltip-content-available-height);
348 | }
349 | ```
350 |
351 | #### Trigger Available Width / Height
352 |
353 | ```css
354 | .w-radix-context-menu-trigger {
355 | width: var(--radix-context-menu-trigger-width);
356 | }
357 | .h-radix-context-menu-trigger {
358 | height: var(--radix-context-menu-trigger-height);
359 | }
360 | .w-radix-dropdown-menu-trigger {
361 | width: var(--radix-dropdown-menu-trigger-width);
362 | }
363 | .h-radix-dropdown-menu-trigger {
364 | height: var(--radix-dropdown-menu-trigger-height);
365 | }
366 | .w-radix-hover-card-trigger {
367 | width: var(--radix-hover-card-trigger-width);
368 | }
369 | .h-radix-hover-card-trigger {
370 | height: var(--radix-hover-card-trigger-height);
371 | }
372 | .w-radix-menubar-trigger {
373 | width: var(--radix-menubar-trigger-width);
374 | }
375 | .h-radix-menubar-trigger {
376 | height: var(--radix-menubar-trigger-height);
377 | }
378 | .w-radix-popover-trigger {
379 | width: var(--radix-popover-trigger-width);
380 | }
381 | .h-radix-popover-trigger {
382 | height: var(--radix-popover-trigger-height);
383 | }
384 | .w-radix-select-trigger {
385 | width: var(--radix-select-trigger-width);
386 | }
387 | .h-radix-select-trigger {
388 | height: var(--radix-select-trigger-height);
389 | }
390 | .w-radix-tooltip-trigger {
391 | width: var(--radix-tooltip-trigger-width);
392 | }
393 | .h-radix-tooltip-trigger {
394 | height: var(--radix-tooltip-trigger-height);
395 | }
396 | ```
397 |
398 | #### Toast swiping
399 |
400 | ```css
401 | .translate-x-radix-toast-swipe-end-x {
402 | transform: translateX(var(--radix-toast-swipe-end-x));
403 | }
404 | .translate-y-radix-toast-swipe-end-y {
405 | transform: translateY(var(--radix-toast-swipe-end-y));
406 | }
407 | .translate-x-radix-toast-swipe-move-x {
408 | transform: translateX(var(--radix-toast-swipe-move-x));
409 | }
410 | .translate-y-radix-toast-swipe-move-y {
411 | transform: translateY(var(--radix-toast-swipe-move-y));
412 | }
413 | ```
414 |
415 | ## Migrate from v1
416 |
417 | To prevent a possible future name clashing the `skipAttributeNames` option has been removed. In case you used this option, please update the class names accordingly.
418 |
419 | ## Migrate from v2
420 |
421 | In case you use `content-available` utilities:
422 |
423 | - Add `-content-available` to the width-based classnames
424 | - Remove `width` and `height` from `content-available-[width|height]`
425 |
426 | View diff
427 |
428 |
429 | ```diff
430 | -w-radix-context-menu
431 | +w-radix-context-menu-content-available
432 |
433 | -h-radix-context-menu-content-available-height
434 | +h-radix-context-menu-content-available
435 |
436 | -max-w-radix-context-menu-content-available-width
437 | +max-w-radix-context-menu-content-available
438 |
439 | -max-h-radix-context-menu-content-available-height
440 | +max-h-radix-context-menu-content-available
441 |
442 |
443 | -w-radix-dropdown-menu
444 | +w-radix-dropdown-menu-content-available
445 |
446 | -h-radix-dropdown-menu-content-available-height
447 | +h-radix-dropdown-menu-content-available
448 |
449 | -max-w-radix-dropdown-menu-content-available-width
450 | +max-w-radix-dropdown-menu-content-available
451 |
452 | -max-h-radix-dropdown-menu-content-available-height
453 | +max-h-radix-dropdown-menu-content-available
454 |
455 |
456 | -w-radix-hover-card
457 | +w-radix-hover-card-content-available
458 |
459 | -h-radix-hover-card-content-available-height
460 | +h-radix-hover-card-content-available
461 |
462 | -max-w-radix-hover-card-content-available-width
463 | +max-w-radix-hover-card-content-available
464 |
465 | -max-h-radix-hover-card-content-available-height
466 | +max-h-radix-hover-card-content-available
467 |
468 |
469 | -w-radix-menubar
470 | +w-radix-menubar-content-available
471 |
472 | -h-radix-menubar-content-available-height
473 | +h-radix-menubar-content-available
474 |
475 | -max-w-radix-menubar-content-available-width
476 | +max-w-radix-menubar-content-available
477 |
478 | -max-h-radix-menubar-content-available-height
479 | +max-h-radix-menubar-content-available
480 |
481 |
482 | -w-radix-popover
483 | +w-radix-popover-content-available
484 |
485 | -h-radix-popover-content-available-height
486 | +h-radix-popover-content-available
487 |
488 | -max-w-radix-popover-content-available-width
489 | +max-w-radix-popover-content-available
490 |
491 | -max-h-radix-popover-content-available-height
492 | +max-h-radix-popover-content-available
493 |
494 |
495 | -w-radix-select
496 | +w-radix-select-content-available
497 |
498 | -h-radix-select
499 | +h-radix-select-content-available
500 |
501 | -max-w-radix-select-content-available-width
502 | +max-w-radix-select-content-available
503 |
504 | -max-h-radix-select-content-available-height
505 | +max-h-radix-select-content-available
506 |
507 |
508 | -w-radix-tooltip
509 | +w-radix-tooltip-content-available
510 |
511 | -h-radix-tooltip
512 | +h-radix-tooltip-content-available
513 |
514 | -max-w-radix-tooltip-content-available-width
515 | +max-w-radix-tooltip-content-available
516 |
517 | -max-h-radix-tooltip-content-available-height
518 | +max-h-radix-tooltip-content-available
519 | ```
520 |
521 |
522 |
523 |
524 | ## Migrate from v3
525 |
526 | - Support for disabled `variantPrefix` has been removed. Please use a prefix instead.
527 |
528 | ## License
529 |
530 | [MIT](LICENSE)
531 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
3 | "formatter": {
4 | "enabled": true,
5 | "formatWithErrors": false,
6 | "indentStyle": "space",
7 | "indentWidth": 2,
8 | "lineEnding": "lf",
9 | "lineWidth": 80,
10 | "attributePosition": "auto",
11 | "ignore": ["dist/", "coverage/", "**/.next/**", "**/*.yaml"]
12 | },
13 | "organizeImports": {
14 | "enabled": true
15 | },
16 | "linter": {
17 | "enabled": true,
18 | "rules": {
19 | "recommended": true,
20 | "complexity": {
21 | "noForEach": "off"
22 | }
23 | },
24 | "ignore": ["dist/", "coverage/", "**/.next/**", "**/*.yaml"]
25 | },
26 | "javascript": {
27 | "formatter": {
28 | "jsxQuoteStyle": "double",
29 | "quoteProperties": "asNeeded",
30 | "trailingCommas": "es5",
31 | "semicolons": "always",
32 | "arrowParentheses": "always",
33 | "bracketSpacing": true,
34 | "bracketSameLine": false,
35 | "quoteStyle": "double",
36 | "attributePosition": "auto"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/demo/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/demo/components/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
2 | import { ChevronDownIcon } from "@radix-ui/react-icons";
3 | import { clsx } from "clsx";
4 | import React from "react";
5 |
6 | interface AccordionItem {
7 | header: string;
8 | content: string;
9 | }
10 |
11 | const items: AccordionItem[] = [
12 | {
13 | header: "What is Radix UI?",
14 | content:
15 | "Radix Primitives is a low-level UI component library with a focus on accessibility, customization and developer experience. You can use these components either as the base layer of your design system, or adopt them incrementally.",
16 | },
17 | {
18 | header: "Why are goats so popular at Vercel",
19 | content:
20 | "Goats are popular at Vercel for a few reasons. First, they provide a healthier and more sustainable solution for grass cutting and vegetation control. Additionally, goats are able to traverse very steep terrain. This makes them perfect for providing maintenance in areas that are difficult to access. Finally, their presence is said to provide a calming atmosphere, which is especially beneficial in stressful engineering environments.",
21 | },
22 | {
23 | header: "Is it accessible?",
24 | content: "Yes!",
25 | },
26 | ];
27 |
28 | const Accordion = () => {
29 | return (
30 |
35 | {items.map(({ header, content }, i) => (
36 |
41 |
42 |
49 |
50 | {header}
51 |
52 |
58 |
59 |
60 |
66 |
72 | {content}
73 |
74 |
75 |
76 | ))}
77 |
78 | );
79 | };
80 |
81 | export { Accordion };
82 |
--------------------------------------------------------------------------------
/demo/components/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | import { Transition } from "@headlessui/react";
2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
3 | import { clsx } from "clsx";
4 | import React, { Fragment, useState } from "react";
5 | import Button from "./shared/button";
6 |
7 | const AlertDialog = () => {
8 | const [isOpen, setIsOpen] = useState(false);
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
26 |
30 |
31 |
40 |
50 |
51 | Are you absolutely sure?
52 |
53 |
54 | This action cannot be undone. This will permanently delete your
55 | account and remove your data from our servers.
56 |
57 |
58 |
66 | Cancel
67 |
68 |
76 | Confirm
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export { AlertDialog };
88 |
--------------------------------------------------------------------------------
/demo/components/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
2 | import { clsx } from "clsx";
3 | import React, { Fragment } from "react";
4 |
5 | const AspectRatio = () => {
6 | return (
7 |
8 |
12 |
13 |
14 | Vancouver
15 |
16 |
17 |
23 | {/* eslint-disable-next-line @next/next/no-img-element */}
24 |

29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export { AspectRatio };
36 |
--------------------------------------------------------------------------------
/demo/components/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
2 | import { clsx } from "clsx";
3 | import React, { Fragment } from "react";
4 | import { getRandomInitials } from "../utils/random";
5 |
6 | enum Variant {
7 | Circle = 0,
8 | Rounded = 1,
9 | }
10 |
11 | type AvatarProps = {
12 | variant: Variant;
13 | renderInvalidUrls?: boolean;
14 | isOnline?: boolean;
15 | };
16 |
17 | const users = [
18 | "https://images.unsplash.com/photo-1573607217032-18299406d100?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=830&q=80",
19 | "https://images.unsplash.com/photo-1594089426440-ab4513b4d0d0?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
20 | "https://images.unsplash.com/photo-1549237511-6b64e006ce65?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
21 | "https://images.unsplash.com/photo-1546456073-ea246a7bd25f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
22 | ];
23 |
24 | const Avatar = ({
25 | variant,
26 | isOnline,
27 | renderInvalidUrls = false,
28 | }: AvatarProps) => {
29 | const urls = renderInvalidUrls
30 | ? Array.from({ length: users.length }, () => "")
31 | : users;
32 |
33 | return (
34 |
35 | {urls.map((src) => (
36 |
40 |
51 | {isOnline && (
52 |
61 |
62 |
63 | )}
64 |
74 |
75 | {getRandomInitials()}
76 |
77 |
78 |
79 | ))}
80 |
81 | );
82 | };
83 |
84 | Avatar.variant = Variant;
85 | export { Avatar };
86 |
--------------------------------------------------------------------------------
/demo/components/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
2 | import { CheckIcon } from "@radix-ui/react-icons";
3 | import * as LabelPrimitive from "@radix-ui/react-label";
4 | import { clsx } from "clsx";
5 | import React from "react";
6 |
7 | const Checkbox = () => {
8 | return (
9 |
31 | );
32 | };
33 |
34 | export { Checkbox };
35 |
--------------------------------------------------------------------------------
/demo/components/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
2 | import { PlayIcon, Share2Icon, TriangleRightIcon } from "@radix-ui/react-icons";
3 | import { clsx } from "clsx";
4 | import React from "react";
5 |
6 | const Collapsible = () => {
7 | const [isOpen, setIsOpen] = React.useState(true);
8 |
9 | return (
10 |
11 |
18 | My Playlists
19 |
20 |
21 |
27 |
28 | {["80s Synthwave", "Maximale Konzentration", "高人氣金曲"].map(
29 | (title) => (
30 |
44 | )
45 | )}
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export { Collapsible };
53 |
--------------------------------------------------------------------------------
/demo/components/context-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
2 | import {
3 | CaretRightIcon,
4 | CheckIcon,
5 | CropIcon,
6 | EyeClosedIcon,
7 | EyeOpenIcon,
8 | FileIcon,
9 | FrameIcon,
10 | GridIcon,
11 | Link2Icon,
12 | MixerHorizontalIcon,
13 | PersonIcon,
14 | TransparencyGridIcon,
15 | } from "@radix-ui/react-icons";
16 | import { clsx } from "clsx";
17 | import React, { type ReactNode, useState } from "react";
18 | import Button from "./shared/button";
19 |
20 | interface RadixMenuItem {
21 | label: string;
22 | shortcut?: string;
23 | icon?: ReactNode;
24 | }
25 |
26 | interface User {
27 | name: string;
28 | url?: string;
29 | }
30 |
31 | const generalMenuItems: RadixMenuItem[] = [
32 | {
33 | label: "New File",
34 | icon: ,
35 | shortcut: "⌘+N",
36 | },
37 | {
38 | label: "Settings",
39 | icon: ,
40 | shortcut: "⌘+,",
41 | },
42 | ];
43 |
44 | const regionToolMenuItems: RadixMenuItem[] = [
45 | {
46 | label: "Frame",
47 | icon: ,
48 | shortcut: "⌘+F",
49 | },
50 | {
51 | label: "Crop",
52 | icon: ,
53 | shortcut: "⌘+S",
54 | },
55 | ];
56 |
57 | const users: User[] = [
58 | {
59 | name: "Adam",
60 | url: "https://github.com/adamwathan.png",
61 | },
62 | {
63 | name: "Steve",
64 | url: "https://github.com/steveschoger.png",
65 | },
66 | {
67 | name: "Robin",
68 | url: "https://github.com/robinmalfait.png",
69 | },
70 | ];
71 |
72 | const ContextMenu = () => {
73 | const [showGrid, setShowGrid] = useState(false);
74 | const [showUi, setShowUi] = useState(false);
75 |
76 | return (
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
91 | {generalMenuItems.map(({ label, icon, shortcut }) => (
92 |
99 | {icon}
100 |
101 | {label}
102 |
103 | {shortcut && {shortcut}}
104 |
105 | ))}
106 |
107 |
108 |
109 | {
112 | if (typeof state === "boolean") {
113 | setShowGrid(state);
114 | }
115 | }}
116 | className={clsx(
117 | "flex w-full cursor-default select-none items-center rounded-md px-2 py-2 text-xs outline-hidden",
118 | "text-gray-400 focus:bg-gray-50 dark:text-gray-500 dark:focus:bg-gray-900"
119 | )}
120 | >
121 | {showGrid ? (
122 |
123 | ) : (
124 |
125 | )}
126 |
127 | Show Grid
128 |
129 |
130 |
131 |
132 |
133 |
134 | {
137 | if (typeof state === "boolean") {
138 | setShowUi(state);
139 | }
140 | }}
141 | className={clsx(
142 | "flex w-full cursor-default select-none items-center rounded-md px-2 py-2 text-xs outline-hidden",
143 | "text-gray-400 focus:bg-gray-50 dark:text-gray-500 dark:focus:bg-gray-900"
144 | )}
145 | >
146 | {showUi ? (
147 |
148 | ) : (
149 |
150 | )}
151 |
152 | Show UI
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 | Region Tools
163 |
164 |
165 | {regionToolMenuItems.map(({ label, icon, shortcut }) => (
166 |
173 | {icon}
174 |
175 | {label}
176 |
177 | {shortcut && {shortcut}}
178 |
179 | ))}
180 |
181 |
182 |
183 |
184 |
190 |
191 |
192 | Share
193 |
194 |
195 |
196 |
197 |
204 | {users.map(({ name, url }, i) => (
205 |
212 | {url ? (
213 |
218 | ) : (
219 |
220 | )}
221 |
222 | {name}
223 |
224 |
225 | ))}
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | );
234 | };
235 |
236 | export { ContextMenu };
237 |
--------------------------------------------------------------------------------
/demo/components/dialog.tsx:
--------------------------------------------------------------------------------
1 | import { Transition } from "@headlessui/react";
2 | import * as DialogPrimitive from "@radix-ui/react-dialog";
3 | import { Cross1Icon } from "@radix-ui/react-icons";
4 | import { clsx } from "clsx";
5 | import React, { Fragment, useState } from "react";
6 | import Button from "./shared/button";
7 |
8 | const Dialog = () => {
9 | const [isOpen, setIsOpen] = useState(false);
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
27 |
31 |
32 |
41 |
51 |
52 | Edit profile
53 |
54 |
55 | Make changes to your profile here. Click save when you're
56 | done.
57 |
58 |
101 |
102 |
103 |
111 | Save
112 |
113 |
114 |
115 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | );
129 | };
130 |
131 | export { Dialog };
132 |
--------------------------------------------------------------------------------
/demo/components/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
2 | import {
3 | CaretRightIcon,
4 | CheckIcon,
5 | CropIcon,
6 | EyeClosedIcon,
7 | EyeOpenIcon,
8 | FileIcon,
9 | FrameIcon,
10 | GridIcon,
11 | Link2Icon,
12 | MixerHorizontalIcon,
13 | PersonIcon,
14 | TransparencyGridIcon,
15 | } from "@radix-ui/react-icons";
16 | import { clsx } from "clsx";
17 | import React, { type ReactNode, useState } from "react";
18 | import Button from "./shared/button";
19 |
20 | interface RadixMenuItem {
21 | label: string;
22 | shortcut?: string;
23 | icon?: ReactNode;
24 | }
25 |
26 | interface User {
27 | name: string;
28 | url?: string;
29 | }
30 |
31 | const generalMenuItems: RadixMenuItem[] = [
32 | {
33 | label: "New File",
34 | icon: ,
35 | shortcut: "⌘+N",
36 | },
37 | {
38 | label: "Settings",
39 | icon: ,
40 | shortcut: "⌘+,",
41 | },
42 | ];
43 |
44 | const regionToolMenuItems: RadixMenuItem[] = [
45 | {
46 | label: "Frame",
47 | icon: ,
48 | shortcut: "⌘+F",
49 | },
50 | {
51 | label: "Crop",
52 | icon: ,
53 | shortcut: "⌘+S",
54 | },
55 | ];
56 |
57 | const users: User[] = [
58 | {
59 | name: "Adam",
60 | url: "https://github.com/adamwathan.png",
61 | },
62 | {
63 | name: "Steve",
64 | url: "https://github.com/steveschoger.png",
65 | },
66 | {
67 | name: "Robin",
68 | url: "https://github.com/robinmalfait.png",
69 | },
70 | ];
71 |
72 | const DropdownMenu = () => {
73 | const [showGrid, setShowGrid] = useState(false);
74 | const [showUi, setShowUi] = useState(false);
75 |
76 | return (
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
93 | {generalMenuItems.map(({ label, icon, shortcut }) => (
94 |
101 | {icon}
102 |
103 | {label}
104 |
105 | {shortcut && {shortcut}}
106 |
107 | ))}
108 |
109 |
110 |
111 | {
114 | if (typeof state === "boolean") {
115 | setShowGrid(state);
116 | }
117 | }}
118 | className={clsx(
119 | "flex w-full cursor-default select-none items-center rounded-md px-2 py-2 text-xs outline-hidden",
120 | "text-gray-400 focus:bg-gray-50 dark:text-gray-500 dark:focus:bg-gray-900"
121 | )}
122 | >
123 | {showGrid ? (
124 |
125 | ) : (
126 |
127 | )}
128 |
129 | Show Grid
130 |
131 |
132 |
133 |
134 |
135 |
136 | {
139 | if (typeof state === "boolean") {
140 | setShowUi(state);
141 | }
142 | }}
143 | className={clsx(
144 | "flex w-full cursor-default select-none items-center rounded-md px-2 py-2 text-xs outline-hidden",
145 | "text-gray-400 focus:bg-gray-50 dark:text-gray-500 dark:focus:bg-gray-900"
146 | )}
147 | >
148 | {showUi ? (
149 |
150 | ) : (
151 |
152 | )}
153 |
154 | Show UI
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | Region Tools
165 |
166 |
167 | {regionToolMenuItems.map(({ label, icon, shortcut }) => (
168 |
175 | {icon}
176 |
177 | {label}
178 |
179 | {shortcut && {shortcut}}
180 |
181 | ))}
182 |
183 |
184 |
185 |
186 |
192 |
193 |
194 | Share
195 |
196 |
197 |
198 |
199 |
206 | {users.map(({ name, url }) => (
207 |
214 | {url ? (
215 |
220 | ) : (
221 |
222 | )}
223 |
224 | {name}
225 |
226 |
227 | ))}
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 | );
236 | };
237 |
238 | export { DropdownMenu };
239 |
--------------------------------------------------------------------------------
/demo/components/hover-card.tsx:
--------------------------------------------------------------------------------
1 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
2 | import { clsx } from "clsx";
3 | import React from "react";
4 |
5 | const TailwindLogo = () => (
6 |
18 | );
19 |
20 | const HoverCard = () => {
21 | return (
22 |
23 |
24 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
45 |
50 |
51 |
52 |
53 |
54 |
55 | Tailwind CSS
56 |
57 |
58 |
59 | A utility-first CSS framework for rapidly building custom user
60 | interfaces.
61 |
62 |
63 |
64 |
65 |
66 | );
67 | };
68 |
69 | export { HoverCard };
70 |
--------------------------------------------------------------------------------
/demo/components/menubar.tsx:
--------------------------------------------------------------------------------
1 | import * as MenubarPrimitive from "@radix-ui/react-menubar";
2 | import {
3 | CheckIcon,
4 | ChevronRightIcon,
5 | DotFilledIcon,
6 | } from "@radix-ui/react-icons";
7 | import { clsx } from "clsx";
8 | import React from "react";
9 |
10 | type MenubarSeparatorProps = Omit<
11 | MenubarPrimitive.MenubarSeparatorProps & React.RefAttributes,
12 | "className"
13 | >;
14 |
15 | const MenubarSeparator = ({ children, ...rest }: MenubarSeparatorProps) => (
16 |
20 | {children}
21 |
22 | );
23 |
24 | type MenubarTriggerProps = Omit<
25 | MenubarPrimitive.MenubarTriggerProps & React.RefAttributes,
26 | "className"
27 | >;
28 |
29 | const MenubarTrigger = ({ children, ...rest }: MenubarTriggerProps) => (
30 |
41 | {children}
42 |
43 | );
44 |
45 | type MenubarSubTriggerProps = Omit<
46 | MenubarPrimitive.MenubarSubTriggerProps & React.RefAttributes,
47 | "className"
48 | >;
49 |
50 | const MenubarSubTrigger = ({ children, ...rest }: MenubarSubTriggerProps) => (
51 |
61 |
62 | {children}
63 |
64 |
65 |
66 | );
67 |
68 | type MenubarItemProps = Omit<
69 | MenubarPrimitive.MenubarItemProps &
70 | React.RefAttributes & { shortcut?: string },
71 | "className"
72 | >;
73 |
74 | const MenubarItem = ({ children, shortcut, ...rest }: MenubarItemProps) => (
75 |
86 |
87 | {children}
88 | {shortcut && (
89 |
90 | {shortcut}
91 |
92 | )}
93 |
94 |
95 | );
96 |
97 | type MenubarCheckboxItemProps = Omit<
98 | MenubarPrimitive.MenubarCheckboxItemProps &
99 | React.RefAttributes,
100 | "className"
101 | >;
102 |
103 | const MenubarCheckboxItem = ({
104 | children,
105 | ...rest
106 | }: MenubarCheckboxItemProps) => (
107 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
{children}
125 |
126 |
127 | );
128 |
129 | type MenubarRadioItemProps = Omit<
130 | MenubarPrimitive.MenubarRadioItemProps & React.RefAttributes,
131 | "className"
132 | >;
133 |
134 | const MenubarRadioItem = ({ children, ...rest }: MenubarRadioItemProps) => (
135 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
{children}
153 |
154 |
155 | );
156 |
157 | const RADIO_ITEMS = ["rauchg", "steventey", "0xca0a"];
158 | const CHECK_ITEMS = ["Always Show Bookmarks Bar", "Always Show Full URLs"];
159 |
160 | const Menubar = () => {
161 | const [checkedSelection, setCheckedSelection] = React.useState([
162 | CHECK_ITEMS[1],
163 | ]);
164 | const [radioSelection, setRadioSelection] = React.useState(RADIO_ITEMS[2]);
165 |
166 | const contentClasses = "bg-white dark:bg-gray-800 rounded-md p-1";
167 |
168 | return (
169 |
170 |
171 | File
172 |
173 |
179 | New Tab
180 | New Window
181 | New Incognito Window
182 |
183 |
184 | Share
185 |
186 |
191 | Email Link
192 | Messages
193 | Notes
194 |
195 |
196 |
197 |
198 |
199 | Print…
200 |
201 |
202 |
203 |
204 |
205 | Edit
206 |
207 |
213 | Undo
214 | Redo
215 |
216 |
217 | Find
218 |
219 |
220 |
225 | Search the web…
226 |
227 |
228 | Find…
229 | Find Next
230 | Find Previous
231 |
232 |
233 |
234 |
235 | Cut
236 | Copy
237 | Paste
238 |
239 |
240 |
241 |
242 |
243 | View
244 |
245 |
251 | {CHECK_ITEMS.map((item) => (
252 |
256 | setCheckedSelection((current) =>
257 | current.includes(item)
258 | ? current.filter((el) => el !== item)
259 | : current.concat(item)
260 | )
261 | }
262 | >
263 | {item}
264 |
265 | ))}
266 |
267 |
268 | Reload
269 | Force Reload
270 |
271 |
272 | Toggle Fullscreen
273 |
274 | Hide Sidebar
275 |
276 |
277 |
278 |
279 |
280 | Profiles
281 |
282 |
288 |
292 | {RADIO_ITEMS.map((item) => (
293 |
294 | {item}
295 |
296 | ))}
297 |
298 |
299 | Edit…
300 |
301 | Add Profile…
302 |
303 |
304 |
305 |
306 |
307 | );
308 | };
309 |
310 | export { Menubar };
311 |
--------------------------------------------------------------------------------
/demo/components/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
2 | import { clsx } from "clsx";
3 | import React from "react";
4 |
5 | const NavigationMenu = () => {
6 | return (
7 |
8 |
9 |
10 |
18 | Overview
19 |
20 |
21 |
30 |
42 |
43 |
44 |
45 |
46 |
53 | Resources
54 |
55 |
56 |
65 |
66 |
67 |
74 |
75 | Tailwind CSS
76 |
77 |
78 |
79 | A utility-first CSS framework for rapidly building custom
80 | user interfaces.
81 |
82 |
83 |
84 |
91 |
92 | Radix UI
93 |
94 |
95 |
96 | An open-source UI component library for building
97 | high-quality, accessible design systems and web apps.
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
113 | GitHub
114 |
115 |
116 |
117 |
126 |
127 |
128 |
129 |
130 |
139 |
149 |
150 |
151 | );
152 | };
153 |
154 | export { NavigationMenu };
155 |
--------------------------------------------------------------------------------
/demo/components/popover.tsx:
--------------------------------------------------------------------------------
1 | import { Cross1Icon } from "@radix-ui/react-icons";
2 | import * as PopoverPrimitive from "@radix-ui/react-popover";
3 | import { clsx } from "clsx";
4 | import React from "react";
5 | import Button from "./shared/button";
6 |
7 | const items = [
8 | {
9 | id: "width",
10 | label: "Width",
11 | defaultValue: "100%",
12 | },
13 | {
14 | id: "max-width",
15 | label: "Max. Width",
16 | defaultValue: "300px",
17 | },
18 | {
19 | id: "height",
20 | label: "Height",
21 | defaultValue: "25px",
22 | },
23 | {
24 | id: "max-height",
25 | label: "Max. Height",
26 | defaultValue: "none",
27 | },
28 | ];
29 |
30 | const Popover = () => {
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
46 |
47 |
48 | Dimensions
49 |
50 |
51 |
80 |
81 |
87 |
88 |
89 |
90 |
91 |
92 | );
93 | };
94 |
95 | export { Popover };
96 |
--------------------------------------------------------------------------------
/demo/components/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as ProgressPrimitive from "@radix-ui/react-progress";
2 | import React, { useEffect } from "react";
3 | import { getRandomArbitrary } from "../utils/math";
4 |
5 | const Progress = () => {
6 | const [progress, setProgress] = React.useState(60);
7 |
8 | useEffect(() => {
9 | let timerId: ReturnType = null;
10 |
11 | timerId = setInterval(() => {
12 | const p = Math.ceil(getRandomArbitrary(0, 100) / 10) * 10;
13 | setProgress(p);
14 | }, 5000);
15 |
16 | return () => {
17 | if (timerId) {
18 | clearInterval(timerId);
19 | }
20 | };
21 | }, []);
22 |
23 | return (
24 |
28 |
32 |
33 | );
34 | };
35 |
36 | export { Progress };
37 |
--------------------------------------------------------------------------------
/demo/components/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import React from "react";
3 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
4 |
5 | const starters = [
6 | { id: "red", title: "Bulbasaur" },
7 | { id: "green", title: "Charmader" },
8 | { id: "blue", title: "Squirtle" },
9 | ];
10 |
11 | const RadioGroup = () => {
12 | const [_value, setValue] = React.useState(starters[0].title);
13 |
14 | return (
15 |
54 | );
55 | };
56 |
57 | export { RadioGroup };
58 |
--------------------------------------------------------------------------------
/demo/components/select.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CheckIcon,
3 | ChevronDownIcon,
4 | ChevronUpIcon,
5 | } from "@radix-ui/react-icons";
6 | import * as SelectPrimitive from "@radix-ui/react-select";
7 | import { clsx } from "clsx";
8 | import React from "react";
9 | import Button from "./shared/button";
10 |
11 | const Select = () => {
12 | return (
13 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {["Apple", "Banana", "Blueberry", "Strawberry", "Grapes"].map(
29 | (f) => (
30 |
40 | {f}
41 |
42 |
43 |
44 |
45 | )
46 | )}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export { Select };
58 |
--------------------------------------------------------------------------------
/demo/components/shared/button.tsx:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import React from "react";
3 |
4 | type Props = Omit, "className"> & {};
5 |
6 | const Button = React.forwardRef(
7 | ({ children, ...props }, ref) => (
8 |
26 | )
27 | );
28 |
29 | Button.displayName = "Button";
30 | export default Button;
31 |
--------------------------------------------------------------------------------
/demo/components/shared/command-menu.tsx:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import React, { useEffect, useState } from "react";
3 | import { Command } from "cmdk";
4 | import { DocumentMagnifyingGlassIcon } from "@heroicons/react/24/outline";
5 | import { DividerVerticalIcon } from "@radix-ui/react-icons";
6 |
7 | interface CommandMenuProps {
8 | items: T[];
9 | onSelect?: (data: {
10 | item: T;
11 | modifiers: {
12 | control: boolean;
13 | };
14 | }) => void;
15 | }
16 |
17 | function CommandMenu({
18 | items,
19 | onSelect,
20 | }: CommandMenuProps) {
21 | const [open, setOpen] = useState(false);
22 | const [isHoldingModifier, setIsHoldingModifier] = useState(false);
23 |
24 | useEffect(() => {
25 | const down = (e: KeyboardEvent) => {
26 | // Toggle the menu when ⌘K is pressed
27 | if (e.key === "k" && e.metaKey) {
28 | e.preventDefault();
29 | setOpen((open) => !open);
30 | }
31 | };
32 |
33 | document.addEventListener("keydown", down);
34 | return () => document.removeEventListener("keydown", down);
35 | }, []);
36 |
37 | useEffect(() => {
38 | const keyDown = (e: KeyboardEvent) => {
39 | if (e.key === "Control") {
40 | setIsHoldingModifier(true);
41 | }
42 | };
43 |
44 | const keyUp = (e: KeyboardEvent) => {
45 | if (e.key === "Control") {
46 | setIsHoldingModifier(false);
47 | }
48 | };
49 |
50 | document.addEventListener("keydown", keyDown);
51 | document.addEventListener("keyup", keyUp);
52 | return () => {
53 | document.removeEventListener("keydown", keyDown);
54 | document.removeEventListener("keyup", keyUp);
55 | };
56 | }, []);
57 |
58 | return (
59 |
71 |
72 |
81 | {
83 | if (e.key === "Escape") {
84 | setOpen(false);
85 | }
86 | }}
87 | onClick={() => setOpen(false)}
88 | className="select-none hover:cursor-pointer w-auto h-5 flex items-center justify-center absolute rounded-md text-[0.6rem] right-5 top-1/2 text-gray-700 dark:text-gray-300 font-bold -translate-y-1/2 bg-black/10 dark:bg-white/10 px-1.5 leading-none"
89 | >
90 | ESC
91 |
92 |
93 |
97 |
105 |
106 |
107 | No Results
108 |
109 | {items.map((item) => (
110 | {
113 | onSelect({ item, modifiers: { control: isHoldingModifier } });
114 | setOpen(false);
115 | }}
116 | className={clsx(
117 | "px-3 py-3",
118 | "cursor-pointer",
119 | "flex items-center rounded-md text-sm text-gray-700 dark:text-gray-300 font-medium",
120 | "aria-selected:bg-black/10 dark:aria-selected:bg-white/10",
121 | "focus:outline-hidden select-none"
122 | )}
123 | >
124 | {item.label}
125 |
126 | ))}
127 |
128 |
129 |
130 |
138 |
139 |
140 |
141 | Go to
142 |
143 | ↵
144 |
145 |
146 |
147 |
148 |
Open code
149 |
150 |
151 | ⌃
152 |
153 | +
154 |
155 | ↵
156 |
157 |
158 |
159 |
160 |
161 |
162 | );
163 | }
164 |
165 | export { CommandMenu };
166 |
--------------------------------------------------------------------------------
/demo/components/shared/demo-card.tsx:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import type { ReactNode } from "react";
3 |
4 | enum Variant {
5 | Default = 0,
6 | ItemsCenter = 1,
7 | JustifyCenter = 2,
8 | }
9 |
10 | type DemoCardProps = {
11 | variant?: Variant;
12 | isNew?: boolean;
13 | children: ReactNode;
14 | data: {
15 | title: string;
16 | link: string;
17 | };
18 | };
19 |
20 | const DemoCard = ({
21 | variant = Variant.Default,
22 | isNew,
23 | children,
24 | data: { title, link },
25 | }: DemoCardProps) => {
26 | const id = title.replace(" ", "_").toLowerCase();
27 |
28 | return (
29 |
30 |
43 | {children}
44 |
67 |
68 |
69 | );
70 | };
71 |
72 | DemoCard.variant = Variant;
73 | export { DemoCard };
74 |
--------------------------------------------------------------------------------
/demo/components/shared/theme-switcher.tsx:
--------------------------------------------------------------------------------
1 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
2 | import { Half2Icon, MoonIcon, SunIcon } from "@radix-ui/react-icons";
3 | import { clsx } from "clsx";
4 | import React, { useEffect, useState } from "react";
5 |
6 | const themes = [
7 | {
8 | key: "light",
9 | label: "Light",
10 | icon: ,
11 | },
12 | {
13 | key: "dark",
14 | label: "Dark",
15 | icon: ,
16 | },
17 |
18 | {
19 | key: "system",
20 | label: "System",
21 | icon: ,
22 | },
23 | ];
24 |
25 | const ThemeSwitcher = () => {
26 | const [preferredTheme, setPreferredTheme] = useState(null);
27 |
28 | useEffect(() => {
29 | try {
30 | const found = localStorage.getItem("theme");
31 | setPreferredTheme(found);
32 | } catch (error) {}
33 | }, []);
34 |
35 | useEffect(() => {
36 | const prefersDarkQuery = window.matchMedia("(prefers-color-scheme: dark)");
37 | const updateTheme = (_e: MediaQueryListEvent) => {
38 | setPreferredTheme("system");
39 | };
40 | prefersDarkQuery.addEventListener("change", updateTheme);
41 |
42 | return () => {
43 | prefersDarkQuery.removeEventListener("change", updateTheme);
44 | };
45 | }, []);
46 |
47 | return (
48 |
49 |
50 |
58 | {(() => {
59 | switch (preferredTheme) {
60 | case "light":
61 | return (
62 |
63 | );
64 | case "dark":
65 | return (
66 |
67 | );
68 | default:
69 | return (
70 |
71 | );
72 | }
73 | })()}
74 | {/* {isDark ? "dark" : "light"} */}
75 |
76 |
77 |
78 |
87 | {themes.map(({ key, label, icon }) => {
88 | return (
89 | {
96 | (
97 | window as unknown as Window & {
98 | __setPreferredTheme: (key: string) => void;
99 | }
100 | ).__setPreferredTheme(key);
101 | setPreferredTheme(key);
102 | }}
103 | >
104 | {React.cloneElement(icon, {
105 | className: "w-5 h-5 mr-2 text-gray-700 dark:text-gray-300",
106 | })}
107 |
108 | {label}
109 |
110 |
111 | );
112 | })}
113 |
114 |
115 |
116 |
117 | );
118 | };
119 |
120 | export { ThemeSwitcher };
121 |
--------------------------------------------------------------------------------
/demo/components/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as SliderPrimitive from "@radix-ui/react-slider";
2 | import { clsx } from "clsx";
3 | import React from "react";
4 |
5 | const Slider = () => {
6 | return (
7 |
14 |
15 |
16 |
17 |
23 |
24 | );
25 | };
26 |
27 | export { Slider };
28 |
--------------------------------------------------------------------------------
/demo/components/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as SwitchPrimitive from "@radix-ui/react-switch";
2 | import { clsx } from "clsx";
3 | import React from "react";
4 |
5 | const Switch = () => {
6 | return (
7 |
16 |
23 |
24 | );
25 | };
26 |
27 | export { Switch };
28 |
--------------------------------------------------------------------------------
/demo/components/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as TabsPrimitive from "@radix-ui/react-tabs";
2 | import { clsx } from "clsx";
3 | import React from "react";
4 |
5 | interface Tab {
6 | title: string;
7 | value: string;
8 | }
9 |
10 | const tabs: Tab[] = [
11 | {
12 | title: "Inbox",
13 | value: "tab1",
14 | },
15 | {
16 | title: "Today",
17 | value: "tab2",
18 | },
19 |
20 | {
21 | title: "Upcoming",
22 | value: "tab3",
23 | },
24 | ];
25 |
26 | const Tabs = () => {
27 | return (
28 |
29 |
32 | {tabs.map(({ title, value }) => (
33 |
47 |
53 | {title}
54 |
55 |
56 | ))}
57 |
58 | {tabs.map(({ value }) => (
59 |
64 |
65 | {
66 | {
67 | tab1: "Your inbox is empty",
68 | tab2: "Make some coffee",
69 | tab3: "Order more coffee",
70 | }[value]
71 | }
72 |
73 |
74 | ))}
75 |
76 | );
77 | };
78 |
79 | export { Tabs };
80 |
--------------------------------------------------------------------------------
/demo/components/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as ToastPrimitive from "@radix-ui/react-toast";
2 | import { clsx } from "clsx";
3 | import React from "react";
4 | import { useMediaQuery } from "../hooks/use-media-query";
5 | import Button from "./shared/button";
6 |
7 | const Toast = () => {
8 | const [open, setOpen] = React.useState(false);
9 | const isMd = useMediaQuery("(min-width: 768px)");
10 |
11 | return (
12 |
13 |
27 |
43 |
44 |
45 |
46 |
47 | Pull Request Review
48 |
49 |
50 | Someone requested your review on{" "}
51 | repository/branch
52 |
53 |
54 |
55 |
56 |
57 |
58 | {
62 | e.preventDefault();
63 | window.open("https://github.com");
64 | }}
65 | >
66 | Review
67 |
68 |
69 |
70 |
71 | Dismiss
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | export { Toast };
85 |
--------------------------------------------------------------------------------
/demo/components/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FontBoldIcon,
3 | FontItalicIcon,
4 | UnderlineIcon,
5 | } from "@radix-ui/react-icons";
6 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
7 | import { clsx } from "clsx";
8 | import React, { type ReactElement } from "react";
9 |
10 | interface ToggleItem {
11 | value: string;
12 | label: string;
13 | icon: ReactElement;
14 | }
15 |
16 | const settings: ToggleItem[] = [
17 | {
18 | value: "bold",
19 | label: "Font bold",
20 | icon: ,
21 | },
22 | {
23 | value: "italic",
24 | label: "Font italic",
25 | icon: ,
26 | },
27 | {
28 | value: "underline",
29 | label: "Underline",
30 | icon: ,
31 | },
32 | ];
33 |
34 | const ToggleGroup = () => {
35 | return (
36 |
37 | {settings.map(({ value, label, icon }, i) => (
38 |
50 | {React.cloneElement(icon, {
51 | className: "w-5 h-5 text-gray-700 dark:text-gray-100",
52 | })}
53 |
54 | ))}
55 |
56 | );
57 | };
58 |
59 | export { ToggleGroup };
60 |
--------------------------------------------------------------------------------
/demo/components/toggle.tsx:
--------------------------------------------------------------------------------
1 | import { StarFilledIcon, StarIcon } from "@radix-ui/react-icons";
2 | import * as TogglePrimitive from "@radix-ui/react-toggle";
3 | import React, { useState } from "react";
4 | import Button from "./shared/button";
5 |
6 | const Toggle = () => {
7 | const [starred, setStarred] = useState(false);
8 |
9 | return (
10 |
15 |
23 |
24 | );
25 | };
26 |
27 | export { Toggle };
28 |
--------------------------------------------------------------------------------
/demo/components/toolbar.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FontBoldIcon,
3 | FontItalicIcon,
4 | TextAlignCenterIcon,
5 | TextAlignLeftIcon,
6 | TextAlignRightIcon,
7 | UnderlineIcon,
8 | } from "@radix-ui/react-icons";
9 | import * as ToolbarPrimitive from "@radix-ui/react-toolbar";
10 | import { clsx } from "clsx";
11 | import React, { type ReactElement } from "react";
12 |
13 | interface ToggleItem {
14 | value: string;
15 | label: string;
16 | icon: ReactElement;
17 | }
18 |
19 | const fontSettings: ToggleItem[] = [
20 | {
21 | value: "bold",
22 | label: "Font bold",
23 | icon: ,
24 | },
25 | {
26 | value: "italic",
27 | label: "Font italic",
28 | icon: ,
29 | },
30 | {
31 | value: "underline",
32 | label: "Underline",
33 | icon: ,
34 | },
35 | ];
36 |
37 | const textSettings: ToggleItem[] = [
38 | {
39 | value: "left",
40 | label: "Text left",
41 | icon: ,
42 | },
43 | {
44 | value: "center",
45 | label: "Text center",
46 | icon: ,
47 | },
48 | {
49 | value: "right",
50 | label: "Text right",
51 | icon: ,
52 | },
53 | ];
54 |
55 | const Toolbar = () => {
56 | return (
57 |
58 |
59 | {fontSettings.map(({ value, label, icon }, i) => (
60 |
72 | {React.cloneElement(icon, {
73 | className: "w-5 h-5 text-gray-700 dark:text-gray-100",
74 | })}
75 |
76 | ))}
77 |
78 |
79 |
84 | {textSettings.map(({ value, label, icon }, i) => (
85 |
97 | {React.cloneElement(icon, {
98 | className: "w-5 h-5 text-gray-700 dark:text-gray-100",
99 | })}
100 |
101 | ))}
102 |
103 |
104 |
105 |
106 |
107 |
115 | Edited 2 hours ago
116 |
117 |
118 |
119 | );
120 | };
121 |
122 | export { Toolbar };
123 |
--------------------------------------------------------------------------------
/demo/components/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2 | import { clsx } from "clsx";
3 | import React from "react";
4 | import Button from "./shared/button";
5 |
6 | const Tooltip = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
24 |
25 |
26 | Sorry, but our princess is in another castle
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export { Tooltip };
35 |
--------------------------------------------------------------------------------
/demo/css/tailwind.css:
--------------------------------------------------------------------------------
1 | @import url("https://rsms.me/inter/inter.css") layer(base);
2 |
3 | @import "tailwindcss";
4 |
5 | @config '../tailwind.config.js';
6 |
7 | @theme {
8 | --animate-accordion-slide-down: accordion-slide-down 300ms
9 | cubic-bezier(0.87, 0, 0.13, 1);
10 |
11 | @keyframes accordion-slide-down {
12 | 0% {
13 | height: 0;
14 | }
15 | 100% {
16 | height: var(--radix-accordion-content-height);
17 | }
18 | }
19 |
20 | --animate-accordion-slide-up: accordion-slide-up 300ms
21 | cubic-bezier(0.87, 0, 0.13, 1);
22 |
23 | @keyframes accordion-slide-up {
24 | 0% {
25 | height: var(--radix-accordion-content-height);
26 | }
27 | 100% {
28 | height: 0;
29 | }
30 | }
31 |
32 | --animate-collapsible-slide-down: collapsible-slide-down 300ms
33 | cubic-bezier(0.87, 0, 0.13, 1);
34 |
35 | @keyframes collapsible-slide-down {
36 | 0% {
37 | height: 0;
38 | }
39 | 100% {
40 | height: var(--radix-collapsible-content-height);
41 | }
42 | }
43 |
44 | --animate-collapsible-slide-up: collapsible-slide-up 300ms
45 | cubic-bezier(0.87, 0, 0.13, 1);
46 |
47 | @keyframes collapsible-slide-up {
48 | 0% {
49 | height: var(--radix-collapsible-content-height);
50 | }
51 | 100% {
52 | height: 0;
53 | }
54 | }
55 | }
56 |
57 | /*
58 | The default border color has changed to `currentColor` in Tailwind CSS v4,
59 | so we've added these compatibility styles to make sure everything still
60 | looks the same as it did with Tailwind CSS v3.
61 |
62 | If we ever want to remove these styles, we need to add an explicit border
63 | color utility to any element that depends on these defaults.
64 | */
65 | @layer base {
66 | *,
67 | ::after,
68 | ::before,
69 | ::backdrop,
70 | ::file-selector-button {
71 | border-color: var(--color-gray-200, currentColor);
72 | }
73 | }
74 |
75 | @layer base {
76 | :root {
77 | @apply scroll-smooth font-display antialiased;
78 | }
79 |
80 | html {
81 | @apply bg-white;
82 | scrollbar-color: #00000080 transparent;
83 | scrollbar-width: auto;
84 | }
85 |
86 | html[class*="dark"] {
87 | @apply bg-gray-900;
88 | scrollbar-color: #ffffffb3 transparent;
89 | scrollbar-width: auto;
90 | }
91 |
92 | ::-webkit-scrollbar {
93 | height: 8px;
94 | width: 8px;
95 | }
96 |
97 | ::-webkit-scrollbar-track {
98 | @apply bg-transparent;
99 | }
100 |
101 | ::-webkit-scrollbar-thumb {
102 | @apply rounded-full bg-black/50 dark:bg-white/70;
103 | }
104 |
105 | ::-webkit-scrollbar-thumb:hover {
106 | @apply bg-black/60 dark:hover:bg-white/80;
107 | }
108 |
109 | button {
110 | /* Disables tap highlight color on Chrome */
111 | -webkit-tap-highlight-color: transparent;
112 | }
113 |
114 | /* Input darkmode fix for checkbox / radio */
115 | .dark [type="checkbox"]:checked,
116 | .dark [type="radio"]:checked {
117 | background-color: currentColor;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/demo/hooks/use-dark-mode.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export const useDarkMode = () => {
4 | const [isDark, setIsDark] = useState(true);
5 |
6 | useEffect(() => {
7 | const observer = new MutationObserver(() => {
8 | const containsDarkClass =
9 | document.documentElement.classList.contains("dark");
10 | setIsDark(containsDarkClass);
11 | });
12 |
13 | observer.observe(document.documentElement, { attributes: true });
14 | return () => {
15 | observer.disconnect();
16 | };
17 | }, []);
18 |
19 | return isDark;
20 | };
21 |
--------------------------------------------------------------------------------
/demo/hooks/use-media-query.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export function useMediaQuery(query: string) {
4 | const [matches, setMatches] = useState(false);
5 | useEffect(
6 | () => {
7 | const mediaQuery = window.matchMedia(query);
8 | // Update the state with the current value
9 | setMatches(mediaQuery.matches);
10 | // Create an event listener
11 | const handler = (event: MediaQueryListEvent) => setMatches(event.matches);
12 | // Attach the event listener to know when the matches value changes
13 | mediaQuery.addEventListener("change", handler);
14 | // Remove the event listener on cleanup
15 | return () => mediaQuery.removeEventListener("change", handler);
16 | },
17 | [query] // Empty array ensures effect is only run on mount and unmount
18 | );
19 | return matches;
20 | }
21 |
--------------------------------------------------------------------------------
/demo/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/demo/next.config.js:
--------------------------------------------------------------------------------
1 | const withTM = require("next-transpile-modules")(["react-github-btn"]);
2 |
3 | module.exports = withTM();
4 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "next dev",
5 | "build": "NEXT_TELEMETRY_DISABLED=1 next build",
6 | "start": "next start"
7 | },
8 | "dependencies": {
9 | "@headlessui/react": "^1.7.18",
10 | "@heroicons/react": "^2.1.3",
11 | "@radix-ui/react-accordion": "^1.1.2",
12 | "@radix-ui/react-alert-dialog": "^1.0.5",
13 | "@radix-ui/react-aspect-ratio": "^1.0.3",
14 | "@radix-ui/react-avatar": "^1.0.4",
15 | "@radix-ui/react-checkbox": "^1.0.4",
16 | "@radix-ui/react-collapsible": "^1.0.3",
17 | "@radix-ui/react-context-menu": "^2.1.5",
18 | "@radix-ui/react-dialog": "^1.0.5",
19 | "@radix-ui/react-dropdown-menu": "^2.0.6",
20 | "@radix-ui/react-hover-card": "^1.0.7",
21 | "@radix-ui/react-icons": "^1.3.0",
22 | "@radix-ui/react-label": "^2.0.2",
23 | "@radix-ui/react-menubar": "^1.0.4",
24 | "@radix-ui/react-navigation-menu": "^1.1.4",
25 | "@radix-ui/react-popover": "^1.0.7",
26 | "@radix-ui/react-progress": "^1.0.3",
27 | "@radix-ui/react-radio-group": "^1.1.3",
28 | "@radix-ui/react-scroll-area": "^1.0.5",
29 | "@radix-ui/react-select": "^2.0.0",
30 | "@radix-ui/react-separator": "^1.0.3",
31 | "@radix-ui/react-slider": "^1.1.2",
32 | "@radix-ui/react-switch": "^1.0.3",
33 | "@radix-ui/react-tabs": "^1.0.4",
34 | "@radix-ui/react-toast": "^1.1.5",
35 | "@radix-ui/react-toggle": "^1.0.3",
36 | "@radix-ui/react-toggle-group": "^1.0.4",
37 | "@radix-ui/react-toolbar": "^1.0.4",
38 | "@radix-ui/react-tooltip": "^1.0.7",
39 | "@vercel/analytics": "^1.2.2",
40 | "clsx": "^2.1.0",
41 | "cmdk": "^1.0.0",
42 | "next": "14.1.4",
43 | "next-seo": "^6.5.0",
44 | "react": "^18.2.0",
45 | "react-dom": "^18.2.0",
46 | "react-github-btn": "^1.4.0"
47 | },
48 | "devDependencies": {
49 | "@tailwindcss/forms": "^0.5.7",
50 | "@tailwindcss/postcss": "^4.0.0",
51 | "@types/node": "^20.11.30",
52 | "@types/react": "^18.2.69",
53 | "eslint": "8.57.0",
54 | "eslint-config-next": "14.1.4",
55 | "next-transpile-modules": "^10.0.1",
56 | "postcss": "^8.4.38",
57 | "prettier": "^3.2.5",
58 | "prettier-plugin-tailwindcss": "^0.6.11",
59 | "tailwindcss": "^4.0.0",
60 | "tailwindcss-radix": "^4.0.1",
61 | "typescript": "^5.4.3"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/demo/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "../css/tailwind.css";
2 | import { Analytics } from "@vercel/analytics/react";
3 |
4 | function App({ Component, pageProps }) {
5 | return (
6 | <>
7 |
8 |
9 | >
10 | );
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/demo/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-sync-scripts */
2 | import Document, { Head, Html, Main, NextScript } from "next/document";
3 |
4 | class MyDocument extends Document {
5 | static async getInitialProps(ctx) {
6 | const initialProps = await Document.getInitialProps(ctx);
7 | return { ...initialProps };
8 | }
9 |
10 | render() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 | }
25 |
26 | export default MyDocument;
27 |
--------------------------------------------------------------------------------
/demo/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2 | import { NextSeo } from "next-seo";
3 | import Head from "next/head";
4 | import { Fragment } from "react";
5 | import { clsx } from "clsx";
6 | import GitHubButton from "react-github-btn";
7 | import { Accordion } from "../components/accordion";
8 | import { AlertDialog } from "../components/alert-dialog";
9 | import { AspectRatio } from "../components/aspect-ratio";
10 | import { Avatar } from "../components/avatar";
11 | import { Checkbox } from "../components/checkbox";
12 | import { Collapsible } from "../components/collapsible";
13 | import { ContextMenu } from "../components/context-menu";
14 | import { Dialog } from "../components/dialog";
15 | import { DropdownMenu } from "../components/dropdown-menu";
16 | import { HoverCard } from "../components/hover-card";
17 | import { Menubar } from "../components/menubar";
18 | import { NavigationMenu } from "../components/navigation-menu";
19 | import { Popover } from "../components/popover";
20 | import { Progress } from "../components/progress";
21 | import { RadioGroup } from "../components/radio-group";
22 | import { Select } from "../components/select";
23 | import { DemoCard } from "../components/shared/demo-card";
24 | import { ThemeSwitcher } from "../components/shared/theme-switcher";
25 | import { Slider } from "../components/slider";
26 | import { Switch } from "../components/switch";
27 | import { Tabs } from "../components/tabs";
28 | import { Toast } from "../components/toast";
29 | import { Toggle } from "../components/toggle";
30 | import { ToggleGroup } from "../components/toggle-group";
31 | import { Toolbar } from "../components/toolbar";
32 | import { Tooltip } from "../components/tooltip";
33 | import { CommandMenu } from "../components/shared/command-menu";
34 | import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
35 |
36 | const REPO_URL = "https://github.com/ecklf/tailwindcss-radix/blob/main/demo";
37 |
38 | interface RadixComponent {
39 | label: string;
40 | link: string;
41 | component: JSX.Element;
42 | isNew?: boolean;
43 | center?: boolean;
44 | wrapper?: React.ReactNode;
45 | }
46 |
47 | const RADIX_COMPONENTS: RadixComponent[] = [
48 | {
49 | label: "Accordion",
50 | link: `${REPO_URL}/components/accordion.tsx`,
51 | center: true,
52 | component: (
53 |
56 | ),
57 | },
58 | {
59 | label: "Alert Dialog",
60 | link: `${REPO_URL}/components/alert-dialog.tsx`,
61 | component: ,
62 | },
63 | {
64 | label: "Aspect Ratio",
65 | link: `${REPO_URL}/components/aspect-ratio.tsx`,
66 | component: (
67 |
70 | ),
71 | },
72 | {
73 | label: "Avatar",
74 | link: `${REPO_URL}/components/avatar.tsx`,
75 | component: (
76 |
82 | ),
83 | },
84 | {
85 | label: "Checkbox",
86 | link: `${REPO_URL}/components/checkbox.tsx`,
87 | component: (
88 |
89 |
90 |
91 | ),
92 | },
93 | {
94 | label: "Collapsible",
95 | link: `${REPO_URL}/components/collapsible.tsx`,
96 | center: true,
97 | component: (
98 |
99 |
100 |
101 | ),
102 | },
103 | {
104 | label: "Context Menu",
105 | link: `${REPO_URL}/components/context-menu.tsx`,
106 | center: true,
107 | component: ,
108 | },
109 | {
110 | label: "Dialog",
111 | link: `${REPO_URL}/components/dialog.tsx`,
112 | component: ,
113 | },
114 | {
115 | label: "Dropdown Menu",
116 | link: `${REPO_URL}/components/dropdown-menu.tsx`,
117 | center: true,
118 | component: ,
119 | },
120 | {
121 | label: "Hover Card",
122 | link: `${REPO_URL}/components/hover-card.tsx`,
123 | component: ,
124 | },
125 | {
126 | label: "Menubar",
127 | link: `${REPO_URL}/components/menubar.tsx`,
128 | center: true,
129 | component: (
130 |
131 |
132 |
133 | ),
134 | },
135 | {
136 | label: "Navigation Menu",
137 | link: `${REPO_URL}/components/navigation-menu.tsx`,
138 | center: true,
139 | component: (
140 |
141 |
142 |
143 | ),
144 | },
145 | {
146 | label: "Popover",
147 | link: `${REPO_URL}/components/popover.tsx`,
148 | center: true,
149 | component: ,
150 | },
151 | {
152 | label: "Progress",
153 | link: `${REPO_URL}/components/progress.tsx`,
154 | component: (
155 |
158 | ),
159 | },
160 | {
161 | label: "Radio Group",
162 | link: `${REPO_URL}/components/radio-group.tsx`,
163 | component: (
164 |
165 |
166 |
167 | ),
168 | },
169 | {
170 | label: "Select",
171 | link: `${REPO_URL}/components/select.tsx`,
172 | component: ,
173 | },
174 | {
175 | label: "Slider",
176 | link: `${REPO_URL}/components/slider.tsx`,
177 | component: ,
178 | },
179 | {
180 | label: "Switch",
181 | link: `${REPO_URL}/components/switch.tsx`,
182 | component: ,
183 | },
184 | {
185 | label: "Tabs",
186 | link: `${REPO_URL}/components/tabs.tsx`,
187 | component: (
188 |
189 |
190 |
191 | ),
192 | },
193 | {
194 | label: "Toast",
195 | link: `${REPO_URL}/components/toast.tsx`,
196 | component: (
197 |
198 |
199 |
200 | ),
201 | },
202 | {
203 | label: "Toggle",
204 | link: `${REPO_URL}/components/toggle.tsx`,
205 | component: ,
206 | },
207 | {
208 | label: "Toggle Group",
209 | link: `${REPO_URL}/components/toggle-group.tsx`,
210 | component: ,
211 | },
212 | {
213 | label: "Toolbar",
214 | link: `${REPO_URL}/components/toolbar.tsx`,
215 | component: ,
216 | },
217 | {
218 | label: "Tooltip",
219 | link: `${REPO_URL}/components/tooltip.tsx`,
220 | component: ,
221 | },
222 | ];
223 |
224 | const Hero = () => {
225 | return (
226 |
227 |
245 |
246 |
261 |
262 |
280 |
281 |
282 |
283 |
291 | Star
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
308 |
309 |
310 |
311 |
322 |
323 |
324 |
325 | Show Menu
326 |
327 |
328 |
329 | ⌘{" "}
330 |
331 | +
332 |
333 | K
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 | );
346 | };
347 |
348 | const Demo = () => {
349 | return (
350 |
351 |
352 | Tailwind CSS Radix
353 |
354 |
355 |
356 |
380 |
381 |
382 |
383 | items={RADIX_COMPONENTS}
384 | onSelect={({ item, modifiers }) => {
385 | if (modifiers.control) {
386 | const newWindow = window.open(item.link, "_blank");
387 | if (newWindow) setTimeout(() => newWindow.focus(), 10);
388 | } else {
389 | const element = document.getElementById(
390 | item.label.replace(" ", "_").toLowerCase()
391 | );
392 | if (element) {
393 | requestAnimationFrame(() =>
394 | element.scrollIntoView({ behavior: "smooth" })
395 | );
396 | }
397 | }
398 | }}
399 | />
400 |
401 |
402 | {RADIX_COMPONENTS.map(({ label, link, component, center }) => (
403 |
411 | {component}
412 |
413 | ))}
414 |
415 |
416 | );
417 | };
418 |
419 | export default Demo;
420 |
--------------------------------------------------------------------------------
/demo/postcss.config.js:
--------------------------------------------------------------------------------
1 | // If you want to use other PostCSS plugins, see the following:
2 | // https://tailwindcss.com/docs/using-with-preprocessors
3 | module.exports = {
4 | plugins: {
5 | '@tailwindcss/postcss': {},
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/demo/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | tailwindConfig: "./tailwind.config.js",
3 | };
4 |
--------------------------------------------------------------------------------
/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ecklf/tailwindcss-radix/1060113fd79be34b96b6c528e06c1a95edd6fa3d/demo/public/favicon.ico
--------------------------------------------------------------------------------
/demo/public/static/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ecklf/tailwindcss-radix/1060113fd79be34b96b6c528e06c1a95edd6fa3d/demo/public/static/og.png
--------------------------------------------------------------------------------
/demo/public/static/og.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ecklf/tailwindcss-radix/1060113fd79be34b96b6c528e06c1a95edd6fa3d/demo/public/static/og.webp
--------------------------------------------------------------------------------
/demo/public/static/theme.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | function changeTheme(newTheme) {
3 | window.__theme = newTheme;
4 | if (newTheme === "light") {
5 | document.documentElement.classList.remove("dark");
6 | // Set Safari status bar color to white
7 | document
8 | .querySelector('meta[name="theme-color"]')
9 | .setAttribute("content", "#fffff");
10 | } else {
11 | document.documentElement.classList.add("dark");
12 | // Set Safari status bar color to gray-900
13 | document
14 | .querySelector('meta[name="theme-color"]')
15 | .setAttribute("content", "#111827");
16 | }
17 | }
18 |
19 | const prefersDarkQuery = window.matchMedia("(prefers-color-scheme: dark)");
20 | let preferredTheme;
21 |
22 | try {
23 | // Obtain the local user theme setting if available
24 | preferredTheme = localStorage.getItem("theme");
25 | } catch (error) {}
26 |
27 | window.__setPreferredTheme = (newTheme) => {
28 | if (newTheme !== "system") {
29 | changeTheme(newTheme);
30 | } else {
31 | changeTheme(prefersDarkQuery.matches ? "dark" : "light");
32 | }
33 |
34 | try {
35 | localStorage.setItem("theme", newTheme);
36 | } catch (err) {}
37 | };
38 |
39 | prefersDarkQuery.addEventListener("change", () => {
40 | // window.__setPreferredTheme(e.matches ? "dark" : "light");
41 | window.__setPreferredTheme("system");
42 | });
43 |
44 | if (preferredTheme && preferredTheme !== "system") {
45 | changeTheme(preferredTheme);
46 | } else {
47 | changeTheme(prefersDarkQuery.matches ? "dark" : "light");
48 | }
49 | })();
50 |
--------------------------------------------------------------------------------
/demo/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./pages/**/*.{js,ts,jsx,tsx}",
4 | "./components/**/*.{js,ts,jsx,tsx}",
5 | ],
6 | darkMode: "class",
7 | theme: {
8 | fontFamily: {
9 | display: [
10 | "Inter",
11 | "-apple-system",
12 | "BlinkMacSystemFont",
13 | '"Segoe UI"',
14 | "Roboto",
15 | '"Helvetica Neue"',
16 | "Arial",
17 | '"Noto Sans"',
18 | "sans-serif",
19 | '"Apple Color Emoji"',
20 | '"Segoe UI Emoji"',
21 | '"Segoe UI Symbol"',
22 | '"Noto Color Emoji"',
23 | ],
24 | },
25 | extend: {
26 | boxShadow: {
27 | slider: "0 0 0 5px rgba(0, 0, 0, 0.3)",
28 | },
29 | keyframes: {
30 | // Dropdown menu
31 | "scale-in": {
32 | "0%": { opacity: 0, transform: "scale(0)" },
33 | "100%": { opacity: 1, transform: "scale(1)" },
34 | },
35 | "slide-down": {
36 | "0%": { opacity: 0, transform: "translateY(-10px)" },
37 | "100%": { opacity: 1, transform: "translateY(0)" },
38 | },
39 | "slide-up": {
40 | "0%": { opacity: 0, transform: "translateY(10px)" },
41 | "100%": { opacity: 1, transform: "translateY(0)" },
42 | },
43 | // Tooltip
44 | "slide-up-fade": {
45 | "0%": { opacity: 0, transform: "translateY(2px)" },
46 | "100%": { opacity: 1, transform: "translateY(0)" },
47 | },
48 | "slide-right-fade": {
49 | "0%": { opacity: 0, transform: "translateX(-2px)" },
50 | "100%": { opacity: 1, transform: "translateX(0)" },
51 | },
52 | "slide-down-fade": {
53 | "0%": { opacity: 0, transform: "translateY(-2px)" },
54 | "100%": { opacity: 1, transform: "translateY(0)" },
55 | },
56 | "slide-left-fade": {
57 | "0%": { opacity: 0, transform: "translateX(2px)" },
58 | "100%": { opacity: 1, transform: "translateX(0)" },
59 | },
60 | // Navigation menu
61 | "enter-from-right": {
62 | "0%": { transform: "translateX(200px)", opacity: 0 },
63 | "100%": { transform: "translateX(0)", opacity: 1 },
64 | },
65 | "enter-from-left": {
66 | "0%": { transform: "translateX(-200px)", opacity: 0 },
67 | "100%": { transform: "translateX(0)", opacity: 1 },
68 | },
69 | "exit-to-right": {
70 | "0%": { transform: "translateX(0)", opacity: 1 },
71 | "100%": { transform: "translateX(200px)", opacity: 0 },
72 | },
73 | "exit-to-left": {
74 | "0%": { transform: "translateX(0)", opacity: 1 },
75 | "100%": { transform: "translateX(-200px)", opacity: 0 },
76 | },
77 | "scale-in-content": {
78 | "0%": { transform: "rotateX(-30deg) scale(0.9)", opacity: 0 },
79 | "100%": { transform: "rotateX(0deg) scale(1)", opacity: 1 },
80 | },
81 | "scale-out-content": {
82 | "0%": { transform: "rotateX(0deg) scale(1)", opacity: 1 },
83 | "100%": { transform: "rotateX(-10deg) scale(0.95)", opacity: 0 },
84 | },
85 | "fade-in": {
86 | "0%": { opacity: 0 },
87 | "100%": { opacity: 1 },
88 | },
89 | "fade-out": {
90 | "0%": { opacity: 1 },
91 | "100%": { opacity: 0 },
92 | },
93 | // Toast
94 | "toast-hide": {
95 | "0%": { opacity: 1 },
96 | "100%": { opacity: 0 },
97 | },
98 | "toast-slide-in-right": {
99 | "0%": { transform: "translateX(calc(100% + 1rem))" },
100 | "100%": { transform: "translateX(0)" },
101 | },
102 | "toast-slide-in-bottom": {
103 | "0%": { transform: "translateY(calc(100% + 1rem))" },
104 | "100%": { transform: "translateY(0)" },
105 | },
106 | "toast-swipe-out-x": {
107 | "0%": { transform: "translateX(var(--radix-toast-swipe-end-x))" },
108 | "100%": {
109 | transform: "translateX(calc(100% + 1rem))",
110 | },
111 | },
112 | "toast-swipe-out-y": {
113 | "0%": { transform: "translateY(var(--radix-toast-swipe-end-y))" },
114 | "100%": {
115 | transform: "translateY(calc(100% + 1rem))",
116 | },
117 | },
118 | },
119 | animation: {
120 | // Dropdown menu
121 | "scale-in": "scale-in 0.2s ease-in-out",
122 | "slide-down": "slide-down 0.6s cubic-bezier(0.16, 1, 0.3, 1)",
123 | "slide-up": "slide-up 0.6s cubic-bezier(0.16, 1, 0.3, 1)",
124 | // Tooltip
125 | "slide-up-fade": "slide-up-fade 0.4s cubic-bezier(0.16, 1, 0.3, 1)",
126 | "slide-right-fade":
127 | "slide-right-fade 0.4s cubic-bezier(0.16, 1, 0.3, 1)",
128 | "slide-down-fade": "slide-down-fade 0.4s cubic-bezier(0.16, 1, 0.3, 1)",
129 | "slide-left-fade": "slide-left-fade 0.4s cubic-bezier(0.16, 1, 0.3, 1)",
130 | // Navigation menu
131 | "enter-from-right": "enter-from-right 0.25s ease",
132 | "enter-from-left": "enter-from-left 0.25s ease",
133 | "exit-to-right": "exit-to-right 0.25s ease",
134 | "exit-to-left": "exit-to-left 0.25s ease",
135 | "scale-in-content": "scale-in-content 0.2s ease",
136 | "scale-out-content": "scale-out-content 0.2s ease",
137 | "fade-in": "fade-in 0.2s ease",
138 | "fade-out": "fade-out 0.2s ease",
139 | // Toast
140 | "toast-hide": "toast-hide 100ms ease-in forwards",
141 | "toast-slide-in-right":
142 | "toast-slide-in-right 150ms cubic-bezier(0.16, 1, 0.3, 1)",
143 | "toast-slide-in-bottom":
144 | "toast-slide-in-bottom 150ms cubic-bezier(0.16, 1, 0.3, 1)",
145 | "toast-swipe-out-x": "toast-swipe-out-x 100ms ease-out forwards",
146 | "toast-swipe-out-y": "toast-swipe-out-y 100ms ease-out forwards",
147 | },
148 | },
149 | },
150 | variants: {
151 | extend: {},
152 | },
153 | plugins: [require("@tailwindcss/forms"), require("tailwindcss-radix")()],
154 | };
155 |
--------------------------------------------------------------------------------
/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "incremental": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve"
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/demo/utils/math.ts:
--------------------------------------------------------------------------------
1 | const getRandomArbitrary = (min: number, max: number) => {
2 | return Math.random() * (max - min) + min;
3 | };
4 |
5 | export { getRandomArbitrary };
6 |
--------------------------------------------------------------------------------
/demo/utils/random.ts:
--------------------------------------------------------------------------------
1 | const getRandomInitials = () => {
2 | return Math.random()
3 | .toString(36)
4 | .replace(/[^a-z]+/g, "")
5 | .substring(0, 2)
6 | .toUpperCase();
7 | };
8 |
9 | export { getRandomInitials };
10 |
--------------------------------------------------------------------------------
/demo/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "github": {
3 | "silent": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tailwindcss-radix",
3 | "version": "4.0.2",
4 | "description": "Utilities and variants for styling Radix state",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "prepare": "husky",
8 | "format": "npx biome format --write",
9 | "test": "vitest --run",
10 | "build": "rimraf dist/ && tsc --removeComments",
11 | "release": "npm run build && release-it"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/ecklf/tailwindcss-radix.git"
16 | },
17 | "keywords": [
18 | "tailwindcss",
19 | "plugin",
20 | "radix",
21 | "react"
22 | ],
23 | "author": {
24 | "email": "ecklf@icloud.com",
25 | "name": "Florentin Eckl",
26 | "url": "https://ecklf.com"
27 | },
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/ecklf/tailwindcss-radix/issues"
31 | },
32 | "homepage": "https://github.com/ecklf/tailwindcss-radix#readme",
33 | "peerDependencies": {
34 | "tailwindcss": "^3.0 || ^4.0"
35 | },
36 | "devDependencies": {
37 | "@biomejs/biome": "1.9.4",
38 | "@types/node": "^22.13.1",
39 | "husky": "^9.1.7",
40 | "postcss": "^8.5.1",
41 | "release-it": "^18.1.2",
42 | "rimraf": "^6.0.1",
43 | "tailwindcss": "^3.4.1",
44 | "typescript": "^5.7.3",
45 | "vitest": "^3.0.5"
46 | },
47 | "release-it": {
48 | "git": {
49 | "commitMessage": "chore: release v${version}",
50 | "tagName": "v${version}",
51 | "requireCleanWorkingDir": false
52 | },
53 | "github": {
54 | "release": true
55 | }
56 | },
57 | "engines": {
58 | "node": ">=20"
59 | },
60 | "packageManager": "pnpm@9.4.0"
61 | }
62 |
--------------------------------------------------------------------------------
/src/__snapshots__/index.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`should generate [width|height] \`content-available\` utilities 1`] = `
4 | ".w-radix-context-menu-content-available {
5 | width: var(--radix-context-menu-content-available-width)
6 | }
7 | .max-w-radix-context-menu-content-available {
8 | max-width: var(--radix-context-menu-content-available-width)
9 | }
10 | .h-radix-context-menu-content-available {
11 | height: var(--radix-context-menu-content-available-height)
12 | }
13 | .max-h-radix-context-menu-content-available {
14 | max-height: var(--radix-context-menu-content-available-height)
15 | }
16 | .w-radix-dropdown-menu-content-available {
17 | width: var(--radix-dropdown-menu-content-available-width)
18 | }
19 | .max-w-radix-dropdown-menu-content-available {
20 | max-width: var(--radix-dropdown-menu-content-available-width)
21 | }
22 | .h-radix-dropdown-menu-content-available {
23 | height: var(--radix-dropdown-menu-content-available-height)
24 | }
25 | .max-h-radix-dropdown-menu-content-available {
26 | max-height: var(--radix-dropdown-menu-content-available-height)
27 | }
28 | .w-radix-hover-card-content-available {
29 | width: var(--radix-hover-card-content-available-width)
30 | }
31 | .max-w-radix-hover-card-content-available {
32 | max-width: var(--radix-hover-card-content-available-width)
33 | }
34 | .h-radix-hover-card-content-available {
35 | height: var(--radix-hover-card-content-available-height)
36 | }
37 | .max-h-radix-hover-card-content-available {
38 | max-height: var(--radix-hover-card-content-available-height)
39 | }
40 | .w-radix-menubar-content-available {
41 | width: var(--radix-menubar-content-available-width)
42 | }
43 | .max-w-radix-menubar-content-available {
44 | max-width: var(--radix-menubar-content-available-width)
45 | }
46 | .h-radix-menubar-content-available {
47 | height: var(--radix-menubar-content-available-height)
48 | }
49 | .max-h-radix-menubar-content-available {
50 | max-height: var(--radix-menubar-content-available-height)
51 | }
52 | .w-radix-popover-content-available {
53 | width: var(--radix-popover-content-available-width)
54 | }
55 | .max-w-radix-popover-content-available {
56 | max-width: var(--radix-popover-content-available-width)
57 | }
58 | .h-radix-popover-content-available {
59 | height: var(--radix-popover-content-available-height)
60 | }
61 | .max-h-radix-popover-content-available {
62 | max-height: var(--radix-popover-content-available-height)
63 | }
64 | .w-radix-select-content-available {
65 | width: var(--radix-select-content-available-width)
66 | }
67 | .max-w-radix-select-content-available {
68 | max-width: var(--radix-select-content-available-width)
69 | }
70 | .h-radix-select-content-available {
71 | height: var(--radix-select-content-available-height)
72 | }
73 | .max-h-radix-select-content-available {
74 | max-height: var(--radix-select-content-available-height)
75 | }
76 | .w-radix-tooltip-content-available {
77 | width: var(--radix-tooltip-content-available-width)
78 | }
79 | .max-w-radix-tooltip-content-available {
80 | max-width: var(--radix-tooltip-content-available-width)
81 | }
82 | .h-radix-tooltip-content-available {
83 | height: var(--radix-tooltip-content-available-height)
84 | }
85 | .max-h-radix-tooltip-content-available {
86 | max-height: var(--radix-tooltip-content-available-height)
87 | }"
88 | `;
89 |
90 | exports[`should generate [width|height] \`trigger\` utilities 1`] = `
91 | ".w-radix-context-menu-trigger {
92 | width: var(--radix-context-menu-trigger-width)
93 | }
94 | .h-radix-context-menu-trigger {
95 | height: var(--radix-context-menu-trigger-height)
96 | }
97 | .w-radix-dropdown-menu-trigger {
98 | width: var(--radix-dropdown-menu-trigger-width)
99 | }
100 | .h-radix-dropdown-menu-trigger {
101 | height: var(--radix-dropdown-menu-trigger-height)
102 | }
103 | .w-radix-hover-card-trigger {
104 | width: var(--radix-hover-card-trigger-width)
105 | }
106 | .h-radix-hover-card-trigger {
107 | height: var(--radix-hover-card-trigger-height)
108 | }
109 | .w-radix-menubar-trigger {
110 | width: var(--radix-menubar-trigger-width)
111 | }
112 | .h-radix-menubar-trigger {
113 | height: var(--radix-menubar-trigger-height)
114 | }
115 | .w-radix-popover-trigger {
116 | width: var(--radix-popover-trigger-width)
117 | }
118 | .h-radix-popover-trigger {
119 | height: var(--radix-popover-trigger-height)
120 | }
121 | .w-radix-select-trigger {
122 | width: var(--radix-select-trigger-width)
123 | }
124 | .h-radix-select-trigger {
125 | height: var(--radix-select-trigger-height)
126 | }
127 | .w-radix-tooltip-trigger {
128 | width: var(--radix-tooltip-trigger-width)
129 | }
130 | .h-radix-tooltip-trigger {
131 | height: var(--radix-tooltip-trigger-height)
132 | }"
133 | `;
134 |
135 | exports[`should generate [width|height] utilities 1`] = `
136 | ".w-radix-accordion-content {
137 | width: var(--radix-accordion-content-width)
138 | }
139 | .h-radix-accordion-content {
140 | height: var(--radix-accordion-content-height)
141 | }
142 | .w-radix-collapsible-content {
143 | width: var(--radix-collapsible-content-width)
144 | }
145 | .h-radix-collapsible-content {
146 | height: var(--radix-collapsible-content-height)
147 | }
148 | .w-radix-navigation-menu-viewport {
149 | width: var(--radix-navigation-menu-viewport-width)
150 | }
151 | .h-radix-navigation-menu-viewport {
152 | height: var(--radix-navigation-menu-viewport-height)
153 | }"
154 | `;
155 |
156 | exports[`should generate \`content-transform-origin\` utilities 1`] = `
157 | ".origin-radix-context-menu {
158 | transform-origin: var(--radix-context-menu-content-transform-origin)
159 | }
160 | .origin-radix-dropdown-menu {
161 | transform-origin: var(--radix-dropdown-menu-content-transform-origin)
162 | }
163 | .origin-radix-hover-card {
164 | transform-origin: var(--radix-hover-card-content-transform-origin)
165 | }
166 | .origin-radix-menubar {
167 | transform-origin: var(--radix-menubar-content-transform-origin)
168 | }
169 | .origin-radix-popover {
170 | transform-origin: var(--radix-popover-content-transform-origin)
171 | }
172 | .origin-radix-select {
173 | transform-origin: var(--radix-select-content-transform-origin)
174 | }
175 | .origin-radix-tooltip {
176 | transform-origin: var(--radix-tooltip-content-transform-origin)
177 | }"
178 | `;
179 |
180 | exports[`should generate tooltip transform utilities 1`] = `
181 | ".translate-x-radix-toast-swipe-end-x {
182 | transform: translateX(var(--radix-toast-swipe-end-x))
183 | }
184 | .translate-y-radix-toast-swipe-end-y {
185 | transform: translateY(var(--radix-toast-swipe-end-y))
186 | }
187 | .translate-x-radix-toast-swipe-move-x {
188 | transform: translateX(var(--radix-toast-swipe-move-x))
189 | }
190 | .translate-y-radix-toast-swipe-move-y {
191 | transform: translateY(var(--radix-toast-swipe-move-y))
192 | }"
193 | `;
194 |
195 | exports[`should generate value data attribute variants 1`] = `
196 | ".radix-state-open\\:opacity-50[data-state="open"] {
197 | opacity: 0.5
198 | }"
199 | `;
200 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import postcss from "postcss";
3 | import tailwind, { type Config } from "tailwindcss";
4 | import { expect, it } from "vitest";
5 | import radix from "./index";
6 |
7 | const html = String.raw;
8 |
9 | function run(input: string, config: Config, plugin = tailwind) {
10 | const { currentTestName } = expect.getState();
11 |
12 | return postcss(plugin(config)).process(input, {
13 | from: `${path.resolve(__filename)}?test=${currentTestName}`,
14 | });
15 | }
16 |
17 | it("should generate boolean data attribute variants", async () => {
18 | const config = {
19 | content: [
20 | {
21 | raw: html`
22 |
23 |
26 |
29 |
30 |
31 |
32 |
33 | `,
34 | },
35 | ],
36 | plugins: [radix],
37 | };
38 |
39 | return run("@tailwind utilities", config).then((result) => {
40 | expect(result.css).toMatchInlineSnapshot(`
41 | ".radix-disabled\\:opacity-50[data-disabled] {
42 | opacity: 0.5
43 | }"
44 | `);
45 | });
46 | });
47 |
48 | it("should generate value data attribute variants", async () => {
49 | const config = {
50 | content: [
51 | {
52 | raw: html`
53 |
54 |
57 |
60 |
61 |
62 |
63 |
64 | `,
65 | },
66 | ],
67 | plugins: [radix],
68 | };
69 |
70 | return run("@tailwind utilities", config).then((result) => {
71 | expect(result.css).toMatchSnapshot();
72 | });
73 | });
74 |
75 | it("should generate [width|height] utilities", async () => {
76 | const config = {
77 | content: [
78 | {
79 | raw: html`
80 |
81 |
82 |
83 |
84 |
85 |
86 | `,
87 | },
88 | ],
89 | plugins: [radix],
90 | };
91 |
92 | return run("@tailwind utilities", config).then((result) => {
93 | expect(result.css).toMatchSnapshot();
94 | });
95 | });
96 |
97 | it("should generate [width|height] `content-available` utilities", async () => {
98 | const config = {
99 | content: [
100 | {
101 | raw: html`
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | `,
137 | },
138 | ],
139 | plugins: [radix],
140 | };
141 |
142 | return run("@tailwind utilities", config).then((result) => {
143 | expect(result.css).toMatchSnapshot();
144 | });
145 | });
146 |
147 | it("should generate [width|height] `trigger` utilities", async () => {
148 | const config = {
149 | content: [
150 | {
151 | raw: html`
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | `,
173 | },
174 | ],
175 | plugins: [radix],
176 | };
177 |
178 | return run("@tailwind utilities", config).then((result) => {
179 | expect(result.css).toMatchSnapshot();
180 | });
181 | });
182 |
183 | it("should generate `content-transform-origin` utilities", async () => {
184 | const config = {
185 | content: [
186 | {
187 | raw: html`
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | `,
196 | },
197 | ],
198 | plugins: [radix],
199 | };
200 |
201 | return run("@tailwind utilities", config).then((result) => {
202 | expect(result.css).toMatchSnapshot();
203 | });
204 | });
205 |
206 | it("should generate tooltip transform utilities", async () => {
207 | const config = {
208 | content: [
209 | {
210 | raw: html`
211 |
212 |
213 |
214 |
215 | `,
216 | },
217 | ],
218 | plugins: [radix],
219 | };
220 |
221 | return run("@tailwind utilities", config).then((result) => {
222 | expect(result.css).toMatchSnapshot();
223 | });
224 | });
225 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import plugin from "tailwindcss/plugin";
2 |
3 | interface Options {
4 | variantPrefix?: string;
5 | }
6 |
7 | export = plugin.withOptions(
8 | (options) =>
9 | ({ addUtilities, matchVariant }) => {
10 | const { variantPrefix } = {
11 | variantPrefix: "radix",
12 | ...options,
13 | };
14 |
15 | // Adds variants for boolean data attributes
16 | const booleanAttributes = {
17 | disabled: "disabled",
18 | highlighted: "highlighted",
19 | placeholder: "placeholder",
20 | } as const;
21 |
22 | matchVariant(
23 | variantPrefix,
24 | (value) => {
25 | return `&[data-${value}]`;
26 | },
27 | {
28 | values: booleanAttributes,
29 | }
30 | );
31 |
32 | // Adds variants for value data attributes
33 | for (const [attributeName, attributeValues] of Object.entries({
34 | align: ["center", "end", "start"],
35 | state: [
36 | "active",
37 | "checked",
38 | "closed",
39 | "delayed-open",
40 | "hidden",
41 | "inactive",
42 | "indeterminate",
43 | "instant-open",
44 | "off",
45 | "on",
46 | "open",
47 | "unchecked",
48 | "visible",
49 | ],
50 | side: ["bottom", "left", "right", "top"],
51 | orientation: ["horizontal", "vertical"],
52 | motion: ["from-end", "from-start", "to-end", "to-start"],
53 | swipe: ["cancel", "end", "move", "start"],
54 | "swipe-direction": ["down", "left", "right", "up"],
55 | } as const)) {
56 | const values = Object.fromEntries(
57 | attributeValues.map((item) => [item, item])
58 | );
59 |
60 | matchVariant(
61 | `${variantPrefix}-${attributeName}`,
62 | (value) => {
63 | return `&[data-${attributeName}="${value}"]`;
64 | },
65 | {
66 | values,
67 | }
68 | );
69 | }
70 |
71 | // Adds the following [width|height] utilities
72 | // `--radix-accordion-content-[width|height]`,
73 | // `--radix-collapsible-content-[width|height]`,
74 | // `--radix-navigation-menu-viewport-[width|height]`,
75 | (
76 | [
77 | "accordion-content",
78 | "collapsible-content",
79 | "navigation-menu-viewport",
80 | ] as const
81 | ).forEach((kind) => {
82 | addUtilities({
83 | [`.w-${variantPrefix}-${kind}`]: {
84 | width: `var(--radix-${kind}-width)`,
85 | },
86 | });
87 | addUtilities({
88 | [`.h-${variantPrefix}-${kind}`]: {
89 | height: `var(--radix-${kind}-height)`,
90 | },
91 | });
92 | });
93 |
94 | // Adds the following [width|height] utilities
95 | // `--radix-context-menu-content-available-[width|height]`,
96 | // `--radix-context-menu-trigger-[width|height]`,
97 | // `--radix-dropdown-menu-content-available-[width|height]`,
98 | // `--radix-dropdown-menu-trigger-[width|height]`,
99 | // `--radix-hover-card-content-available-[width|height]`,
100 | // `--radix-hover-card-trigger-[width|height]`,
101 | // `--radix-menubar-content-available-[width|height]`,
102 | // `--radix-menubar-trigger-[width|height]`,
103 | // `--radix-popover-content-available-[width|height]`,
104 | // `--radix-popover-trigger-[width|height]`,
105 | // `--radix-select-content-available-[width|height]`,
106 | // `--radix-select-trigger-[width|height]`,
107 | // `--radix-tooltip-content-available-[width|height]`,
108 | // `--radix-tooltip-trigger-[width|height]`,
109 | (
110 | [
111 | "context-menu",
112 | "dropdown-menu",
113 | "hover-card",
114 | "menubar",
115 | "popover",
116 | "select",
117 | "tooltip",
118 | ] as const
119 | ).forEach((component) => {
120 | addUtilities({
121 | [`.w-${variantPrefix}-${component}-content-available`]: {
122 | width: `var(--radix-${component}-content-available-width)`,
123 | },
124 | });
125 | addUtilities({
126 | [`.max-w-${variantPrefix}-${component}-content-available`]: {
127 | maxWidth: `var(--radix-${component}-content-available-width)`,
128 | },
129 | });
130 | addUtilities({
131 | [`.h-${variantPrefix}-${component}-content-available`]: {
132 | height: `var(--radix-${component}-content-available-height)`,
133 | },
134 | });
135 | addUtilities({
136 | [`.max-h-${variantPrefix}-${component}-content-available`]: {
137 | maxHeight: `var(--radix-${component}-content-available-height)`,
138 | },
139 | });
140 | addUtilities({
141 | [`.w-${variantPrefix}-${component}-trigger`]: {
142 | width: `var(--radix-${component}-trigger-width)`,
143 | },
144 | });
145 | addUtilities({
146 | [`.h-${variantPrefix}-${component}-trigger`]: {
147 | height: `var(--radix-${component}-trigger-height)`,
148 | },
149 | });
150 | });
151 |
152 | // Adds the following content-transform-origin utilities
153 | // `--radix-context-menu-content-transform-origin`,
154 | // `--radix-dropdown-menu-content-transform-origin`,
155 | // `--radix-hover-card-content-transform-origin `,
156 | // `--radix-menubar-content-transform-origin`
157 | // `--radix-popover-content-transform-origin`,
158 | // `--radix-select-content-transform-origin`,
159 | // `--radix-tooltip-content-transform-origin`
160 | (
161 | [
162 | "context-menu",
163 | "dropdown-menu",
164 | "hover-card",
165 | "menubar",
166 | "popover",
167 | "select",
168 | "tooltip",
169 | ] as const
170 | ).forEach((component) => {
171 | addUtilities({
172 | [`.origin-${variantPrefix}-${component}`]: {
173 | "transform-origin": `var(--radix-${component}-content-transform-origin)`,
174 | },
175 | });
176 | });
177 |
178 | // Adds the following [x|y] utilities
179 | // `--radix-toast-swipe-end-[x|y]`,
180 | // `--radix-toast-swipe-move-[x|y]`,
181 | (["toast-swipe-end", "toast-swipe-move"] as const).forEach((swipe) => {
182 | addUtilities({
183 | [`.translate-x-${variantPrefix}-${swipe}-x`]: {
184 | transform: `translateX(var(--radix-${swipe}-x))`,
185 | },
186 | });
187 | addUtilities({
188 | [`.translate-y-${variantPrefix}-${swipe}-y`]: {
189 | transform: `translateY(var(--radix-${swipe}-y))`,
190 | },
191 | });
192 | });
193 | }
194 | );
195 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 | /* Projects */
5 | // "incremental": true, /* Enable incremental compilation */
6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
11 | /* Language and Environment */
12 | "target": "es6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
14 | // "jsx": "preserve", /* Specify what JSX code is generated. */
15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
20 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
23 | /* Modules */
24 | "module": "commonjs" /* Specify what module code is generated. */,
25 | // "rootDir": "./", /* Specify the root folder within your source files. */
26 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
27 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
28 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
29 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
30 | "typeRoots": [
31 | "./types"
32 | ] /* Specify multiple folders that act like `./node_modules/@types`. */,
33 | "types": [] /* Specify type package names to be included without being referenced in a source file. */,
34 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
35 | // "resolveJsonModule": true, /* Enable importing .json files */
36 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
37 | /* JavaScript Support */
38 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
39 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
40 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
41 | /* Emit */
42 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
43 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
44 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
45 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
46 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
47 | "outDir": "dist" /* Specify an output folder for all emitted files. */,
48 | // "removeComments": true, /* Disable emitting comments. */
49 | // "noEmit": true, /* Disable emitting files from a compilation. */
50 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
51 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
52 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
53 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
55 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
56 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
57 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
58 | // "newLine": "crlf", /* Set the newline character for emitting files. */
59 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
60 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
61 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
62 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
63 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
64 | /* Interop Constraints */
65 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
66 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
67 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
68 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
69 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
70 | /* Type Checking */
71 | "strict": true /* Enable all strict type-checking options. */,
72 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
73 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
74 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
75 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
76 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
77 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
78 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
79 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
80 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
81 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
82 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
83 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
84 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
85 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
86 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
87 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
88 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
89 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
90 | /* Completeness */
91 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
92 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
93 | },
94 | "include": ["./src/**/*.ts"],
95 | "exclude": ["node_modules", "*.test.ts"]
96 | }
97 |
--------------------------------------------------------------------------------