Compare commits

..

208 Commits

Author SHA1 Message Date
github-actions[bot]
d28e02be1b chore(release): version packages (#8998)
* chore(release): version packages

* chore(release): version packages

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-12-09 21:25:59 +04:00
shadcn
6699158a22 fix: handling of base style for add command (#8997)
* fix: handling of base style for add command

* chore: changeset

* fix: shadow config
2025-12-09 20:55:58 +04:00
Pasquale Vitiello
142cd8ef13 Prevent duplicate keyframes when adding components (#8993)
* fix: prevent duplicate keyframes when adding components

- Check for existing keyframes in @theme inline before adding
- Replace existing keyframes instead of creating duplicates
- Add test to verify keyframe replacement behavior

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-12-09 13:01:34 +04:00
shadcn
bdedce2750 feat: add start-app (#8992) 2025-12-09 01:54:22 +04:00
shadcn
4cb283d68e fix 2025-12-09 00:43:17 +04:00
shadcn
480a6cdb37 fix: layout 2025-12-09 00:40:19 +04:00
shadcn
8ba883738e chore: update monorepo-next 2025-12-09 00:31:33 +04:00
shadcn
b022c24825 chore: remove pnpm lock file 2025-12-09 00:27:17 +04:00
shadcn
3587477865 feat: add vite-app template (#8989) 2025-12-08 23:03:13 +04:00
Tommy Lundy
05143a80e6 Add @doras-ui registry (#8966)
* Add @doras-ui registry URL to registries.json

* Add @doras-ui component block details to directory
2025-12-08 23:01:20 +04:00
Jarrod Watts
728d2003b7 Add new component registry entries for @abstract (#8962) 2025-12-08 23:00:58 +04:00
Jarrod Watts
12c9e6b0b5 Add @abstract registry URL to registries.json (#8536)
Added new registry entry for @abstract.

Co-authored-by: shadcn <m@shadcn.com>
2025-12-05 15:39:49 +04:00
I Plan Websites
56cd757c45 @ai-blocks registry (#8956)
* Add AI Blocks registry URL to registries.json

* Add AI components for web to directory.json
2025-12-05 15:37:53 +04:00
Ella
9eb784054f feat: add @lucide-animated to directory.json and registries.json (#8937) 2025-12-05 15:37:41 +04:00
Hin
824577692b feat: add tour to registries and directory (#8917)
Co-authored-by: shadcn <m@shadcn.com>
2025-12-05 15:37:21 +04:00
Antonio Brandao
6be68df08c Add @abui registry to directory and index (#8908) 2025-12-05 15:35:50 +04:00
Admin Mart
cc48808a0d feat: add my custom registry (#8879)
Co-authored-by: wrappixelTeam <wrappixelteam.2016@gmail.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-12-05 15:35:26 +04:00
Admin Mart
a56b3720d1 feat/added my custom registry (#8904)
Co-authored-by: wrappixelTeam <wrappixelteam.2016@gmail.com>
2025-12-05 15:34:33 +04:00
dependabot[bot]
334db11234 chore(deps): bump next from 16.0.0 to 16.0.7 (#8954)
Bumps [next](https://github.com/vercel/next.js) from 16.0.0 to 16.0.7.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.0.0...v16.0.7)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.0.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 16:24:46 +04:00
Luis Llanes
8a5027a0cd feat: add @shadcraft to directory.json and registries.json (#8913) 2025-12-01 00:44:28 +04:00
github-actions[bot]
803206305d chore(release): version packages (#8665)
* chore(release): version packages

* chore(release): version packages

* chore: lock

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-11-27 21:16:23 +04:00
shadcn
d0fb73ac0e fix: do not install baseStyle when adding registry:theme (#8900) 2025-11-27 21:13:56 +04:00
shadcn
62218c1c0c feat: update color value detection for cssVars (#8901) 2025-11-27 21:12:31 +04:00
Aditya Mathur
dd1563d57d fix: update author links in documentation for Drawer and Sonner components (#8881) 2025-11-27 13:38:26 +04:00
Amartya Singh
0538384860 Add @nexus-elements registry URL to registries.json and directory.json (#8797)
* Add @nexus-elements registry URL to registries.json

* feat: updated directory.json with nexus-elements

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-25 11:08:47 +04:00
shadcn
d43b437abc fix 2025-11-25 10:59:48 +04:00
Burhanuddin S. Tinwala
8fbfacd243 docs: fix typo 'mcpServers' to 'servers' in mcp server setup documentation for vs code (#8864) 2025-11-24 12:51:06 +04:00
Neha Prasad
778cee31ee feat: add @ui-layouts registry to directory (#8878) 2025-11-24 12:49:24 +04:00
Wolfr
73d8b8a817 docs - Move free kits to the top (#8639)
* docs - Move free kits to the top

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-24 12:48:21 +04:00
Phuc Bui
55ab069aca feat: add @phucbm in trusted registries (#8830)
* feat: add @phucbm in trusted registries

* Add @phucbm to registry directory

* fix: change svg quotes from double to single
2025-11-19 16:58:43 +04:00
Ajay Patel
c39925a9be Added shadcn/studio UI Kit to figma docs (#8852)
docs: Added shadcn/studio UI Kit to Figma paid section
2025-11-19 16:48:09 +04:00
shadcn
51179ccd64 fix: directory 2025-11-19 12:26:52 +04:00
Hin
dcfa05e392 feat: add shadcn-map to directory (#8832) 2025-11-19 12:20:20 +04:00
Aryan
541f55df04 feat: add @gaia registry entry to directory and registries.json (#8836)
Co-authored-by: shadcn <m@shadcn.com>
2025-11-19 12:19:53 +04:00
Edu Calvo
69010e0230 feat: add new registry entry for @smoothui with logo and description (#8837) 2025-11-19 12:18:35 +04:00
Rushil
a8025c866e feat: add moleculeui to registries and directories (#8838)
Co-authored-by: shadcn <m@shadcn.com>
2025-11-19 12:18:14 +04:00
Kaiyu Hsu
6e34ec7280 feat: add @uicapsule to registry (#8848)
Co-authored-by: shadcn <m@shadcn.com>
2025-11-19 12:16:42 +04:00
Arif Hossain
10ccb244a1 feat: add @commercn in registry directory (#8842) 2025-11-19 12:15:53 +04:00
Akshay Joshi
16fdb07ccc feat: added @crenspire/glass-ui in trusted registries and directory (#8831)
* feat: added @glass-ui in trusted registries and directory

* fix: svg logo not displayed properly
2025-11-19 12:14:41 +04:00
Moumen Soliman
49da1fae79 Add @uitripled registry (#8834)
* Add @uitripled registry

* Update registries.json

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-19 12:13:09 +04:00
Sepehr Soheili
a2244d42f7 docs: fix typo in documentation (#8793) 2025-11-17 20:06:32 +04:00
Brendan Dash
c2075e2a8b fix: typo (#8800) 2025-11-17 20:06:04 +04:00
Anthony Shew
dd2d8d7ead fix: dependencies for Bun in monorepo-next (#8791) 2025-11-13 09:42:31 +04:00
Akash Moradiya
b6a93b7ec6 feat: add @shadcnui-blocks to registry directory (#8763) 2025-11-12 09:39:21 +04:00
shadcn
4899d3f0da fix: minor directory updates 2025-11-10 15:35:21 +04:00
Pawan Kumar
3d04cb099a Add new registry entry for @taki (#8758)
* add taki ui in registry

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-10 15:27:56 +04:00
François Best
cde343916c ref: refactor nuqs usage in directory search (#8756)
* chore: fix typo

* ref: query is non-nullable (as it has a default value)

* ref: leverage nuqs' clearOnDefault behaviour
2025-11-10 15:27:06 +04:00
LN
c877df07b8 feat: add @square-ui in registries (#8761)
* feat: add new registry entry for @square-ui

* feat: add new registry entry for @square-ui with logo and description
2025-11-10 15:17:01 +04:00
DimaDevelopment
65e5c1c3cf Add search input for Directory list (#8673)
* feat(directory): Added directory search input

* feat(directory): Added nuqs for a search state management. Refactor searchFn - includes the description in the search criteria

* fear(directory): Added default query value. Added useQueryState limitUrlUpdates 250ms

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-10 12:24:15 +04:00
Anvarys
8a7f05f670 fix: correct typos in docs/components/chart (#8750) 2025-11-10 11:34:53 +04:00
Miracle Onyenma
db004ce4c0 Add new registry entry for @aevr (#8726)
* Add new registry entry for @aevr

Would like to add Aevr UI, a small collection of focused, production‑ready components and primitives for React/Next.js projects—built on shadcn/ui and complementary libraries—to the shadcn registry. It emphasizes practical, purpose‑specific building blocks with consistent design tokens, accessibility, and copy‑paste examples tailored for real product use.

Website: https://ui.aevr.space 

Repo: https://github.com/aevrhq/ui

* Add new component library entry for @aevr in registry directory

* Update logo SVG format in directory.json

* fix: logo color

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-10 11:30:16 +04:00
shadcn
e23698a897 feat: add tracking for directories 2025-11-10 11:27:10 +04:00
preet
5813ef20a3 Add new registry entry for @hextaui (#8715)
* Add new registry entry for @hextaui

* feat: add new registry entry for @HextaUI

* Update directory.json

Co-authored-by: shadcn <m@shadcn.com>

* fix: update directory

* fix: logo

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-10 11:20:23 +04:00
Arif Hossain
515024b69e feat: add @commercn in trusted registries (#8751) 2025-11-10 11:12:35 +04:00
Shaban
f7284c5cc3 feat(directory): update the @efferd logo (#8733) 2025-11-10 10:43:54 +04:00
Harshul
c02d00aafc fix(shadcn): Restore two-finger navigation on macOS by adjusting overscroll behavior (#8714)
* fix(shadcn): Tow finger navigation on macOS

* Add smooth scrolling to html element
2025-11-06 10:04:15 +04:00
Bassim Shahidy
df497ad236 feat: add assistant-ui to directory.json (#8720)
* feat: add assistant-ui to directory.json

* update description

* desc
2025-11-06 10:01:33 +04:00
shadcn
1e468e33ac feat: add more registries 2025-11-05 10:15:32 +04:00
dependabot[bot]
ff91c31a71 chore(deps): bump @radix-ui/react-toggle from 1.1.9 to 1.1.10 (#8702)
Bumps [@radix-ui/react-toggle](https://github.com/radix-ui/primitives) from 1.1.9 to 1.1.10.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-toggle"
  dependency-version: 1.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-05 09:51:45 +04:00
dependabot[bot]
25d6a18f6f chore(deps): bump @radix-ui/react-tooltip from 1.2.7 to 1.2.8 (#8701)
Bumps [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives) from 1.2.7 to 1.2.8.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-05 09:51:11 +04:00
dependabot[bot]
c0309510b6 chore(deps): bump @radix-ui/react-accordion from 1.2.11 to 1.2.12 (#8700)
Bumps [@radix-ui/react-accordion](https://github.com/radix-ui/primitives) from 1.2.11 to 1.2.12.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-accordion"
  dependency-version: 1.2.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-05 09:50:52 +04:00
dependabot[bot]
a3a1574668 chore(deps): bump @commitlint/cli from 17.8.1 to 20.1.0 (#8698)
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 17.8.1 to 20.1.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.1.0/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-05 09:50:29 +04:00
shadcn
65d581ea5a chore: remove console 2025-11-04 15:19:45 +04:00
shadcn
fdf80a1d49 fix: gh link 2025-11-04 09:21:43 +04:00
Ajay Patel
86c494c452 feat: add new entry for @shadcn-studio in directory.json (#8692)
* feat: add new entry for @shadcn-studio in directory.json

* [fix]: for @shadcn-studio in directory.json, Updated logo SVG to use CSS variables

Updated logo SVG to use CSS variables for colors.Refactor logo SVG to use CSS variables
2025-11-03 12:48:36 +04:00
Paul Burke
eb158686b9 feat: adds lens-blocks to the registry index (#8687) 2025-11-03 11:45:57 +04:00
shadcn
134cd46edb feat: add hsl and color indicators to theme (#8691) 2025-11-03 11:25:06 +04:00
shadcn
47b0efb20c docs: update tanstack start 2025-11-03 10:46:35 +04:00
shadcn
bd4d09d33e fix 2025-11-03 10:30:18 +04:00
Dylan
14d6265580 docs: fix typos in Empty (#8675) 2025-11-03 08:57:19 +04:00
Ali Imam
68805d29a1 Update directory.json (#8682) 2025-11-03 08:56:03 +04:00
ocavue
c100d5841a feat: add prosekit to directory (#8677) 2025-11-02 15:00:40 +04:00
Ali Imam
7a71da5218 Update directory.json (#8671)
* Update directory.json

* Update registries.json
2025-11-02 14:51:58 +04:00
Mudunuri bhaskara karthikeya varma
e18902039a adding @eldoraui to directory.json (#8670) 2025-10-31 21:54:20 +04:00
Emmanuel Odii
559af6c245 feat(registry): add paykit to directory (#8654)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-31 12:24:15 +04:00
Alex Paduraru
8971be484f feat: add @creative-tim to registries.json, directory.json (#8651)
* feat: add @creative-tim to registries.json, directory.json

* fix: logo color

---------

Co-authored-by: Alexandru Paduraru <axelut@yahoo.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-31 12:19:31 +04:00
Tommy D. Rossi
ad6a3c6367 Fix utils import transform when workspace alias does not start with @ (#7557)
* Fix nested src folder for utils import

* remove .only

* Update packages/shadcn/src/utils/transformers/transform-import.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* check for empty utils

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-31 12:07:02 +04:00
dependabot[bot]
befa56b5be chore(deps): bump @faker-js/faker from 8.4.1 to 10.1.0 (#8520)
Bumps [@faker-js/faker](https://github.com/faker-js/faker) from 8.4.1 to 10.1.0.
- [Release notes](https://github.com/faker-js/faker/releases)
- [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md)
- [Commits](https://github.com/faker-js/faker/compare/v8.4.1...v10.1.0)

---
updated-dependencies:
- dependency-name: "@faker-js/faker"
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-31 11:15:26 +04:00
dependabot[bot]
5d1770e36d chore(deps): bump @radix-ui/react-navigation-menu from 1.2.13 to 1.2.14 (#8519)
Bumps [@radix-ui/react-navigation-menu](https://github.com/radix-ui/primitives) from 1.2.13 to 1.2.14.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-navigation-menu"
  dependency-version: 1.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-31 11:14:27 +04:00
dependabot[bot]
653521725a chore(deps): bump jotai from 2.13.1 to 2.15.0 (#8521)
Bumps [jotai](https://github.com/pmndrs/jotai) from 2.13.1 to 2.15.0.
- [Release notes](https://github.com/pmndrs/jotai/releases)
- [Commits](https://github.com/pmndrs/jotai/compare/v2.13.1...v2.15.0)

---
updated-dependencies:
- dependency-name: jotai
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-31 11:14:12 +04:00
shadcn
7c0618bf43 feat: add new registries (#8661) 2025-10-31 10:39:29 +04:00
shadcn
854641cea1 fix 2025-10-30 12:51:15 +04:00
shadcn
3a72007f61 fix 2025-10-30 12:04:55 +04:00
shadcn
6b53b238fb fix: spacing 2025-10-30 11:59:50 +04:00
shadcn
b398fea304 feat: add dir action (#8647)
* feat: add mcp config

* feat

* fix
2025-10-30 11:53:24 +04:00
shadcn
f22174a77f feat: add mcp config (#8641) 2025-10-29 22:56:24 +04:00
shadcn
c9a39f1007 docs: update README 2025-10-29 21:38:18 +04:00
shadcn
a8ad21f81f fix: svgs 2025-10-29 21:37:15 +04:00
shadcn
504503c638 feat: add new registries 2025-10-29 21:20:04 +04:00
Pasquale Vitiello
f8df5c95cb docs: update coss logo (#8631) 2025-10-29 20:57:42 +04:00
shadcn
2bfc1c82ba chore: deprecate www (#8629)
* chore: deprecate www

* chore: updates

* fix
2025-10-29 20:50:55 +04:00
shadcn
84bd724d97 feat: refactor registry (#8598)
* feat: refactor registry

* fix: remove components

* refactor: getActiveStyle

* fix: prettier in build-registry

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix

* Update apps/v4/scripts/build-registry.mts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix

* Update apps/v4/scripts/build-registry.mts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update apps/v4/components/block-viewer.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-29 15:07:56 +04:00
shadcn
39fdf94550 feat: add svgl 2025-10-29 14:58:25 +04:00
shadcn
08479cc3db feat: add wigggle-ui 2025-10-29 13:55:50 +04:00
shadcn
02d5ce85ec feat: upgrade to Next.js 16 (#8615)
* feat: upgrade to Next.js 16

* chore: deps

* fix

* fix

* fix

* fix: workaround zod 4 for now

* fix

* fix: copy button

* fix: update apps/v4/hooks/use-is-mac.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix

* fix: remove

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-29 13:37:41 +04:00
shadcn
c0329c86b9 feat: add retroui and reui 2025-10-29 09:53:25 +04:00
shadcn
3b1491f908 fix: typo 2025-10-29 09:30:49 +04:00
shadcn
ca4c1c43ec feat: update registry directory 2025-10-29 00:15:20 +04:00
shadcn
1e840eb53c feat: update directory 2025-10-29 00:05:03 +04:00
shadcn
96ac92e63f fix 2025-10-29 00:00:20 +04:00
shadcn
e11546e692 fix: examples page 2025-10-28 23:56:15 +04:00
shadcn
0b4d62f95c chore: rebuild registry 2025-10-28 22:59:12 +04:00
shadcn
dae80dad65 fix: password field 2025-10-28 22:58:17 +04:00
shadcn
abc09809e8 feat: add blocks to directory 2025-10-28 22:56:29 +04:00
shadcn
8a40fe0ead feat: update registries 2025-10-28 22:45:06 +04:00
shadcn
b3ab304a00 fix: minor styles updates 2025-10-28 21:34:31 +04:00
Elliot Sutton
bb45fd83c3 fix(v4): correction of the Animate UI logo (#8608)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-28 21:03:19 +04:00
Chisom Uma
84678ee1c0 Add new registry entry for @chisom-ui (#8601) 2025-10-28 21:01:08 +04:00
shadcn
33ffb0419c Merge branch 'main' of github.com:shadcn-ui/ui
# Conflicts:
#	apps/v4/registry/directory.json
2025-10-28 20:52:28 +04:00
shadcn
a2f6c031e2 feat: add new registries 2025-10-28 20:49:49 +04:00
shadcn
ac098d8cf0 feat: add new registries (#8600) 2025-10-28 18:28:06 +04:00
shadcn
8160610410 Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-28 11:26:17 +04:00
shadcn
c7901e3a41 chore: update validate-registries script 2025-10-28 11:26:04 +04:00
shadcn
d73ac361b3 feat: add registry directory (#8574)
* feat: add registry directory

* fix: lint

* feat: add more registries

* feat: add nuqs to directory

* feat: add shadcndesign

* feat: add more registries

* feat: add new registries

* chore: remove hooks
2025-10-28 11:25:25 +04:00
François Best
ebad2901ce feat: add nuqs to registries.json (#8579) 2025-10-27 11:16:18 +04:00
shadcn
4f617d59b8 Fix description formatting in registry_directory.yml 2025-10-26 16:22:48 +04:00
shadcn
ed0e103bd6 Refactor checklist validations in registry_directory.yml
Removed individual required validations from checklist items and added a single required validation for the entire checklist.
2025-10-26 16:22:17 +04:00
shadcn
9cab0c9b18 chore: add registry directory issue template 2025-10-26 16:18:46 +04:00
DimaDevelopment
d80e084814 feat: add @wandry-ui to registries.json (#8566) 2025-10-26 15:28:42 +04:00
shadcn
efcf9728c2 feat: add docs for cell size 2025-10-26 14:53:50 +04:00
Lakshya Thakur
8835bacc8b fix: use calendar-05 blocks instead of calendar-02 for range (#8029) 2025-10-26 14:47:06 +04:00
Ian Thorslund
f2556d2386 fix(components): Fix left radius of days when weeks are shown in range calendar (#8570)
* fix(components): Fix left radius of days when weeks are shown in range calendar

* feat: update block to show range

* chore: rebuild registry

* docs: add changelog

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-26 14:44:24 +04:00
albertasaftei
75a0000075 Add Pixelact UI registry to registries.json (#8301) 2025-10-25 09:40:38 +04:00
Dillion Verma
ac306c60f5 feat: add magic ui pro registry (#8302)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-24 11:19:25 +04:00
shadcn
5e2ef1f8bd docs: updates 2025-10-24 10:56:23 +04:00
shadcn
7d9b8aefff Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-24 10:44:39 +04:00
shadcn
58208e3802 fix: minor updates 2025-10-24 10:44:21 +04:00
Shaban
a16a77446a feat: rename @efferd-ui -> @efferd (#8557) 2025-10-24 10:37:47 +04:00
github-actions[bot]
39032bb390 chore(release): version packages (#8551)
* chore(release): version packages

* chore(release): version packages

* chore: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-23 22:12:05 +04:00
shadcn
d7e0dc3ec8 feat(shadcn): middleware to proxy (#8555)
* feat: implement getFrameworkVersion

* feat(shadcn): add transformNext transformer

* feat(shadcn): rename

* chore: update

* chore: changeset

* fix

* fix: small refactor
2025-10-23 22:00:55 +04:00
shadcn
6bddba986d feat(shadcn): add next 16 to init (#8550)
* feat: add next 16 init

* chore: changeset

* fix: tests

* fix
2025-10-23 17:20:45 +04:00
shadcn
b70059b25b docs: add react-19 docs back 2025-10-23 17:14:09 +04:00
shadcn
37bc2eec1f feat: update code block for component preview (#8549) 2025-10-23 16:24:44 +04:00
mw10013
bb048fb532 feat: add @oui to registries.json (#8541) 2025-10-22 21:35:55 +04:00
Alejandro Wurts
9c373dbd27 feat: add @simple-ai to trusted registries (#8539) 2025-10-22 21:35:03 +04:00
shadcn
d75b092c61 docs: remove legacy docs 2025-10-22 07:02:23 +04:00
Gihan Rangana
be49662bf5 shadix-ui registry added (#8505) 2025-10-21 19:16:23 +04:00
FadilRumasoreng
b2b2e3fc98 docs (security) : add clickable link to security reporting section (#8525)
* docs (security) : add clickable link to security reporting section

* fix(docs): use markdown link format for security section
2025-10-21 17:41:45 +04:00
Neeraj Dalal
188b746074 refactor(field): no duplicate errors (#8514)
* chore: tweaks > field.tsx

apps/v4/registry/new-york-v4/ui/field.tsx

* chore: tweaks > field.tsx

apps/v4/registry/new-york-v4/ui/field.tsx

* chore: tweaks > field.json field.json

apps/www/public/r/styles/new-york-v4/field.json
apps/v4/public/r/styles/new-york-v4/field.json
2025-10-21 16:02:52 +04:00
Joshua Chung
6f093a0f3f Added ha-components registry to registries.json (#8531) 2025-10-21 15:40:03 +04:00
Matt Wierzbicki
f18f1eaff7 Added shadcndesign.com registry to registries.json (#8516)
* Added shadcndesign.com registry to registries.json

* Update registries.json with open source shadcndesign registry
2025-10-21 14:33:40 +04:00
shadcn
9ac1b5c0a5 feat: add native select (#8528)
* feat: add native select

* fix: width
2025-10-21 11:48:25 +04:00
shadcn
f63b70b413 feat: implement search via fumadocs (#8523)
* feat: implement search

* fix: update message when searching
2025-10-21 10:26:29 +04:00
shadcn
54e725d986 feat(toggle-group): add spacing props 2025-10-20 21:28:40 +04:00
shadcn
62dbad36bb fix: theme-customizer 2025-10-20 20:42:55 +04:00
shadcn
a707424fa2 fix: front page blocks 2025-10-20 20:39:01 +04:00
shadcn
e2bfa6bd85 feat(badge): make rounded-full (#8518) 2025-10-20 20:24:45 +04:00
preet
6292464d90 docs(calendar): add timezone fix for date selection offset issue (#8515)
Added a new section to the Calendar documentation explaining how to fix the issue where selecting a date highlights the previous day. The section includes an example showing how to detect the user’s local timezone and pass it as a timeZone prop to the Calendar component to resolve the date offset problem.
2025-10-20 13:17:58 +00:00
shadcn
6617167d6f Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-20 16:05:21 +04:00
shadcn
ca28857d40 fix: themes 2025-10-20 16:05:14 +04:00
Rakesh
343bc941b1 Fix/theme code mismatch (#8368)
* fix/replaced stale theme colors

* fix:missing chart colors added
2025-10-20 15:56:57 +04:00
Pasquale Vitiello
c9311f26fa feat: add @coss to registries.json (#8490) 2025-10-17 15:01:04 +04:00
Ivan Vasilov
4e0871f426 Add supabase registry to trusted registries. (#8161) 2025-10-17 15:00:51 +04:00
shadcn
cb769b7059 fix: navigation menu demo on mobile (#8488) 2025-10-16 14:44:27 +04:00
Dylan Tientcheu
93037dca94 feat: add @algolia in trusted registries (#8485) 2025-10-16 13:05:51 +04:00
github-actions[bot]
ed9d5939e6 chore(release): version packages (#8479)
* chore(release): version packages

* fix

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-15 22:55:13 +04:00
shadcn
b52ec12f1e fix(shadcn): universal items handling (#8478)
* fix(shadcn): universal items handling

* chore: add changeset
2025-10-15 22:27:12 +04:00
Igor S. Zizinio
2ab9bff4bb Fix formatting of Badge variant prop in documentation (#8477) 2025-10-15 22:09:06 +04:00
Shaban Haider
2f6b51fa0a feat: add @efferd-ui in trusted registries (#8474) 2025-10-15 22:03:39 +04:00
shadcn
8a4764ed91 chore: registry build 2025-10-15 12:05:03 +04:00
Chisom Uma
e934d4645b feat(registry): Add Austin UI Components to the registry index (#8456)
* feat(registry): Add Austin UI Components to the registry index

* Fix JSON syntax for @austin-ui registry URL

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-15 12:02:40 +04:00
shadcn
08b8e499d8 chore: update registries.json (#8470) 2025-10-15 11:52:43 +04:00
shadcn
69402b3579 ci: update deprecated to ignore www/package.json 2025-10-15 11:39:53 +04:00
shadcn
679c852254 Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-15 11:37:26 +04:00
github-actions[bot]
d478412e44 chore(release): version packages (#8453)
* chore(release): version packages

* chore(release): version packages

* chore: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-15 11:37:01 +04:00
shadcn
d5c8a25150 Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-15 11:30:53 +04:00
Benjamin
26433a651c Added react-market registry (#8346)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-15 11:30:43 +04:00
shadcn
c3da716e94 debug: ci 2025-10-15 11:30:24 +04:00
dependabot[bot]
b2572d0287 chore(deps): bump tj-actions/changed-files in /.github/workflows (#8466)
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 44 to 46.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v44...v46)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: '46'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 11:30:03 +04:00
shadcn
b83f042416 fix: actions 2025-10-15 11:26:28 +04:00
shadcn
6567897393 fix: actions 2025-10-15 11:07:14 +04:00
shadcn
2675fa3941 ci: add deprecated action (#8465)
* ci: add deprecated action

* ci: add label

* Potential fix for code scanning alert no. 12: Workflow does not contain permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-10-15 11:04:30 +04:00
Shodai Suzuki
fbda67c88c docs: remove isolated remix page from installation guides (#7027) 2025-10-15 10:39:15 +04:00
shadcn
e8674ee848 feat(shadcn): allow path to override targets (#8452) 2025-10-15 10:37:58 +04:00
kuzma031
adb66f4d43 fix(components): use type-only import for VariantProps in sidebar component to support verbatimModuleSyntax (#7437)
* fix sidebar variantprops imports

* chore: registry build

---------

Co-authored-by: Djordje Kuzmanovic <djordje@voiced.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-15 10:36:32 +04:00
shadcn
3afb46eaf6 feat: add llms.txt (#8460) 2025-10-14 23:30:07 +04:00
shadcn
7cd019ad36 feat(shadcn): add support for color vars (#8459)
* feat(shadcn): add support for color vars

* chore: add changeset
2025-10-14 23:07:27 +04:00
Serhat Arslan
bea7d30536 docs: fix next/link import syntax in component examples (#8427)
Fix incorrect named import syntax for Next.js Link component.

Changed from:
  import { Link } from "next/link"

To correct default import:
  import Link from "next/link"

Next.js Link is a default export, not a named export. The incorrect
syntax causes TypeScript error:
'Module "next/link" has no exported member "Link"'

Affected files:
- Button component docs
- Badge component docs
- Breadcrumb component docs (v4 and www)
- Navigation Menu component docs

Fixes issue where users copying code snippets get immediate errors.

Co-authored-by: serhatx1 <armonikadijital@gmail.com>
2025-10-14 22:38:58 +04:00
Ziad Beyens
40c3ff513a fix(registry): handle universal registry items with no files (#8420)
* fix(registry): handle universal registry items with no files

Allow registry items with registryDependencies but no files to be
considered universal registry items. Previously the function required
files.length to be truthy, which excluded valid items with only
registryDependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-14 17:14:05 +04:00
Gildas Garcia
89ebfdce47 feat(registry): Add Shadcn Admin Kit to the registry index (#8442) 2025-10-14 17:12:12 +04:00
Haz
b83023034a fix(cli): fix add registry item with at-property css rule (#8451)
* Add fixture and test

* Handle at-property as regular CSS rules in updateCssPlugin

* Add changeset entry

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-14 17:11:29 +04:00
Ujjwal Sharma
6a534d7954 fix(components): use type import for ToasterProps (#8376)
* fix(components): use type import for ToasterProps

* chore: build registry

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-14 16:40:17 +04:00
Felix
ef1987ded9 docs(Toast): add deprecation Callout (#6982)
* docs(Toast): add deprecation Callout

* docs(Toast): add tw4 and react19 specifics
2025-10-13 22:21:50 +04:00
Jakob Guddas
77bf7d28b4 feat(components): changed sonner defaults to use lucide icons (#7620)
* feat(components): changed sonner defaults to use lucide icons

* Update new-york-v4 sonner.tsx

* fix: icons and docs

* fix

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-13 22:16:50 +04:00
Ayush Shrestha
41f4f7357d refactor: initialize FormFieldContext and FormItemContext with null values rather empty object with type assertion (#4847) 2025-10-13 21:32:43 +04:00
Carlos Pegueros
bc99818e04 docs(www): fix sonner not showing on first render (#6235) 2025-10-13 21:17:32 +04:00
Shahar Ilany
162ba7b13c Small fixes for installation document (#6623)
* Fix TanStack Start icon color

* Fix a11y for Laravel and TanStack Start
2025-10-13 21:14:11 +04:00
Ryann Mack
f12db1e3a2 fix(sonner): support border radius from theme (#7825)
* fix(sonner): support border radius from theme

* chore: rebuild registry

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-13 21:12:00 +04:00
shadcn
ce3e2b1df8 Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-13 12:39:19 +04:00
shadcn
dcfe911b33 docs: button cursor 2025-10-13 12:38:58 +04:00
Jrocam
7210a4919a fix(components): checkbox alignment with grid 🔳 (#4772)
* fix(components): checkbox alignment with grid 🔳

* fix(checkbox): internal checkbox check alignment (default style)

* fix: new-york-v4

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-13 12:29:44 +04:00
Krishna Agarwal
d198908510 fix(field): return null when errors are empty (#8419)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-13 12:22:34 +04:00
shadcn
b0b1cd1f0d feat: add dropdown menu dialog example (#8438)
* feat: add dropdown menu dialog example

* chore: remove dropdown-dialog
2025-10-13 11:57:20 +04:00
shadcn
f3d70724b6 chore: remove new-components-01 (#8439) 2025-10-13 11:55:08 +04:00
Dada Khalandar
407e9c6802 fix: correct link to Form documentation in form.mdx (#8416) 2025-10-11 10:15:07 +04:00
shadcn
c67e630521 feat: add docs and examples for react-hook-form and tanstack form (#8412)
* feat: add next forms docs

* feat

* docs: rhf and tsf

* docs: forms

* feat: update react-hook-form docs

* feat: update docs for both lib

* docs: update tanstack docs

* docs: update

* fix

* fix

* fix

* add forms link in sidebar
2025-10-10 21:29:30 +04:00
Daniel Petho
f494411953 feat(registry): add fancy components to registries (#8397) 2025-10-10 16:09:13 +04:00
Mudunuri bhaskara karthikeya varma
a43c1d1342 feat: add @eldoraui registry URL to registries.json (#8379)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-09 11:06:55 +04:00
Hin
607a6fd127 feat: add shadcn map to the registry (#8375) 2025-10-08 20:15:45 +04:00
Irsyad A. Panjaitan
fbcc665b49 Add @intentui registry URL to registries.json (#8380) 2025-10-08 00:07:59 +04:00
Shivam S.
7ddcf31e43 fix(docs): correct Empty component structure in documentation (#8374) 2025-10-07 20:58:15 +04:00
Louis J.
3e39163b08 feat: add @elevenlabs-ui registry URL to registries.json (#8364) 2025-10-07 13:12:24 +04:00
shadcn
e311fdae04 fix(www): dashboard 2025-10-07 10:09:34 +04:00
shadcn
26640d9d88 fix: code 2025-10-06 16:57:33 +04:00
Seoku
3e20c228da fix(input-group-textarea): prevent child elements from overflowing by… (#8341)
* fix(input-group-textarea): prevent child elements from overflowing by adding min-w-0

* chore: run registry:build

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-06 16:11:01 +04:00
Morgan Feeney
0810c0e1a2 Add @zippystarter registry URL to registries.json (#8354) 2025-10-06 16:00:28 +04:00
Ahdeetai
1205ea5445 Add @scrollxui in trusted registries (#8322)
* Add @scrollxui in trusted registries

* fix: missing comma

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-06 16:00:12 +04:00
Rob Austin
4430ab8bab Add @shadcnblocks registry URL to registries.json (#8344)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-05 20:03:40 +04:00
3636 changed files with 32452 additions and 21238 deletions

View File

@@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["www", "v4", "tests"]
"ignore": ["v4", "tests"]
}

View File

@@ -0,0 +1,63 @@
name: Add registry to directory
description: Add your registry to the directory
title: "[Registry Directory]: "
labels: ["registry", "directory"]
assignees: []
body:
- type: input
id: name
attributes:
label: Name
description: The name of your registry. This is also the namespace.
placeholder: e.g., "@acme"
validations:
required: true
- type: input
id: url
attributes:
label: URL
description: The URL to your registry index. Use {name} placeholder.
placeholder: https://ui.acme.com/r/{name}.json
validations:
required: true
- type: input
id: homepage
attributes:
label: Homepage
description: The URL to your registry homepage. This is where users can browse your registry.
placeholder: https://ui.acme.com
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Briefly describe what is your registry and what type of components or code it distributes.
placeholder:
validations:
required: true
- type: textarea
id: logo
attributes:
label: Logo
description: Add your SVG logo here.
placeholder:
validations:
required: true
- type: checkboxes
id: requirements
attributes:
label: Checklist
description: Verify that your registry meets the following requirements.
options:
- label: The registry must be open source and publicly accessible.
- label: The registry must be a valid JSON file that conforms to the [registry schema](https://ui.shadcn.com/docs/registry/registry-json) specification.
- label: The `files` array, if present on your registry items, must NOT include a `content` property.
- label: I've attached a square SVG logo to this issue
validations:
required: true

78
.github/workflows/deprecated.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
name: Deprecated
on:
pull_request_target:
types: [opened, synchronize]
permissions:
issues: write
contents: read
pull-requests: write
jobs:
deprecated:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
with:
files: |
apps/www/**
files_ignore: |
apps/www/public/r/**
base_sha: ${{ github.event.pull_request.base.sha }}
sha: ${{ github.event.pull_request.head.sha }}
- name: Comment on PR if www files changed
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
const wwwFiles = changedFiles.filter(file =>
file.startsWith('apps/www/') &&
!file.startsWith('apps/www/public/r/') &&
file !== 'apps/www/package.json'
);
if (wwwFiles.length > 0) {
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
Consider applying the change to \`apps/v4\` if relevant.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
// Add deprecated label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['deprecated']
});
} else {
// Remove deprecated label if no www files are changed
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'deprecated'
});
} catch (error) {
// Label doesn't exist, which is fine
console.log('Deprecated label not found, skipping removal');
}
}

View File

@@ -27,10 +27,10 @@ jobs:
with:
version: 9.0.6
- name: Use Node.js 18
- name: Use Node.js 20
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
cache: "pnpm"
- name: Install NPM Dependencies

View File

@@ -23,11 +23,11 @@ jobs:
with:
version: 9.0.6
- name: Use Node.js 18
- name: Use Node.js 20
uses: actions/setup-node@v3
with:
version: 9.0.6
node-version: 18
node-version: 20
cache: "pnpm"
- name: Install NPM Dependencies

View File

@@ -19,7 +19,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
- uses: pnpm/action-setup@v4
name: Install pnpm

View File

@@ -3,5 +3,5 @@ node_modules
.next
build
.contentlayer
apps/www/pages/api/registry.json
**/fixtures
deprecated

View File

@@ -10,6 +10,6 @@
"**/fixtures/**"
],
"files.exclude": {
"apps/www": true
"deprecated": true
}
}

View File

@@ -20,28 +20,25 @@ This repository is structured as follows:
```
apps
└── www
└── v4
├── app
├── components
├── content
└── registry
── default
│ ├── example
│ └── ui
└── new-york
── new-york-v4
├── example
└── ui
packages
└── cli
└── shadcn
```
| Path | Description |
| --------------------- | ---------------------------------------- |
| `apps/www/app` | The Next.js application for the website. |
| `apps/www/components` | The React components for the website. |
| `apps/www/content` | The content for the website. |
| `apps/www/registry` | The registry for the components. |
| `packages/cli` | The `shadcn-ui` package. |
| Path | Description |
| -------------------- | ---------------------------------------- |
| `apps/v4/app` | The Next.js application for the website. |
| `apps/v4/components` | The React components for the website. |
| `apps/v4/content` | The content for the website. |
| `apps/v4/registry` | The registry for the components. |
| `packages/shadcn` | The `shadcn` package. |
## Development
@@ -82,32 +79,26 @@ You can use the `pnpm --filter=[WORKSPACE]` command to start the development pro
1. To run the `ui.shadcn.com` website:
```bash
pnpm --filter=www dev
pnpm --filter=v4 dev
```
2. To run the `shadcn-ui` package:
2. To run the `shadcn` package:
```bash
pnpm --filter=shadcn-ui dev
pnpm --filter=shadcn dev
```
## Running the CLI Locally
To run the CLI locally, you can follow the workflow:
1. Start by running the registry (main site) to make sure the components are up to date:
1. Start by running the dev server:
```bash
pnpm v4:dev
pnpm dev
```
2. Run the development script for the CLI:
```bash
pnpm shadcn:dev
```
3. In another terminal tab, test the CLI by running:
2. In another terminal tab, test the CLI by running:
```bash
pnpm shadcn
@@ -119,36 +110,27 @@ To run the CLI locally, you can follow the workflow:
pnpm shadcn <init | add | ...> -c ~/Desktop/my-app
```
4. To run the tests for the CLI:
```bash
pnpm --filter=shadcn test
```
This workflow ensures that you are running the most recent version of the registry and testing the CLI properly in your local environment.
## Documentation
The documentation for this project is located in the `www` workspace. You can run the documentation locally by running the following command:
The documentation for this project is located in the `v4` workspace. You can run the documentation locally by running the following command:
```bash
pnpm --filter=www dev
pnpm --filter=v4 dev
```
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/www/content/docs` directory.
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/v4/content/docs` directory.
## Components
We use a registry system for developing components. You can find the source code for the components under `apps/www/registry`. The components are organized by styles.
We use a registry system for developing components. You can find the source code for the components under `apps/v4/registry`. The components are organized by styles.
```bash
apps
└── www
└── v4
└── registry
── default
│ ├── example
│ └── ui
└── new-york
── new-york-v4
├── example
└── ui
```
@@ -157,7 +139,7 @@ When adding or modifying components, please ensure that:
1. You make the changes for every style.
2. You update the documentation.
3. You run `pnpm build:registry` to update the registry.
3. You run `pnpm registry:build` to update the registry.
## Commit Convention
@@ -196,9 +178,9 @@ If you have a request for a new component, please open a discussion on GitHub. W
## CLI
The `shadcn-ui` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
The `shadcn` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
Any changes to the CLI should be made in the `packages/cli` directory. If you can, it would be great if you could add tests for your changes.
Any changes to the CLI should be made in the `packages/shadcn` directory. If you can, it would be great if you could add tests for your changes.
## Testing

View File

@@ -1,8 +1,8 @@
# shadcn/ui
Accessible and customizable components that you can copy and paste into your apps. Free. Open Source. **Use this to build your own component library**.
A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code. **Use this to build your own component library**.
![hero](apps/www/public/og.jpg)
![hero](apps/v4/public/opengraph-image.png)
## Documentation

View File

@@ -6,4 +6,4 @@ We will investigate all legitimate reports and do our best to quickly fix the pr
Our preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software.
To do this, please visit the security tab of the repository and click the "Report a vulnerability" button.
To do this, please visit the security tab of the repository and click the [Report a vulnerability](https://github.com/shadcn-ui/ui/security/advisories/new) button.

View File

@@ -1,9 +1,8 @@
"use client"
import * as React from "react"
import { IconMinus, IconPlus } from "@tabler/icons-react"
import { CheckIcon } from "lucide-react"
import { useThemeConfig } from "@/components/active-theme"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import {
@@ -18,34 +17,31 @@ import {
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import { Switch } from "@/registry/new-york-v4/ui/switch"
const accents = [
{
name: "Blue",
value: "blue",
},
{
name: "Amber",
value: "amber",
},
{
name: "Green",
value: "green",
},
{
name: "Rose",
value: "rose",
},
]
export function AppearanceSettings() {
const { activeTheme, setActiveTheme } = useThemeConfig()
const [gpuCount, setGpuCount] = React.useState(8)
const handleGpuAdjustment = React.useCallback((adjustment: number) => {
setGpuCount((prevCount) =>
Math.max(1, Math.min(99, prevCount + adjustment))
)
}, [])
const handleGpuInputChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value, 10)
if (!isNaN(value) && value >= 1 && value <= 99) {
setGpuCount(value)
}
},
[]
)
return (
<FieldSet>
<FieldGroup>
@@ -90,37 +86,6 @@ export function AppearanceSettings() {
</RadioGroup>
</FieldSet>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Accent</FieldTitle>
<FieldDescription>Select the accent color.</FieldDescription>
</FieldContent>
<FieldSet aria-label="Accent">
<RadioGroup
className="flex flex-wrap gap-2"
value={activeTheme}
onValueChange={setActiveTheme}
>
{accents.map((accent) => (
<Label
htmlFor={accent.value}
key={accent.value}
data-theme={accent.value}
className="flex size-6 items-center justify-center rounded-full data-[theme=amber]:bg-amber-600 data-[theme=blue]:bg-blue-700 data-[theme=green]:bg-green-600 data-[theme=rose]:bg-rose-600"
>
<RadioGroupItem
id={accent.value}
value={accent.value}
aria-label={accent.name}
className="peer sr-only"
/>
<CheckIcon className="hidden size-4 stroke-white peer-data-[state=checked]:block" />
</Label>
))}
</RadioGroup>
</FieldSet>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="number-of-gpus-f6l">Number of GPUs</FieldLabel>
@@ -129,7 +94,8 @@ export function AppearanceSettings() {
<ButtonGroup>
<Input
id="number-of-gpus-f6l"
placeholder="8"
value={gpuCount}
onChange={handleGpuInputChange}
size={3}
className="h-8 !w-14 font-mono"
maxLength={3}
@@ -139,6 +105,8 @@ export function AppearanceSettings() {
size="icon-sm"
type="button"
aria-label="Decrement"
onClick={() => handleGpuAdjustment(-1)}
disabled={gpuCount <= 1}
>
<IconMinus />
</Button>
@@ -147,6 +115,8 @@ export function AppearanceSettings() {
size="icon-sm"
type="button"
aria-label="Increment"
onClick={() => handleGpuAdjustment(1)}
disabled={gpuCount >= 99}
>
<IconPlus />
</Button>

View File

@@ -33,7 +33,7 @@ export function RootComponents() {
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<InputGroupButtonExample />
<ItemDemo />
<FieldSeparator>Appearance Settings</FieldSeparator>
<FieldSeparator className="my-4">Appearance Settings</FieldSeparator>
<AppearanceSettings />
</div>
<div className="order-first flex flex-col gap-6 lg:hidden xl:order-last xl:flex *:[div]:w-full *:[div]:max-w-full">

View File

@@ -3,7 +3,7 @@ import { Spinner } from "@/registry/new-york-v4/ui/spinner"
export function SpinnerBadge() {
return (
<div className="flex items-center gap-2 [--radius:1.2rem]">
<div className="flex items-center gap-2">
<Badge>
<Spinner />
Syncing

View File

@@ -1,6 +1,7 @@
import { getAllBlockIds } from "@/lib/blocks"
import { registryCategories } from "@/lib/categories"
import { BlockDisplay } from "@/components/block-display"
import { registryCategories } from "@/registry/registry-categories"
import { getActiveStyle } from "@/registry/styles"
export const revalidate = false
export const dynamic = "force-static"
@@ -17,13 +18,16 @@ export default async function BlocksPage({
}: {
params: Promise<{ categories?: string[] }>
}) {
const { categories = [] } = await params
const [{ categories = [] }, activeStyle] = await Promise.all([
params,
getActiveStyle(),
])
const blocks = await getAllBlockIds(["registry:block"], categories)
return (
<div className="flex flex-col gap-12 md:gap-24">
{blocks.map((name) => (
<BlockDisplay name={name} key={name} />
<BlockDisplay name={name} key={name} styleName={activeStyle.name} />
))}
</div>
)

View File

@@ -2,6 +2,7 @@ import Link from "next/link"
import { BlockDisplay } from "@/components/block-display"
import { Button } from "@/registry/new-york-v4/ui/button"
import { getActiveStyle } from "@/registry/styles"
export const dynamic = "force-static"
export const revalidate = false
@@ -15,10 +16,12 @@ const FEATURED_BLOCKS = [
]
export default async function BlocksPage() {
const activeStyle = await getActiveStyle()
return (
<div className="flex flex-col gap-12 md:gap-24">
{FEATURED_BLOCKS.map((name) => (
<BlockDisplay name={name} key={name} />
<BlockDisplay name={name} key={name} styleName={activeStyle.name} />
))}
<div className="container-wrapper">
<div className="container flex justify-center py-6">

View File

@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
import { cn } from "@/lib/utils"
import { ChartDisplay } from "@/components/chart-display"
import { getActiveStyle } from "@/registry/styles"
import { charts } from "@/app/(app)/charts/charts"
export const revalidate = false
@@ -41,6 +42,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
const chartType = type as ChartType
const chartList = charts[chartType]
const activeStyle = await getActiveStyle()
return (
<div className="grid flex-1 gap-12 lg:gap-24">
@@ -54,6 +56,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
<ChartDisplay
key={chart.id}
name={chart.id}
styleName={activeStyle.name}
className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")}
>
<chart.component />

View File

@@ -6,7 +6,9 @@ import {
IconArrowRight,
IconArrowUpRight,
} from "@tabler/icons-react"
import { findNeighbour } from "fumadocs-core/server"
import fm from "front-matter"
import { findNeighbour } from "fumadocs-core/page-tree"
import z from "zod"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
@@ -25,7 +27,7 @@ export function generateStaticParams() {
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>
params: Promise<{ slug: string[] }>
}) {
const params = await props.params
const page = source.getPage(params.slug)
@@ -73,7 +75,7 @@ export async function generateMetadata(props: {
}
export default async function Page(props: {
params: Promise<{ slug?: string[] }>
params: Promise<{ slug: string[] }>
}) {
const params = await props.params
const page = source.getPage(params.slug)
@@ -82,18 +84,24 @@ export default async function Page(props: {
}
const doc = page.data
// @ts-expect-error - revisit fumadocs types.
const MDX = doc.body
const neighbours = await findNeighbour(source.pageTree, page.url)
const neighbours = findNeighbour(source.pageTree, page.url)
// @ts-expect-error - revisit fumadocs types.
const links = doc.links
const raw = await page.data.getText("raw")
const { attributes } = fm(raw)
const { links } = z
.object({
links: z
.object({
doc: z.string().optional(),
api: z.string().optional(),
})
.optional(),
})
.parse(attributes)
return (
<div
data-slot="docs"
className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full"
>
<div className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full">
<div className="flex min-w-0 flex-1 flex-col">
<div className="h-(--top-spacing) shrink-0" />
<div className="mx-auto flex w-full max-w-2xl min-w-0 flex-1 flex-col gap-8 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
@@ -104,11 +112,7 @@ export default async function Page(props: {
{doc.title}
</h1>
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:pt-1.5 sm:backdrop-blur-none">
<DocsCopyPage
// @ts-expect-error - revisit fumadocs types.
page={doc.content}
url={absoluteUrl(page.url)}
/>
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
{neighbours.previous && (
<Button
variant="secondary"
@@ -195,10 +199,8 @@ export default async function Page(props: {
</div>
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
<div className="h-(--top-spacing) shrink-0" />
{/* @ts-expect-error - revisit fumadocs types. */}
{doc.toc?.length ? (
<div className="no-scrollbar overflow-y-auto px-8">
{/* @ts-expect-error - revisit fumadocs types. */}
<DocsTableOfContents toc={doc.toc} />
<div className="h-12" />
</div>

View File

@@ -142,13 +142,7 @@ const chartConfig = {
export function ChartAreaInteractive() {
const isMobile = useIsMobile()
const [timeRange, setTimeRange] = React.useState("90d")
React.useEffect(() => {
if (isMobile) {
setTimeRange("7d")
}
}, [isMobile])
const [timeRange, setTimeRange] = React.useState("7d")
const filteredData = chartData.filter((item) => {
const date = new Date(item.date)

View File

@@ -16,8 +16,9 @@ import { Button } from "@/registry/new-york-v4/ui/button"
export const dynamic = "force-static"
export const revalidate = false
const title = "Examples"
const description = "Check out some examples app built using the components."
const title = "The Foundation for your Design System"
const description =
"A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code."
export const metadata: Metadata = {
title,
@@ -52,24 +53,20 @@ export default function ExamplesLayout({
<>
<PageHeader>
<Announcement />
<PageHeaderHeading>Build your Component Library</PageHeaderHeading>
<PageHeaderDescription>
A set of beautifully-designed, accessible components and a code
distribution platform. Works with your favorite frameworks. Open
Source. Open Code.
</PageHeaderDescription>
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
<PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions>
<Button asChild size="sm">
<Link href="/docs">Get Started</Link>
<Link href="/docs/installation">Get Started</Link>
</Button>
<Button asChild size="sm" variant="ghost">
<Link href="/blocks">Browse Blocks</Link>
<Link href="/docs/components">View Components</Link>
</Button>
</PageActions>
</PageHeader>
<PageNav id="examples">
<PageNav id="examples" className="hidden md:flex">
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
<ThemeSelector className="mr-4 hidden md:block" />
<ThemeSelector className="mr-4 hidden md:flex" />
</PageNav>
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
<div className="theme-container container flex flex-1 scroll-mt-20 flex-col">

View File

@@ -3,7 +3,10 @@ import { SiteHeader } from "@/components/site-header"
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div className="bg-background relative z-10 flex min-h-svh flex-col">
<div
data-slot="layout"
className="bg-background relative z-10 flex min-h-svh flex-col"
>
<SiteHeader />
<main className="flex flex-1 flex-col">{children}</main>
<SiteFooter />

View File

@@ -3,22 +3,26 @@ import { NextResponse, type NextRequest } from "next/server"
import { processMdxForLLMs } from "@/lib/llm"
import { source } from "@/lib/source"
import { getActiveStyle } from "@/registry/styles"
export const revalidate = false
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
{ params }: { params: Promise<{ slug?: string[] }> }
) {
const slug = (await params).slug
const [{ slug }, activeStyle] = await Promise.all([params, getActiveStyle()])
const page = source.getPage(slug)
if (!page) {
notFound()
}
// @ts-expect-error - revisit fumadocs types.
const processedContent = processMdxForLLMs(page.data.content)
const processedContent = processMdxForLLMs(
await page.data.getText("raw"),
activeStyle.name
)
return new NextResponse(processedContent, {
headers: {

View File

@@ -36,6 +36,7 @@ import { ItemDemo } from "./components/item-demo"
import { KbdDemo } from "./components/kbd-demo"
import { LabelDemo } from "./components/label-demo"
import { MenubarDemo } from "./components/menubar-demo"
import { NativeSelectDemo } from "./components/native-select-demo"
import { NavigationMenuDemo } from "./components/navigation-menu-demo"
import { PaginationDemo } from "./components/pagination-demo"
import { PopoverDemo } from "./components/popover-demo"
@@ -279,6 +280,13 @@ export const componentRegistry: Record<string, ComponentConfig> = {
type: "registry:ui",
href: "/sink/navigation-menu",
},
"native-select": {
name: "Native Select",
component: NativeSelectDemo,
type: "registry:ui",
href: "/sink/native-select",
label: "New",
},
pagination: {
name: "Pagination",
component: PaginationDemo,

View File

@@ -0,0 +1,135 @@
import {
NativeSelect,
NativeSelectOptGroup,
NativeSelectOption,
} from "@/registry/new-york-v4/ui/native-select"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
export function NativeSelectDemo() {
return (
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3">
<div className="text-muted-foreground text-sm font-medium">
Basic Select
</div>
<div className="flex flex-col gap-4">
<NativeSelect>
<NativeSelectOption value="">Select a fruit</NativeSelectOption>
<NativeSelectOption value="apple">Apple</NativeSelectOption>
<NativeSelectOption value="banana">Banana</NativeSelectOption>
<NativeSelectOption value="blueberry">Blueberry</NativeSelectOption>
<NativeSelectOption value="grapes" disabled>
Grapes
</NativeSelectOption>
<NativeSelectOption value="pineapple">Pineapple</NativeSelectOption>
</NativeSelect>
<Select>
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="blueberry">Blueberry</SelectItem>
<SelectItem value="grapes" disabled>
Grapes
</SelectItem>
<SelectItem value="pineapple">Pineapple</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="text-muted-foreground text-sm font-medium">
With Groups
</div>
<div className="flex flex-col gap-4">
<NativeSelect>
<NativeSelectOption value="">Select a food</NativeSelectOption>
<NativeSelectOptGroup label="Fruits">
<NativeSelectOption value="apple">Apple</NativeSelectOption>
<NativeSelectOption value="banana">Banana</NativeSelectOption>
<NativeSelectOption value="blueberry">
Blueberry
</NativeSelectOption>
</NativeSelectOptGroup>
<NativeSelectOptGroup label="Vegetables">
<NativeSelectOption value="carrot">Carrot</NativeSelectOption>
<NativeSelectOption value="broccoli">Broccoli</NativeSelectOption>
<NativeSelectOption value="spinach">Spinach</NativeSelectOption>
</NativeSelectOptGroup>
</NativeSelect>
<Select>
<SelectTrigger>
<SelectValue placeholder="Select a food" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="blueberry">Blueberry</SelectItem>
</SelectGroup>
<SelectGroup>
<SelectLabel>Vegetables</SelectLabel>
<SelectItem value="carrot">Carrot</SelectItem>
<SelectItem value="broccoli">Broccoli</SelectItem>
<SelectItem value="spinach">Spinach</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="text-muted-foreground text-sm font-medium">
Disabled State
</div>
<div className="flex flex-col gap-4">
<NativeSelect disabled>
<NativeSelectOption value="">Disabled</NativeSelectOption>
<NativeSelectOption value="apple">Apple</NativeSelectOption>
<NativeSelectOption value="banana">Banana</NativeSelectOption>
</NativeSelect>
<Select disabled>
<SelectTrigger>
<SelectValue placeholder="Disabled" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="text-muted-foreground text-sm font-medium">
Error State
</div>
<div className="flex flex-col gap-4">
<NativeSelect aria-invalid="true">
<NativeSelectOption value="">Error state</NativeSelectOption>
<NativeSelectOption value="apple">Apple</NativeSelectOption>
<NativeSelectOption value="banana">Banana</NativeSelectOption>
</NativeSelect>
<Select>
<SelectTrigger aria-invalid="true">
<SelectValue placeholder="Error state" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
)
}

View File

@@ -1,4 +1,11 @@
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react"
import {
BoldIcon,
BookmarkIcon,
HeartIcon,
ItalicIcon,
StarIcon,
UnderlineIcon,
} from "lucide-react"
import {
ToggleGroup,
@@ -8,7 +15,7 @@ import {
export function ToggleGroupDemo() {
return (
<div className="flex flex-wrap items-start gap-4">
<ToggleGroup type="multiple">
<ToggleGroup type="multiple" spacing={2}>
<ToggleGroupItem value="bold" aria-label="Toggle bold">
<BoldIcon />
</ToggleGroupItem>
@@ -54,12 +61,7 @@ export function ToggleGroupDemo() {
</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup
type="single"
size="sm"
defaultValue="last-24-hours"
className="*:data-[slot=toggle-group-item]:px-3"
>
<ToggleGroup type="single" size="sm" defaultValue="last-24-hours">
<ToggleGroupItem
value="last-24-hours"
aria-label="Toggle last 24 hours"
@@ -70,6 +72,68 @@ export function ToggleGroupDemo() {
Last 7 days
</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup type="single" size="sm" defaultValue="top" variant="outline">
<ToggleGroupItem value="top" aria-label="Toggle top">
Top
</ToggleGroupItem>
<ToggleGroupItem value="bottom" aria-label="Toggle bottom">
Bottom
</ToggleGroupItem>
<ToggleGroupItem value="left" aria-label="Toggle left">
Left
</ToggleGroupItem>
<ToggleGroupItem value="right" aria-label="Toggle right">
Right
</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup
type="single"
size="sm"
defaultValue="top"
variant="outline"
spacing={2}
>
<ToggleGroupItem value="top" aria-label="Toggle top">
Top
</ToggleGroupItem>
<ToggleGroupItem value="bottom" aria-label="Toggle bottom">
Bottom
</ToggleGroupItem>
<ToggleGroupItem value="left" aria-label="Toggle left">
Left
</ToggleGroupItem>
<ToggleGroupItem value="right" aria-label="Toggle right">
Right
</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup type="multiple" variant="outline" spacing={2} size="sm">
<ToggleGroupItem
value="star"
aria-label="Toggle star"
className="data-[state=on]:bg-transparent data-[state=on]:*:[svg]:fill-yellow-500 data-[state=on]:*:[svg]:stroke-yellow-500"
>
<StarIcon />
Star
</ToggleGroupItem>
<ToggleGroupItem
value="heart"
aria-label="Toggle heart"
className="data-[state=on]:bg-transparent data-[state=on]:*:[svg]:fill-red-500 data-[state=on]:*:[svg]:stroke-red-500"
>
<HeartIcon />
Heart
</ToggleGroupItem>
<ToggleGroupItem
value="bookmark"
aria-label="Toggle bookmark"
className="data-[state=on]:bg-transparent data-[state=on]:*:[svg]:fill-blue-500 data-[state=on]:*:[svg]:stroke-blue-500"
>
<BookmarkIcon />
Bookmark
</ToggleGroupItem>
</ToggleGroup>
</div>
)
}

View File

@@ -0,0 +1,105 @@
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { siteConfig } from "@/lib/config"
import { getRegistryComponent, getRegistryItems } from "@/lib/registry"
import { absoluteUrl, cn } from "@/lib/utils"
import { getStyle, STYLES } from "@/registry/styles"
export const revalidate = false
export const dynamic = "force-static"
export const dynamicParams = false
const allowedTypes = ["registry:example"]
export async function generateMetadata({
params,
}: {
params: Promise<{
style: string
}>
}): Promise<Metadata> {
const { style: styleName } = await params
const style = getStyle(styleName)
if (!style) {
return {}
}
const title = style.title
return {
title,
openGraph: {
title,
type: "article",
url: absoluteUrl(`/sandbox/${style.name}`),
images: [
{
url: siteConfig.ogImage,
width: 1200,
height: 630,
alt: siteConfig.name,
},
],
},
twitter: {
card: "summary_large_image",
title,
images: [siteConfig.ogImage],
creator: "@shadcn",
},
}
}
export async function generateStaticParams() {
return STYLES.map((style) => ({
style: style.name,
}))
}
export default async function BlockPage({
params,
}: {
params: Promise<{
style: string
}>
}) {
const { style: styleName } = await params
const style = getStyle(styleName)
if (!style) {
return notFound()
}
const items = await getRegistryItems(style.name, (item) =>
allowedTypes.includes(item.type)
)
if (items.length === 0) {
return notFound()
}
return (
<>
<div className={cn("grid gap-6")}>
{items
.filter((item) => item !== null)
.map((item) => {
const Component = getRegistryComponent(item.name, style.name)
if (!Component) {
return null
}
return (
<div
key={item.name}
className={cn("bg-background", item.meta?.container)}
>
<Component />
</div>
)
})}
</div>
</>
)
}

View File

@@ -1,30 +1,39 @@
/* eslint-disable react-hooks/static-components */
import * as React from "react"
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { registryItemSchema } from "shadcn/schema"
import { z } from "zod"
import { siteConfig } from "@/lib/config"
import { getRegistryComponent, getRegistryItem } from "@/lib/registry"
import { absoluteUrl, cn } from "@/lib/utils"
import { getStyle, STYLES, type Style } from "@/registry/styles"
export const revalidate = false
export const dynamic = "force-static"
export const dynamicParams = false
const getCachedRegistryItem = React.cache(async (name: string) => {
return await getRegistryItem(name)
})
const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => {
return await getRegistryItem(name, styleName)
}
)
export async function generateMetadata({
params,
}: {
params: Promise<{
style: string
name: string
}>
}): Promise<Metadata> {
const { name } = await params
const item = await getCachedRegistryItem(name)
const { style: styleName, name } = await params
const style = getStyle(styleName)
if (!style) {
return {}
}
const item = await getCachedRegistryItem(name, style.name)
if (!item) {
return {}
@@ -34,13 +43,13 @@ export async function generateMetadata({
const description = item.description
return {
title: item.description,
title: item.name,
description,
openGraph: {
title,
description,
type: "article",
url: absoluteUrl(`/view/${item.name}`),
url: absoluteUrl(`/view/${style.name}/${item.name}`),
images: [
{
url: siteConfig.ogImage,
@@ -62,32 +71,52 @@ export async function generateMetadata({
export async function generateStaticParams() {
const { Index } = await import("@/registry/__index__")
const index = z.record(registryItemSchema).parse(Index)
const params: Array<{ style: string; name: string }> = []
return Object.values(index)
.filter((block) =>
[
"registry:block",
"registry:component",
"registry:example",
"registry:internal",
].includes(block.type)
)
.map((block) => ({
name: block.name,
}))
for (const style of STYLES) {
if (!Index[style.name]) {
continue
}
const styleIndex = Index[style.name]
for (const itemName in styleIndex) {
const item = styleIndex[itemName]
if (
[
"registry:block",
"registry:component",
"registry:example",
"registry:internal",
].includes(item.type)
) {
params.push({
style: style.name,
name: item.name,
})
}
}
}
return params
}
export default async function BlockPage({
params,
}: {
params: Promise<{
style: string
name: string
}>
}) {
const { name } = await params
const item = await getCachedRegistryItem(name)
const Component = getRegistryComponent(name)
const { style: styleName, name } = await params
const style = getStyle(styleName)
if (!style) {
return notFound()
}
const item = await getCachedRegistryItem(name, style.name)
const Component = getRegistryComponent(name, style.name)
if (!item || !Component) {
return notFound()

View File

@@ -0,0 +1,5 @@
import { createFromSource } from "fumadocs-core/search/server"
import { source } from "@/lib/source"
export const { GET } = createFromSource(source)

View File

@@ -1,4 +1,5 @@
import type { Metadata } from "next"
import { NuqsAdapter } from "nuqs/adapters/next/app"
import { META_THEME_COLORS, siteConfig } from "@/lib/config"
import { fontVariables } from "@/lib/fonts"
@@ -84,18 +85,20 @@ export default function RootLayout({
</head>
<body
className={cn(
"text-foreground group/body theme-blue overscroll-none font-sans antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
"group/body overscroll-none antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
fontVariables
)}
>
<ThemeProvider>
<LayoutProvider>
<ActiveThemeProvider initialTheme="blue">
{children}
<TailwindIndicator />
<Toaster position="top-center" />
<Analytics />
</ActiveThemeProvider>
<NuqsAdapter>
<ActiveThemeProvider>
{children}
<TailwindIndicator />
<Toaster position="top-center" />
<Analytics />
</ActiveThemeProvider>
</NuqsAdapter>
</LayoutProvider>
</ThemeProvider>
</body>

View File

@@ -8,7 +8,7 @@ import {
useState,
} from "react"
const DEFAULT_THEME = "blue"
const DEFAULT_THEME = "default"
type ThemeContextType = {
activeTheme: string

View File

@@ -5,7 +5,7 @@ import { Badge } from "@/registry/new-york-v4/ui/badge"
export function Announcement() {
return (
<Badge asChild variant="secondary" className="rounded-full">
<Badge asChild variant="secondary" className="bg-transparent">
<Link href="/docs/changelog">
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
New Components: Field, Input Group, Item and more <ArrowRightIcon />

View File

@@ -10,9 +10,16 @@ import {
import { cn } from "@/lib/utils"
import { BlockViewer } from "@/components/block-viewer"
import { ComponentPreview } from "@/components/component-preview"
import { type Style } from "@/registry/styles"
export async function BlockDisplay({ name }: { name: string }) {
const item = await getCachedRegistryItem(name)
export async function BlockDisplay({
name,
styleName,
}: {
name: string
styleName: Style["name"]
}) {
const item = await getCachedRegistryItem(name, styleName)
if (!item?.files) {
return null
@@ -24,9 +31,15 @@ export async function BlockDisplay({ name }: { name: string }) {
])
return (
<BlockViewer item={item} tree={tree} highlightedFiles={highlightedFiles}>
<BlockViewer
item={item}
tree={tree}
highlightedFiles={highlightedFiles}
styleName={styleName}
>
<ComponentPreview
name={item.name}
styleName={styleName}
hideCode
className={cn(
"my-0 **:[.preview]:h-auto **:[.preview]:p-4 **:[.preview>.p-6]:p-0",
@@ -37,9 +50,11 @@ export async function BlockDisplay({ name }: { name: string }) {
)
}
const getCachedRegistryItem = React.cache(async (name: string) => {
return await getRegistryItem(name)
})
const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => {
return await getRegistryItem(name, styleName)
}
)
const getCachedFileTree = React.cache(
async (files: Array<{ path: string; target?: string }>) => {

View File

@@ -54,6 +54,7 @@ import {
ToggleGroup,
ToggleGroupItem,
} from "@/registry/new-york-v4/ui/toggle-group"
import { type Style } from "@/registry/styles"
type BlockViewerContext = {
item: z.infer<typeof registryItemSchema>
@@ -128,7 +129,15 @@ function BlockViewerProvider({
)
}
function BlockViewerToolbar() {
type BlockViewerProps = Pick<
BlockViewerContext,
"item" | "tree" | "highlightedFiles"
> & {
children: React.ReactNode
styleName: Style["name"]
}
function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
const { setView, view, item, resizablePanelRef, setIframeKey } =
useBlockViewer()
const { copyToClipboard, isCopied } = useCopyToClipboard()
@@ -181,7 +190,7 @@ function BlockViewerToolbar() {
asChild
title="Open in New Tab"
>
<Link href={`/view/${item.name}`} target="_blank">
<Link href={`/view/${styleName}/${item.name}`} target="_blank">
<span className="sr-only">Open in New Tab</span>
<Fullscreen />
</Link>
@@ -222,13 +231,19 @@ function BlockViewerToolbar() {
)
}
function BlockViewerIframe({ className }: { className?: string }) {
function BlockViewerIframe({
className,
styleName,
}: {
className?: string
styleName: Style["name"]
}) {
const { item, iframeKey } = useBlockViewer()
return (
<iframe
key={iframeKey}
src={`/view/${item.name}`}
src={`/view/${styleName}/${item.name}`}
height={item.meta?.iframeHeight ?? 930}
loading="lazy"
className={cn(
@@ -239,7 +254,7 @@ function BlockViewerIframe({ className }: { className?: string }) {
)
}
function BlockViewerView() {
function BlockViewerView({ styleName }: { styleName: Style["name"] }) {
const { resizablePanelRef } = useBlockViewer()
return (
@@ -256,7 +271,7 @@ function BlockViewerView() {
defaultSize={100}
minSize={30}
>
<BlockViewerIframe />
<BlockViewerIframe styleName={styleName} />
</ResizablePanel>
<ResizableHandle className="after:bg-border relative hidden w-3 bg-transparent p-0 after:absolute after:top-1/2 after:right-0 after:h-8 after:w-[6px] after:translate-x-[-1px] after:-translate-y-1/2 after:rounded-full after:transition-all after:hover:h-10 md:block" />
<ResizablePanel defaultSize={0} minSize={0} />
@@ -471,10 +486,9 @@ function BlockViewer({
tree,
highlightedFiles,
children,
styleName,
...props
}: Pick<BlockViewerContext, "item" | "tree" | "highlightedFiles"> & {
children: React.ReactNode
}) {
}: BlockViewerProps) {
return (
<BlockViewerProvider
item={item}
@@ -482,8 +496,8 @@ function BlockViewer({
highlightedFiles={highlightedFiles}
{...props}
>
<BlockViewerToolbar />
<BlockViewerView />
<BlockViewerToolbar styleName={styleName} />
<BlockViewerView styleName={styleName} />
<BlockViewerCode />
<BlockViewerMobile>{children}</BlockViewerMobile>
</BlockViewerProvider>

View File

@@ -3,8 +3,8 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { registryCategories } from "@/lib/categories"
import { ScrollArea, ScrollBar } from "@/registry/new-york-v4/ui/scroll-area"
import { registryCategories } from "@/registry/registry-categories"
export function BlocksNav() {
const pathname = usePathname()

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { CheckIcon, ClipboardIcon } from "lucide-react"
import { IconCheck, IconCopy } from "@tabler/icons-react"
import { Event, trackEvent } from "@/lib/events"
import { cn } from "@/lib/utils"
@@ -54,7 +54,7 @@ export function ChartCopyButton({
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
</TooltipTrigger>
<TooltipContent className="bg-black text-white">Copy code</TooltipContent>

View File

@@ -6,6 +6,7 @@ import { highlightCode } from "@/lib/highlight-code"
import { getRegistryItem } from "@/lib/registry"
import { cn } from "@/lib/utils"
import { ChartToolbar } from "@/components/chart-toolbar"
import { type Style } from "@/registry/styles"
export type Chart = z.infer<typeof registryItemSchema> & {
highlightedCode: string
@@ -13,10 +14,14 @@ export type Chart = z.infer<typeof registryItemSchema> & {
export async function ChartDisplay({
name,
styleName,
children,
className,
}: { name: string } & React.ComponentProps<"div">) {
const chart = await getCachedRegistryItem(name)
}: {
name: string
styleName: Style["name"]
} & React.ComponentProps<"div">) {
const chart = await getCachedRegistryItem(name, styleName)
const highlightedCode = await getChartHighlightedCode(
chart?.files?.[0]?.content ?? ""
)
@@ -45,9 +50,11 @@ export async function ChartDisplay({
)
}
const getCachedRegistryItem = React.cache(async (name: string) => {
return await getRegistryItem(name)
})
const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => {
return await getRegistryItem(name, styleName)
}
)
const getChartHighlightedCode = React.cache(async (content: string) => {
return await highlightCode(content)

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { CheckIcon, ClipboardIcon, TerminalIcon } from "lucide-react"
import { IconCheck, IconCopy, IconTerminal } from "@tabler/icons-react"
import { useConfig } from "@/hooks/use-config"
import { copyToClipboardWithMeta } from "@/components/copy-button"
@@ -80,7 +80,7 @@ export function CodeBlockCommand({
>
<div className="border-border/50 flex items-center gap-2 border-b px-3 py-1">
<div className="bg-foreground flex size-4 items-center justify-center rounded-[1px] opacity-70">
<TerminalIcon className="text-code size-3" />
<IconTerminal className="text-code size-3" />
</div>
<TabsList className="rounded-none bg-transparent p-0">
{Object.entries(tabs).map(([key]) => {
@@ -123,7 +123,7 @@ export function CodeBlockCommand({
onClick={copyCommand}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
</TooltipTrigger>
<TooltipContent>

View File

@@ -4,14 +4,15 @@ import * as React from "react"
import { useRouter } from "next/navigation"
import { type DialogProps } from "@radix-ui/react-dialog"
import { IconArrowRight } from "@tabler/icons-react"
import { useDocsSearch } from "fumadocs-core/search/client"
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
import { type Color, type ColorPalette } from "@/lib/colors"
import { trackEvent } from "@/lib/events"
import { showMcpDocs } from "@/lib/flags"
import { source } from "@/lib/source"
import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
import { useIsMac } from "@/hooks/use-is-mac"
import { useMutationObserver } from "@/hooks/use-mutation-observer"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/registry/new-york-v4/ui/button"
@@ -33,6 +34,7 @@ import {
} from "@/registry/new-york-v4/ui/dialog"
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
export function CommandMenu({
tree,
@@ -47,15 +49,63 @@ export function CommandMenu({
navItems?: { href: string; label: string }[]
}) {
const router = useRouter()
const isMac = useIsMac()
const [config] = useConfig()
const [open, setOpen] = React.useState(false)
const [selectedType, setSelectedType] = React.useState<
"color" | "page" | "component" | "block" | null
>(null)
const [copyPayload, setCopyPayload] = React.useState("")
const { search, setSearch, query } = useDocsSearch({
type: "fetch",
})
const packageManager = config.packageManager || "pnpm"
// Track search queries with debouncing to avoid excessive tracking.
const searchTimeoutRef = React.useRef<NodeJS.Timeout | undefined>(undefined)
const lastTrackedQueryRef = React.useRef<string>("")
const trackSearchQuery = React.useCallback((query: string) => {
const trimmedQuery = query.trim()
// Only track if the query is different from the last tracked query and has content.
if (trimmedQuery && trimmedQuery !== lastTrackedQueryRef.current) {
lastTrackedQueryRef.current = trimmedQuery
trackEvent({
name: "search_query",
properties: {
query: trimmedQuery,
query_length: trimmedQuery.length,
},
})
}
}, [])
const handleSearchChange = React.useCallback(
(value: string) => {
// Clear existing timeout.
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current)
}
// Set new timeout to debounce both search and tracking.
searchTimeoutRef.current = setTimeout(() => {
setSearch(value)
trackSearchQuery(value)
}, 500)
},
[setSearch, trackSearchQuery]
)
// Cleanup timeout on unmount.
React.useEffect(() => {
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current)
}
}
}, [])
const handlePageHighlight = React.useCallback(
(isComponent: boolean, item: { url: string; name?: React.ReactNode }) => {
if (isComponent) {
@@ -154,7 +204,7 @@ export function CommandMenu({
<span className="inline-flex lg:hidden">Search...</span>
<div className="absolute top-1.5 right-1.5 hidden gap-1 sm:flex">
<KbdGroup>
<Kbd className="border">{isMac ? "⌘" : "Ctrl"}</Kbd>
<Kbd className="border"></Kbd>
<Kbd className="border">K</Kbd>
</KbdGroup>
</div>
@@ -171,6 +221,7 @@ export function CommandMenu({
<Command
className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border"
filter={(value, search, keywords) => {
handleSearchChange(search)
const extendValue = value + " " + (keywords?.join(" ") || "")
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
return 1
@@ -178,10 +229,17 @@ export function CommandMenu({
return 0
}}
>
<CommandInput placeholder="Search documentation..." />
<div className="relative">
<CommandInput placeholder="Search documentation..." />
{query.isLoading && (
<div className="pointer-events-none absolute top-1/2 right-3 z-10 flex -translate-y-1/2 items-center justify-center">
<Spinner className="text-muted-foreground size-4" />
</div>
)}
</div>
<CommandList className="no-scrollbar min-h-80 scroll-pt-2 scroll-pb-1.5">
<CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
No results found.
{query.isLoading ? "Searching..." : "No results found."}
</CommandEmpty>
{navItems && navItems.length > 0 && (
<CommandGroup
@@ -322,6 +380,12 @@ export function CommandMenu({
))}
</CommandGroup>
) : null}
<SearchResults
open={open}
setOpen={setOpen}
query={query}
search={search}
/>
</CommandList>
</Command>
<div className="text-muted-foreground absolute inset-x-0 bottom-0 z-20 flex h-10 items-center gap-2 rounded-b-xl border-t border-t-neutral-100 bg-neutral-50 px-4 text-xs font-medium dark:border-t-neutral-700 dark:bg-neutral-800">
@@ -338,7 +402,7 @@ export function CommandMenu({
<>
<Separator orientation="vertical" className="!h-4" />
<div className="flex items-center gap-1">
<CommandMenuKbd>{isMac ? "⌘" : "Ctrl"}</CommandMenuKbd>
<CommandMenuKbd></CommandMenuKbd>
<CommandMenuKbd>C</CommandMenuKbd>
{copyPayload}
</div>
@@ -399,3 +463,66 @@ function CommandMenuKbd({ className, ...props }: React.ComponentProps<"kbd">) {
/>
)
}
type Query = Awaited<ReturnType<typeof useDocsSearch>>["query"]
function SearchResults({
setOpen,
query,
search,
}: {
open: boolean
setOpen: (open: boolean) => void
query: Query
search: string
}) {
const router = useRouter()
const uniqueResults =
query.data && Array.isArray(query.data)
? query.data.filter(
(item, index, self) =>
!(
item.type === "text" &&
item.content.trim().split(/\s+/).length <= 1
) && index === self.findIndex((t) => t.content === item.content)
)
: []
if (!search.trim()) {
return null
}
if (!query.data || query.data === "empty") {
return null
}
if (query.data && uniqueResults.length === 0) {
return null
}
return (
<CommandGroup
className="!px-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
heading="Search Results"
>
{uniqueResults.map((item) => {
return (
<CommandItem
key={item.id}
data-type={item.type}
onSelect={() => {
router.push(item.url)
setOpen(false)
}}
className="data-[selected=true]:border-input data-[selected=true]:bg-input/50 h-9 rounded-md border border-transparent !px-3 font-normal"
keywords={[item.content]}
value={`${item.content} ${item.type}`}
>
<div className="line-clamp-1 text-sm">{item.content}</div>
</CommandItem>
)
})}
</CommandGroup>
)
}

View File

@@ -3,77 +3,48 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { Tabs, TabsList, TabsTrigger } from "@/registry/new-york-v4/ui/tabs"
export function ComponentPreviewTabs({
className,
align = "center",
hideCode = false,
chromeLessOnMobile = false,
component,
source,
...props
}: React.ComponentProps<"div"> & {
align?: "center" | "start" | "end"
hideCode?: boolean
chromeLessOnMobile?: boolean
component: React.ReactNode
source: React.ReactNode
}) {
const [tab, setTab] = React.useState("preview")
return (
<div
className={cn("group relative mt-4 mb-12 flex flex-col gap-2", className)}
className={cn(
"group relative mt-4 mb-12 flex flex-col gap-2 rounded-lg border",
className
)}
{...props}
>
<Tabs
className="relative mr-auto w-full"
value={tab}
onValueChange={setTab}
>
<div className="flex items-center justify-between">
{!hideCode && (
<TabsList className="justify-start gap-4 rounded-none bg-transparent px-2 md:px-0">
<TabsTrigger
value="preview"
className="text-muted-foreground data-[state=active]:text-foreground px-0 text-base data-[state=active]:shadow-none dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-transparent"
>
Preview
</TabsTrigger>
<TabsTrigger
value="code"
className="text-muted-foreground data-[state=active]:text-foreground px-0 text-base data-[state=active]:shadow-none dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-transparent"
>
Code
</TabsTrigger>
</TabsList>
<div data-slot="preview">
<div
data-align={align}
className={cn(
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
)}
</div>
</Tabs>
<div
data-tab={tab}
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-1"
>
<div
data-slot="preview"
data-active={tab === "preview"}
className="invisible data-[active=true]:visible"
>
{component}
</div>
{!hideCode && (
<div
data-align={align}
className={cn(
"preview flex h-[450px] w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start"
)}
data-slot="code"
className="overflow-hidden [&_[data-rehype-pretty-code-figure]]:!m-0 [&_[data-rehype-pretty-code-figure]]:rounded-t-none [&_[data-rehype-pretty-code-figure]]:border-t [&_pre]:max-h-[400px]"
>
{component}
{source}
</div>
</div>
<div
data-slot="code"
data-active={tab === "code"}
className="absolute inset-0 hidden overflow-hidden data-[active=true]:block **:[figure]:!m-0 **:[pre]:h-[450px]"
>
{source}
</div>
)}
</div>
</div>
)

View File

@@ -3,26 +3,31 @@ import Image from "next/image"
import { ComponentPreviewTabs } from "@/components/component-preview-tabs"
import { ComponentSource } from "@/components/component-source"
import { Index } from "@/registry/__index__"
import { type Style } from "@/registry/styles"
export function ComponentPreview({
name,
styleName = "new-york-v4",
type,
className,
align = "center",
hideCode = false,
chromeLessOnMobile = false,
...props
}: React.ComponentProps<"div"> & {
name: string
styleName?: Style["name"]
align?: "center" | "start" | "end"
description?: string
hideCode?: boolean
type?: "block" | "component" | "example"
chromeLessOnMobile?: boolean
}) {
const Component = Index[name]?.component
const Component = Index[styleName]?.[name]?.component
if (!Component) {
return (
<p className="text-muted-foreground text-sm">
<p className="text-muted-foreground mt-6 text-sm">
Component{" "}
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
@@ -50,7 +55,7 @@ export function ComponentPreview({
className="bg-background absolute top-0 left-0 z-20 hidden w-[970px] max-w-none sm:w-[1280px] md:hidden dark:block md:dark:hidden"
/>
<div className="bg-background absolute inset-0 hidden w-[1600px] md:block">
<iframe src={`/view/${name}`} className="size-full" />
<iframe src={`/view/${styleName}/${name}`} className="size-full" />
</div>
</div>
)
@@ -62,7 +67,14 @@ export function ComponentPreview({
align={align}
hideCode={hideCode}
component={<Component />}
source={<ComponentSource name={name} collapsible={false} />}
source={
<ComponentSource
name={name}
collapsible={false}
styleName={styleName}
/>
}
chromeLessOnMobile={chromeLessOnMobile}
{...props}
/>
)

View File

@@ -8,6 +8,7 @@ import { cn } from "@/lib/utils"
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
import { CopyButton } from "@/components/copy-button"
import { getIconForLanguageExtension } from "@/components/icons"
import { type Style } from "@/registry/styles"
export async function ComponentSource({
name,
@@ -16,12 +17,14 @@ export async function ComponentSource({
language,
collapsible = true,
className,
styleName = "new-york-v4",
}: React.ComponentProps<"div"> & {
name?: string
src?: string
title?: string
language?: string
collapsible?: boolean
styleName?: Style["name"]
}) {
if (!name && !src) {
return null
@@ -30,7 +33,7 @@ export async function ComponentSource({
let code: string | undefined
if (name) {
const item = await getRegistryItem(name)
const item = await getRegistryItem(name, styleName)
code = item?.files?.[0]?.content
}
@@ -43,6 +46,14 @@ export async function ComponentSource({
return null
}
// Fix imports.
// Replace @/registry/${style}/ with @/components/.
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
// Replace export default with export.
code = code.replaceAll("export default", "export")
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
const lang = language ?? title?.split(".").pop() ?? "tsx"
const highlightedCode = await highlightCode(code, lang)

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { CheckIcon, ClipboardIcon } from "lucide-react"
import { IconCheck, IconCopy } from "@tabler/icons-react"
import { Event, trackEvent } from "@/lib/events"
import { cn } from "@/lib/utils"
@@ -24,11 +24,13 @@ export function CopyButton({
className,
variant = "ghost",
event,
tooltip = "Copy to Clipboard",
...props
}: React.ComponentProps<typeof Button> & {
value: string
src?: string
event?: Event["name"]
tooltip?: string
}) {
const [hasCopied, setHasCopied] = React.useState(false)
@@ -43,6 +45,7 @@ export function CopyButton({
<TooltipTrigger asChild>
<Button
data-slot="copy-button"
data-copied={hasCopied}
size="icon"
variant={variant}
className={cn(
@@ -66,12 +69,10 @@ export function CopyButton({
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied" : "Copy to Clipboard"}
</TooltipContent>
<TooltipContent>{hasCopied ? "Copied" : tooltip}</TooltipContent>
</Tooltip>
)
}

View File

@@ -0,0 +1,157 @@
"use client"
import * as React from "react"
import Link from "next/link"
import { IconCheck } from "@tabler/icons-react"
import { cn } from "@/lib/utils"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { useIsMobile } from "@/hooks/use-mobile"
import { CopyButton } from "@/components/copy-button"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/registry/new-york-v4/ui/dialog"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/registry/new-york-v4/ui/drawer"
export function DirectoryAddButton({
registry,
}: {
registry: {
name: string
url: string
}
}) {
const { copyToClipboard, isCopied } = useCopyToClipboard()
const isMobile = useIsMobile()
const [open, setOpen] = React.useState(false)
const jsonValue = `{
"registries": {
"${registry.name}": "${registry.url}"
}
}`
const Trigger = (
<Button
size="sm"
variant="outline"
className="relative z-10"
onClick={() => setOpen(true)}
>
{isCopied ? (
<IconCheck />
) : (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Model Context Protocol</title>
<path
d="M13.85 0a4.16 4.16 0 0 0-2.95 1.217L1.456 10.66a.835.835 0 0 0 0 1.18.835.835 0 0 0 1.18 0l9.442-9.442a2.49 2.49 0 0 1 3.541 0 2.49 2.49 0 0 1 0 3.541L8.59 12.97l-.1.1a.835.835 0 0 0 0 1.18.835.835 0 0 0 1.18 0l.1-.098 7.03-7.034a2.49 2.49 0 0 1 3.542 0l.049.05a2.49 2.49 0 0 1 0 3.54l-8.54 8.54a1.96 1.96 0 0 0 0 2.755l1.753 1.753a.835.835 0 0 0 1.18 0 .835.835 0 0 0 0-1.18l-1.753-1.753a.266.266 0 0 1 0-.394l8.54-8.54a4.185 4.185 0 0 0 0-5.9l-.05-.05a4.16 4.16 0 0 0-2.95-1.218c-.2 0-.401.02-.6.048a4.17 4.17 0 0 0-1.17-3.552A4.16 4.16 0 0 0 13.85 0m0 3.333a.84.84 0 0 0-.59.245L6.275 10.56a4.186 4.186 0 0 0 0 5.902 4.186 4.186 0 0 0 5.902 0L19.16 9.48a.835.835 0 0 0 0-1.18.835.835 0 0 0-1.18 0l-6.985 6.984a2.49 2.49 0 0 1-3.54 0 2.49 2.49 0 0 1 0-3.54l6.983-6.985a.835.835 0 0 0 0-1.18.84.84 0 0 0-.59-.245"
className="fill-foreground"
/>
</svg>
)}
MCP
</Button>
)
const Content = (
<>
<figure
data-rehype-pretty-code-figure
className={cn(
"group relative mt-0",
!isMobile &&
"dark:bg-background dark:[&_[data-line]:not([data-highlighted-line]):before]:bg-background!"
)}
>
<CopyButton
value={jsonValue}
className="top-3 right-2"
tooltip="Copy Code"
/>
<div data-rehype-pretty-code-title>components.json</div>
<pre className="no-scrollbar min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0">
<code data-line-numbers data-language="json">
<span data-line>{"{"}</span>
<span data-line>{' "registries": {'}</span>
<span
data-line
data-highlighted-line
>{` "${registry.name}": "${registry.url}"`}</span>
<span data-line>{" }"}</span>
<span data-line>{"}"}</span>
</code>
</pre>
</figure>
</>
)
if (isMobile) {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>{Trigger}</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Configure MCP</DrawerTitle>
<DrawerDescription>
Copy and paste the following code into your project&apos;s
components.json.
</DrawerDescription>
</DrawerHeader>
<div className="px-6">{Content}</div>
<DrawerFooter>
<DrawerClose asChild>
<Button size="sm">Close</Button>
</DrawerClose>
<Button size="sm" asChild variant="outline">
<Link href="/docs/mcp">Read the docs</Link>
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{Trigger}</DialogTrigger>
<DialogContent
className="rounded-xl border-none bg-clip-padding shadow-2xl ring-4 ring-neutral-200/80 sm:max-w-[600px] dark:bg-neutral-900 dark:ring-neutral-800"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>Configure MCP</DialogTitle>
<DialogDescription>
Copy and paste the following code into your project&apos;s
components.json.
</DialogDescription>
</DialogHeader>
{Content}
<DialogFooter className="justify-between!">
<Button size="sm" asChild variant="ghost">
<Link href="/docs/mcp">Read the docs</Link>
</Button>
<DialogClose asChild>
<Button size="sm">Done</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,90 @@
"use client"
import * as React from "react"
import { IconArrowUpRight } from "@tabler/icons-react"
import { useSearchRegistry } from "@/hooks/use-search-registry"
import { DirectoryAddButton } from "@/components/directory-add-button"
import globalRegistries from "@/registry/directory.json"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemFooter,
ItemGroup,
ItemMedia,
ItemSeparator,
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
import { SearchDirectory } from "./search-directory"
function getHomepageUrl(homepage: string) {
const url = new URL(homepage)
url.searchParams.set("utm_source", "ui.shadcn.com")
url.searchParams.set("utm_medium", "referral")
url.searchParams.set("utm_campaign", "directory")
return url.toString()
}
export function DirectoryList() {
const { registries } = useSearchRegistry()
return (
<div className="mt-6">
<SearchDirectory />
<ItemGroup className="my-8">
{registries.map((registry, index) => (
<React.Fragment key={index}>
<Item className="group/item relative gap-6 px-0">
<ItemMedia
variant="image"
dangerouslySetInnerHTML={{ __html: registry.logo }}
className="*:[svg]:fill-foreground grayscale *:[svg]:size-8"
/>
<ItemContent>
<ItemTitle>
<a
href={getHomepageUrl(registry.homepage)}
target="_blank"
rel="noopener noreferrer external"
>
{registry.name}
</a>
</ItemTitle>
{registry.description && (
<ItemDescription className="text-pretty">
{registry.description}
</ItemDescription>
)}
</ItemContent>
<ItemActions className="relative z-10 hidden self-start sm:flex">
<Button size="sm" variant="outline" asChild>
<a
href={getHomepageUrl(registry.homepage)}
target="_blank"
rel="noopener noreferrer external"
>
View <IconArrowUpRight />
</a>
</Button>
<DirectoryAddButton registry={registry} />
</ItemActions>
<ItemFooter className="justify-start pl-16 sm:hidden">
<Button size="sm" variant="outline">
View <IconArrowUpRight />
</Button>
<DirectoryAddButton registry={registry} />
</ItemFooter>
</Item>
{index < globalRegistries.length - 1 && (
<ItemSeparator className="my-1" />
)}
</React.Fragment>
))}
</ItemGroup>
</div>
)
}

View File

@@ -4,7 +4,7 @@ import { Fragment } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useBreadcrumb } from "fumadocs-core/breadcrumb"
import type { PageTree } from "fumadocs-core/server"
import type { Root } from "fumadocs-core/page-tree"
import {
Breadcrumb,
@@ -19,7 +19,7 @@ export function DocsBreadcrumb({
tree,
className,
}: {
tree: PageTree.Root
tree: Root
className?: string
}) {
const pathname = usePathname()

View File

@@ -24,13 +24,17 @@ const TOP_LEVEL_SECTIONS = [
href: "/docs/components",
},
{
name: "Registry",
href: "/docs/registry",
name: "Directory",
href: "/docs/directory",
},
{
name: "MCP Server",
href: "/docs/mcp",
},
{
name: "Forms",
href: "/docs/forms",
},
{
name: "Changelog",
href: "/docs/changelog",
@@ -47,12 +51,12 @@ export function DocsSidebar({
return (
<Sidebar
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)+2rem)] bg-transparent lg:flex"
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)-4rem)] overscroll-none bg-transparent lg:flex"
collapsible="none"
{...props}
>
<SidebarContent className="no-scrollbar overflow-x-hidden px-2 pb-12">
<div className="h-(--top-spacing) shrink-0" />
<SidebarContent className="no-scrollbar overflow-x-hidden px-2">
<div className="from-background via-background/80 to-background/50 sticky -top-1 z-10 h-8 shrink-0 bg-gradient-to-b blur-xs" />
<SidebarGroup>
<SidebarGroupLabel className="text-muted-foreground font-medium">
Sections
@@ -137,6 +141,7 @@ export function DocsSidebar({
</SidebarGroup>
)
})}
<div className="from-background via-background/80 to-background/50 sticky -bottom-1 z-10 h-16 shrink-0 bg-gradient-to-t blur-xs" />
</SidebarContent>
</Sidebar>
)

View File

@@ -11,7 +11,7 @@ export function GitHubLink() {
<Button asChild size="sm" variant="ghost" className="h-8 shadow-none">
<Link href={siteConfig.links.github} target="_blank" rel="noreferrer">
<Icons.gitHub />
<React.Suspense fallback={<Skeleton className="h-4 w-8" />}>
<React.Suspense fallback={<Skeleton className="h-4 w-[42px]" />}>
<StarsCount />
</React.Suspense>
</Link>
@@ -21,15 +21,20 @@ export function GitHubLink() {
export async function StarsCount() {
const data = await fetch("https://api.github.com/repos/shadcn-ui/ui", {
next: { revalidate: 86400 }, // Cache for 1 day (86400 seconds)
next: { revalidate: 86400 },
})
const json = await data.json()
const formattedCount =
json.stargazers_count >= 1000
? json.stargazers_count % 1000 === 0
? `${Math.floor(json.stargazers_count / 1000)}k`
: `${(json.stargazers_count / 1000).toFixed(1)}k`
: json.stargazers_count.toLocaleString()
return (
<span className="text-muted-foreground w-8 text-xs tabular-nums">
{json.stargazers_count >= 1000
? `${(json.stargazers_count / 1000).toFixed(1)}k`
: json.stargazers_count.toLocaleString()}
<span className="text-muted-foreground w-fit text-xs tabular-nums">
{formattedCount.replace(".0k", "k")}
</span>
)
}

View File

@@ -16,7 +16,7 @@ export function MainNav({
const pathname = usePathname()
return (
<nav className={cn("items-center gap-0.5", className)} {...props}>
<nav className={cn("items-center", className)} {...props}>
{items.map((item) => (
<Button key={item.href} variant="ghost" asChild size="sm">
<Link

View File

@@ -22,13 +22,17 @@ const TOP_LEVEL_SECTIONS = [
href: "/docs/components",
},
{
name: "Registry",
href: "/docs/registry",
name: "Directory",
href: "/docs/directory",
},
{
name: "MCP Server",
href: "/docs/mcp",
},
{
name: "Forms",
href: "/docs/forms",
},
{
name: "Changelog",
href: "/docs/changelog",

View File

@@ -0,0 +1,49 @@
import * as React from "react"
import { Search, X } from "lucide-react"
import { useSearchRegistry } from "@/hooks/use-search-registry"
import { Field } from "@/registry/new-york-v4/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
export const SearchDirectory = () => {
const { query, setQuery } = useSearchRegistry()
const onQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setQuery(value)
}
return (
<Field>
<InputGroup>
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupInput
placeholder="Search"
value={query}
onChange={onQueryChange}
/>
<InputGroupAddon
align="inline-end"
data-disabled={!query.length}
className="data-[disabled=true]:hidden"
>
<InputGroupButton
aria-label="Clear"
title="Clear"
size="icon-xs"
onClick={() => setQuery(null)}
>
<X />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
)
}

View File

@@ -21,7 +21,7 @@ export function SiteHeader() {
return (
<header className="bg-background sticky top-0 z-50 w-full">
<div className="container-wrapper 3xl:fixed:px-0 px-6">
<div className="3xl:fixed:container flex h-(--header-height) items-center gap-2 **:data-[slot=separator]:!h-4">
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
<MobileNav
tree={pageTree}
items={siteConfig.navItems}

View File

@@ -1,13 +1,15 @@
"use client"
import * as React from "react"
import { IconCheck, IconCopy } from "@tabler/icons-react"
import template from "lodash/template"
import { CheckIcon, ClipboardIcon } from "lucide-react"
import { THEMES } from "@/lib/themes"
import { cn } from "@/lib/utils"
import { useThemeConfig } from "@/components/active-theme"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Icons } from "@/components/icons"
import { BaseColor, baseColors, baseColorsOKLCH } from "@/registry/base-colors"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Dialog,
@@ -41,21 +43,12 @@ import {
TabsList,
TabsTrigger,
} from "@/registry/new-york-v4/ui/tabs"
import {
BaseColor,
baseColors,
baseColorsOKLCH,
} from "@/registry/registry-base-colors"
interface BaseColorOKLCH {
light: Record<string, string>
dark: Record<string, string>
}
const THEMES = baseColors.filter(
(theme) => !["slate", "stone", "gray", "zinc"].includes(theme.name)
)
export function ThemeCustomizer({ className }: React.ComponentProps<"div">) {
const { activeTheme = "neutral", setActiveTheme } = useThemeConfig()
@@ -131,9 +124,7 @@ export function CopyCodeButton({
</DrawerTrigger>
<DrawerContent className="h-auto">
<DrawerHeader>
<DrawerTitle className="capitalize">
{activeThemeName === "neutral" ? "Default" : activeThemeName}
</DrawerTitle>
<DrawerTitle className="capitalize">{activeThemeName}</DrawerTitle>
<DrawerDescription>
Copy and paste the following code into your CSS file.
</DrawerDescription>
@@ -143,15 +134,20 @@ export function CopyCodeButton({
</Drawer>
<Dialog>
<DialogTrigger asChild>
<Button className={cn("hidden sm:flex", className)} {...props}>
Copy Code
<Button
data-size={props.size}
className={cn("group/button hidden sm:flex", className)}
{...props}
>
<IconCopy />
<span className="group-data-[size=icon-sm]/button:sr-only">
Copy Code
</span>
</Button>
</DialogTrigger>
<DialogContent className="outline-none md:max-w-3xl">
<DialogContent className="rounded-xl border-none bg-clip-padding shadow-2xl ring-4 ring-neutral-200/80 outline-none md:max-w-2xl dark:bg-neutral-800 dark:ring-neutral-900">
<DialogHeader>
<DialogTitle className="capitalize">
{activeThemeName === "neutral" ? "Default" : activeThemeName}
</DialogTitle>
<DialogTitle className="capitalize">{activeThemeName}</DialogTitle>
<DialogDescription>
Copy and paste the following code into your CSS file.
</DialogDescription>
@@ -165,7 +161,7 @@ export function CopyCodeButton({
function CustomizerCode({ themeName }: { themeName: string }) {
const [hasCopied, setHasCopied] = React.useState(false)
const [tailwindVersion, setTailwindVersion] = React.useState("v4")
const [tailwindVersion, setTailwindVersion] = React.useState("v4-oklch")
const activeTheme = React.useMemo(
() => baseColors.find((theme) => theme.name === themeName),
[themeName]
@@ -191,10 +187,11 @@ function CustomizerCode({ themeName }: { themeName: string }) {
className="min-w-0 px-4 pb-4 md:p-0"
>
<TabsList>
<TabsTrigger value="v4">Tailwind v4</TabsTrigger>
<TabsTrigger value="v4-oklch">OKLCH</TabsTrigger>
<TabsTrigger value="v4-hsl">HSL</TabsTrigger>
<TabsTrigger value="v3">Tailwind v3</TabsTrigger>
</TabsList>
<TabsContent value="v4">
<TabsContent value="v4-oklch">
<figure
data-rehype-pretty-code-figure
className="!mx-0 mt-0 rounded-lg"
@@ -216,14 +213,12 @@ function CustomizerCode({ themeName }: { themeName: string }) {
className="bg-code text-code-foreground absolute top-3 right-2 z-10 size-7 shadow-none hover:opacity-100 focus-visible:opacity-100"
onClick={() => {
copyToClipboardWithMeta(
tailwindVersion === "v3"
? getThemeCode(activeTheme, 0.65)
: getThemeCodeOKLCH(activeThemeOKLCH, 0.65),
getThemeCodeOKLCH(activeThemeOKLCH, 0.65),
{
name: "copy_theme_code",
properties: {
theme: themeName,
radius: 0.5,
radius: 0.65,
},
}
)
@@ -231,7 +226,7 @@ function CustomizerCode({ themeName }: { themeName: string }) {
}}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
<code data-line-numbers data-language="css">
<span data-line className="line text-code-foreground">
@@ -246,7 +241,8 @@ function CustomizerCode({ themeName }: { themeName: string }) {
className="line text-code-foreground"
key={key}
>
&nbsp;&nbsp;&nbsp;--{key}: {value};
&nbsp;&nbsp;&nbsp;--{key}: <ColorIndicator color={value} />{" "}
{value};
</span>
))}
<span data-line className="line text-code-foreground">
@@ -264,7 +260,8 @@ function CustomizerCode({ themeName }: { themeName: string }) {
className="line text-code-foreground"
key={key}
>
&nbsp;&nbsp;&nbsp;--{key}: {value};
&nbsp;&nbsp;&nbsp;--{key}: <ColorIndicator color={value} />{" "}
{value};
</span>
))}
<span data-line className="line text-code-foreground">
@@ -274,6 +271,90 @@ function CustomizerCode({ themeName }: { themeName: string }) {
</pre>
</figure>
</TabsContent>
<TabsContent value="v4-hsl">
<figure
data-rehype-pretty-code-figure
className="!mx-0 mt-0 rounded-lg"
>
<figcaption
className="text-code-foreground [&_svg]:text-code-foreground flex items-center gap-2 [&_svg]:size-4 [&_svg]:opacity-70"
data-rehype-pretty-code-title=""
data-language="css"
data-theme="github-dark github-light-default"
>
<Icons.css className="fill-foreground" />
app/globals.css
</figcaption>
<pre className="no-scrollbar max-h-[300px] min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0 md:max-h-[450px]">
<Button
data-slot="copy-button"
size="icon"
variant="ghost"
className="bg-code text-code-foreground absolute top-3 right-2 z-10 size-7 shadow-none hover:opacity-100 focus-visible:opacity-100"
onClick={() => {
copyToClipboardWithMeta(
getThemeCodeHSLV4(activeTheme, 0.65),
{
name: "copy_theme_code",
properties: {
theme: themeName,
radius: 0.65,
},
}
)
setHasCopied(true)
}}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
<code data-line-numbers data-language="css">
<span data-line className="line text-code-foreground">
&nbsp;:root &#123;
</span>
<span data-line className="line text-code-foreground">
&nbsp;&nbsp;&nbsp;--radius: 0.65rem;
</span>
{Object.entries(activeTheme?.cssVars.light || {}).map(
([key, value]) => (
<span
data-line
className="line text-code-foreground"
key={key}
>
&nbsp;&nbsp;&nbsp;--{key}:{" "}
<ColorIndicator color={`hsl(${value})`} /> hsl({value});
</span>
)
)}
<span data-line className="line text-code-foreground">
&nbsp;&#125;
</span>
<span data-line className="line text-code-foreground">
&nbsp;
</span>
<span data-line className="line text-code-foreground">
&nbsp;.dark &#123;
</span>
{Object.entries(activeTheme?.cssVars.dark || {}).map(
([key, value]) => (
<span
data-line
className="line text-code-foreground"
key={key}
>
&nbsp;&nbsp;&nbsp;--{key}:{" "}
<ColorIndicator color={`hsl(${value})`} /> hsl({value});
</span>
)
)}
<span data-line className="line text-code-foreground">
&nbsp;&#125;
</span>
</code>
</pre>
</figure>
</TabsContent>
<TabsContent value="v3">
<figure
data-rehype-pretty-code-figure
@@ -295,23 +376,18 @@ function CustomizerCode({ themeName }: { themeName: string }) {
variant="ghost"
className="bg-code text-code-foreground absolute top-3 right-2 z-10 size-7 shadow-none hover:opacity-100 focus-visible:opacity-100"
onClick={() => {
copyToClipboardWithMeta(
tailwindVersion === "v3"
? getThemeCode(activeTheme, 0.65)
: getThemeCodeOKLCH(activeThemeOKLCH, 0.65),
{
name: "copy_theme_code",
properties: {
theme: themeName,
radius: 0.5,
},
}
)
copyToClipboardWithMeta(getThemeCode(activeTheme, 0.5), {
name: "copy_theme_code",
properties: {
theme: themeName,
radius: 0.5,
},
})
setHasCopied(true)
}}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
<code data-line-numbers data-language="css">
<span data-line className="line">
@@ -322,10 +398,16 @@ function CustomizerCode({ themeName }: { themeName: string }) {
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--background:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.light["background"]})`}
/>{" "}
{activeTheme?.cssVars.light["background"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--foreground:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.light["foreground"]})`}
/>{" "}
{activeTheme?.cssVars.light["foreground"]};
</span>
{[
@@ -340,6 +422,13 @@ function CustomizerCode({ themeName }: { themeName: string }) {
<React.Fragment key={prefix}>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.light[
prefix as keyof typeof activeTheme.cssVars.light
]
})`}
/>{" "}
{
activeTheme?.cssVars.light[
prefix as keyof typeof activeTheme.cssVars.light
@@ -349,6 +438,13 @@ function CustomizerCode({ themeName }: { themeName: string }) {
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}-foreground:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.light[
`${prefix}-foreground` as keyof typeof activeTheme.cssVars.light
]
})`}
/>{" "}
{
activeTheme?.cssVars.light[
`${prefix}-foreground` as keyof typeof activeTheme.cssVars.light
@@ -360,14 +456,23 @@ function CustomizerCode({ themeName }: { themeName: string }) {
))}
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--border:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.light["border"]})`}
/>{" "}
{activeTheme?.cssVars.light["border"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--input:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.light["input"]})`}
/>{" "}
{activeTheme?.cssVars.light["input"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--ring:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.light["ring"]})`}
/>{" "}
{activeTheme?.cssVars.light["ring"]};
</span>
<span data-line className="line">
@@ -378,6 +483,13 @@ function CustomizerCode({ themeName }: { themeName: string }) {
<React.Fragment key={prefix}>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.light[
prefix as keyof typeof activeTheme.cssVars.light
]
})`}
/>{" "}
{
activeTheme?.cssVars.light[
prefix as keyof typeof activeTheme.cssVars.light
@@ -399,10 +511,16 @@ function CustomizerCode({ themeName }: { themeName: string }) {
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--background:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.dark["background"]})`}
/>{" "}
{activeTheme?.cssVars.dark["background"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--foreground:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.dark["foreground"]})`}
/>{" "}
{activeTheme?.cssVars.dark["foreground"]};
</span>
{[
@@ -417,6 +535,13 @@ function CustomizerCode({ themeName }: { themeName: string }) {
<React.Fragment key={prefix}>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.dark[
prefix as keyof typeof activeTheme.cssVars.dark
]
})`}
/>{" "}
{
activeTheme?.cssVars.dark[
prefix as keyof typeof activeTheme.cssVars.dark
@@ -426,6 +551,13 @@ function CustomizerCode({ themeName }: { themeName: string }) {
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}-foreground:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.dark[
`${prefix}-foreground` as keyof typeof activeTheme.cssVars.dark
]
})`}
/>{" "}
{
activeTheme?.cssVars.dark[
`${prefix}-foreground` as keyof typeof activeTheme.cssVars.dark
@@ -437,14 +569,23 @@ function CustomizerCode({ themeName }: { themeName: string }) {
))}
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--border:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.dark["border"]})`}
/>{" "}
{activeTheme?.cssVars.dark["border"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--input:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.dark["input"]})`}
/>{" "}
{activeTheme?.cssVars.dark["input"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--ring:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.dark["ring"]})`}
/>{" "}
{activeTheme?.cssVars.dark["ring"]};
</span>
{["chart-1", "chart-2", "chart-3", "chart-4", "chart-5"].map(
@@ -452,6 +593,13 @@ function CustomizerCode({ themeName }: { themeName: string }) {
<React.Fragment key={prefix}>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.dark[
prefix as keyof typeof activeTheme.cssVars.dark
]
})`}
/>{" "}
{
activeTheme?.cssVars.dark[
prefix as keyof typeof activeTheme.cssVars.dark
@@ -477,6 +625,15 @@ function CustomizerCode({ themeName }: { themeName: string }) {
)
}
function ColorIndicator({ color }: { color: string }) {
return (
<span
className="border-border/50 inline-block size-3 border"
style={{ backgroundColor: color }}
/>
)
}
function getThemeCodeOKLCH(theme: BaseColorOKLCH | undefined, radius: number) {
if (!theme) {
return ""
@@ -509,6 +666,27 @@ function getThemeCode(theme: BaseColor | undefined, radius: number) {
})
}
function getThemeCodeHSLV4(theme: BaseColor | undefined, radius: number) {
if (!theme) {
return ""
}
const rootSection =
":root {\n --radius: " +
radius +
"rem;\n" +
Object.entries(theme.cssVars.light)
.map((entry) => " --" + entry[0] + ": hsl(" + entry[1] + ");")
.join("\n") +
"\n}\n\n.dark {\n" +
Object.entries(theme.cssVars.dark)
.map((entry) => " --" + entry[0] + ": hsl(" + entry[1] + ");")
.join("\n") +
"\n}\n"
return rootSection
}
const BASE_STYLES_WITH_VARIABLES = `
@layer base {
:root {

View File

@@ -1,74 +1,30 @@
"use client"
import { THEMES } from "@/lib/themes"
import { cn } from "@/lib/utils"
import { useThemeConfig } from "@/components/active-theme"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
const DEFAULT_THEMES = [
{
name: "Default",
value: "default",
},
{
name: "Scaled",
value: "scaled",
},
{
name: "Mono",
value: "mono",
},
]
const COLOR_THEMES = [
{
name: "Blue",
value: "blue",
},
{
name: "Green",
value: "green",
},
{
name: "Amber",
value: "amber",
},
{
name: "Rose",
value: "rose",
},
{
name: "Purple",
value: "purple",
},
{
name: "Orange",
value: "orange",
},
{
name: "Teal",
value: "teal",
},
]
import { CopyCodeButton } from "./theme-customizer"
export function ThemeSelector({ className }: React.ComponentProps<"div">) {
const { activeTheme, setActiveTheme } = useThemeConfig()
const value = activeTheme === "default" ? "neutral" : activeTheme
return (
<div className={cn("flex items-center gap-2", className)}>
<Label htmlFor="theme-selector" className="sr-only">
Theme
</Label>
<Select value={activeTheme} onValueChange={setActiveTheme}>
<Select value={value} onValueChange={setActiveTheme}>
<SelectTrigger
id="theme-selector"
size="sm"
@@ -78,32 +34,18 @@ export function ThemeSelector({ className }: React.ComponentProps<"div">) {
<SelectValue placeholder="Select a theme" />
</SelectTrigger>
<SelectContent align="end">
<SelectGroup>
{DEFAULT_THEMES.map((theme) => (
<SelectItem
key={theme.name}
value={theme.value}
className="data-[state=checked]:opacity-50"
>
{theme.name}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Colors</SelectLabel>
{COLOR_THEMES.map((theme) => (
<SelectItem
key={theme.name}
value={theme.value}
className="data-[state=checked]:opacity-50"
>
{theme.name}
</SelectItem>
))}
</SelectGroup>
{THEMES.map((theme) => (
<SelectItem
key={theme.name}
value={theme.name}
className="data-[state=checked]:opacity-50"
>
{theme.label}
</SelectItem>
))}
</SelectContent>
</Select>
<CopyCodeButton variant="secondary" size="icon-sm" />
</div>
)
}

View File

@@ -34,15 +34,24 @@ import { Spinner } from "@/components/ui/spinner"
Here's what it looks like:
<ComponentPreview name="spinner-basic" className="[&_.preview]:h-[250px]" />
<ComponentPreview
name="spinner-basic"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
Here's what it looks like in a button:
<ComponentPreview name="spinner-button" className="[&_.preview]:h-[250px]" />
<ComponentPreview
name="spinner-button"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
You can edit the code and replace it with your own spinner.
<ComponentPreview name="spinner-custom" className="[&_.preview]:h-[250px]" />
<ComponentPreview
name="spinner-custom"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
### Kbd
@@ -65,7 +74,10 @@ Use `KbdGroup` to group keyboard keys together.
</KbdGroup>
```
<ComponentPreview name="kbd-demo" className="[&_.preview]:h-[250px]" />
<ComponentPreview
name="kbd-demo"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
You can add it to buttons, tooltips, input groups, and more.
@@ -73,7 +85,10 @@ You can add it to buttons, tooltips, input groups, and more.
I got a lot of requests for this one: Button Group. It's a container that groups related buttons together with consistent styling. Great for action groups, split buttons, and more.
<ComponentPreview name="button-group-demo" className="[&_.preview]:h-[250px]" />
<ComponentPreview
name="button-group-demo"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
Here's the code:
@@ -107,14 +122,14 @@ Use `ButtonGroupSeparator` to create split buttons. Classic dropdown pattern.
<ComponentPreview
name="button-group-dropdown"
className="[&_.preview]:h-[250px]"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
You can also use it to add prefix or suffix buttons and text to inputs.
<ComponentPreview
name="button-group-select"
className="[&_.preview]:h-[250px]"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
```tsx showLineNumbers
@@ -148,31 +163,37 @@ import {
Here's a preview with icons:
<ComponentPreview name="input-group-icon" className="[&_.preview]:h-[300px]" />
<ComponentPreview
name="input-group-icon"
className="[&_.preview]:h-[300px] [&_pre]:!h-[300px]"
/>
You can also add buttons to the input group.
<ComponentPreview
name="input-group-button"
className="[&_.preview]:h-[300px]"
className="[&_.preview]:h-[300px] [&_pre]:!h-[300px]"
/>
Or text, labels, tooltips,...
<ComponentPreview name="input-group-text" className="[&_.preview]:h-[350px]" />
<ComponentPreview
name="input-group-text"
className="[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
/>
It also works with textareas so you can build really complex components with lots of knobs and dials or yet another prompt form.
<ComponentPreview
name="input-group-textarea"
className="[&_.preview]:h-[450px]"
className="[&_.preview]:h-[450px] [&_pre]:!h-[450px]"
/>
Oh here are some cool ones with spinners:
<ComponentPreview
name="input-group-spinner"
className="[&_.preview]:h-[350px]"
className="[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
/>
### Field
@@ -202,15 +223,24 @@ Here's a basic field with an input:
</Field>
```
<ComponentPreview name="field-input" className="[&_.preview]:h-[350px]" />
<ComponentPreview
name="field-input"
className="[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
/>
It works with all form controls. Inputs, textareas, selects, checkboxes, radios, switches, sliders, you name it. Here's a full example:
<ComponentPreview name="field-demo" className="[&_.preview]:h-[850px]" />
<ComponentPreview
name="field-demo"
className="[&_.preview]:h-[850px] [&_pre]:!h-[850px]"
/>
Here are some checkbox fields:
<ComponentPreview name="field-checkbox" className="[&_.preview]:h-[500px]" />
<ComponentPreview
name="field-checkbox"
className="[&_.preview]:h-[500px] [&_pre]:!h-[500px]"
/>
You can group fields together using `FieldGroup` and `FieldSet`. Perfect for
multi-section forms.
@@ -225,16 +255,25 @@ multi-section forms.
</FieldSet>
```
<ComponentPreview name="field-fieldset" className="[&_.preview]:h-[500px]" />
<ComponentPreview
name="field-fieldset"
className="[&_.preview]:h-[500px] [&_pre]:!h-[500px]"
/>
Making it responsive is easy. Use `orientation="responsive"` and it switches
between vertical and horizontal layouts based on container width. Done.
<ComponentPreview name="field-responsive" className="[&_.preview]:h-[600px]" />
<ComponentPreview
name="field-responsive"
className="[&_.preview]:h-[600px] [&_pre]:!h-[600px]"
/>
Wait here's more. Wrap your fields in `FieldLabel` to create a selectable field group. Really easy. And it looks great.
<ComponentPreview name="field-choice-card" className="[&_.preview]:h-[600px]" />
<ComponentPreview
name="field-choice-card"
className="[&_.preview]:h-[600px] [&_pre]:!h-[600px]"
/>
### Item
@@ -268,26 +307,26 @@ Here's a basic item:
<ComponentPreview
name="item-demo"
className="[&_.preview]:h-[300px] [&_.preview]:p-4"
className="[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
/>
You can add icons, avatars, or images to the item.
<ComponentPreview
name="item-icon"
className="[&_.preview]:h-[300px] [&_.preview]:p-4"
className="[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
/>
<ComponentPreview
name="item-avatar"
className="[&_.preview]:h-[300px] [&_.preview]:p-4"
className="[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
/>
And here's what a list of items looks like with `ItemGroup`:
<ComponentPreview
name="item-group"
className="[&_.preview]:h-[500px] [&_.preview]:p-4"
className="[&_.preview]:h-[500px] [&_.preview]:p-4 [&_pre]:!h-[500px]"
/>
Need it as a link? Use the `asChild` prop:
@@ -308,7 +347,7 @@ Need it as a link? Use the `asChild` prop:
<ComponentPreview
name="item-link"
className="[&_.preview]:h-[400px] [&_.preview]:p-4"
className="[&_.preview]:h-[400px] [&_.preview]:p-4 [&_pre]:!h-[400px]"
/>
### Empty
@@ -342,16 +381,22 @@ Here's how you use it:
<ComponentPreview
name="empty-demo"
className="[&_.preview]:h-[400px] [&_.preview]:p-4"
className="[&_.preview]:h-[400px] [&_.preview]:p-4 [&_pre]:!h-[400px]"
/>
You can use it with avatars:
<ComponentPreview name="empty-avatar" className="[&_.preview]:h-[400px]" />
<ComponentPreview
name="empty-avatar"
className="[&_.preview]:h-[400px] [&_pre]:!h-[400px]"
/>
Or with input groups for things like search results or email subscriptions:
<ComponentPreview name="empty-input-group" className="[&_.preview]:h-[450px]" />
<ComponentPreview
name="empty-input-group"
className="[&_.preview]:h-[450px] [&_pre]:!h-[450px]"
/>
That's it. Seven new components. Works with all your libraries. Ready for your projects.
@@ -969,7 +1014,7 @@ It has support for infinite looping, autoplay, vertical orientation, and more.
### Drawer
Oh the drawer component 😍. Built on top of [Vaul](https://github.com/emilkowalski/vaul) by [emilkowalski\_](https://twitter.com/emilkowalski_).
Oh the drawer component 😍. Built on top of [Vaul](https://github.com/emilkowalski/vaul) by [emilkowalski](https://twitter.com/emilkowalski).
Try opening the following drawer on mobile. It looks amazing!
@@ -991,7 +1036,7 @@ Build resizable panel groups and layouts with this `<Resizable />` component.
### Sonner
Another one by [emilkowalski\_](https://twitter.com/emilkowalski_). The last toast component you'll ever need. Sonner is now availabe in shadcn/ui.
Another one by [emilkowalski](https://twitter.com/emilkowalski). The last toast component you'll ever need. Sonner is now availabe in shadcn/ui.
<ComponentPreview name="sonner-demo" />

View File

@@ -0,0 +1,69 @@
---
title: Registry Directory
description: Discover community registries for shadcn/ui components and blocks.
---
These registries are built into the CLI with no additional configuration required. To add a component, run: `npx shadcn add @<registry>/<component>`.
<DirectoryList />
Don't see a registry? Learn how to [add it here](/docs/registry/registry-index).
## Documentation
You can use the `shadcn` CLI to run your own code registry. Running your own registry allows you to distribute your custom components, hooks, pages, config, rules and other files to any project.
<div className="mt-6 grid gap-4 sm:grid-cols-2">
<LinkedCard href="/docs/registry/getting-started" className="items-start text-sm md:p-6">
<div className="font-medium">Getting Started</div>
<div className="text-muted-foreground">
Set up and build your own registry
</div>
</LinkedCard>
<LinkedCard
href="/docs/registry/authentication"
className="items-start text-sm md:p-6"
>
<div className="font-medium">Authentication</div>
<div className="text-muted-foreground">
Secure your registry with authentication
</div>
</LinkedCard>
<LinkedCard
href="/docs/registry/namespace"
className="items-start text-sm md:p-6"
>
<div className="font-medium">Namespaces</div>
<div className="text-muted-foreground">
Configure registries with namespaces
</div>
</LinkedCard>
<LinkedCard
href="/docs/registry/registry-index"
className="items-start text-sm md:p-6"
>
<div className="font-medium">Add a Registry</div>
<div className="text-muted-foreground">
Learn how to add a registry to the directory
</div>
</LinkedCard>
<LinkedCard
href="/docs/registry/examples"
className="items-start text-sm md:p-6"
>
<div className="font-medium">Examples</div>
<div className="text-muted-foreground">
Registry item examples and configurations
</div>
</LinkedCard>
<LinkedCard
href="/docs/registry/registry-json"
className="items-start text-sm md:p-6"
>
<div className="font-medium">Schema</div>
<div className="text-muted-foreground">
Schema specification for registry.json
</div>
</LinkedCard>
</div>

View File

@@ -8,12 +8,13 @@ description: Every component recreated in Figma. With customizable props, typogr
questions or feedback, please reach out to the Figma file maintainers.
</Callout>
## Free
- [Obra shadcn/ui](https://www.figma.com/community/file/1514746685758799870/obra-shadcn-ui) by [Obra Studio](https://obra.studio/) - Carefully crafted kit designed in the philosophy of shadcn, tracks v4, MIT licensed
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
## Paid
- [shadcn/ui kit](https://shadcndesign.com) by [ Matt Wierzbicki](https://x.com/matsugfx) - A premium, always up-to-date UI kit for Figma - shadcn/ui compatible and optimized for smooth design-to-dev handoff.
- [Shadcraft UI Kit](https://shadcraft.com) - The most advanced shadcn-compatible kit with instant theming via [tweakcn](https://tweakcn.com), a pro library of components and templates, and complete coverage of shadcn components and blocks.
## Free
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
- [Obra shadcn/ui](https://www.figma.com/community/file/1514746685758799870/obra-shadcn-ui) by [Obra Studio](https://obra.studio/) - Carefully crafted kit designed in the philosophy of shadcn, tracks v4, MIT licensed
- [shadcn/studio UI Kit](https://shadcnstudio.com/figma) - Accelerate design & development with a shadcn/ui compatible Figma kit with updated components, 550+ blocks, 10+ templates, 20+ themes, and an AI tool that converts designs into shadcn/ui code.

View File

@@ -9,20 +9,24 @@ However we provide a JavaScript version of the components as well. The JavaScrip
To opt-out of TypeScript, you can use the `tsx` flag in your `components.json` file.
```json {10} title="components.json" showLineNumbers
```json {4} title="components.json" showLineNumbers
{
"style": "default",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"config": "",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
"rsc": false,
"tsx": false,
"iconLibrary": "lucide",
"aliases": {
"utils": "~/lib/utils",
"components": "~/components"
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
```

View File

@@ -182,7 +182,7 @@ To configure MCP in VS Code with GitHub Copilot, add the shadcn server to your p
```json title=".vscode/mcp.json" showLineNumbers
{
"mcpServers": {
"servers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]

View File

@@ -14,6 +14,7 @@
"blocks",
"figma",
"changelog",
"[llms.txt](/llms.txt)",
"legacy"
]
}

View File

@@ -24,7 +24,7 @@ To create a new monorepo project, run the `init` command. You will be prompted
to select the type of project you are creating.
```bash
npx shadcn@canary init
npx shadcn@latest init
```
Select the `Next.js (Monorepo)` option.
@@ -51,15 +51,15 @@ cd apps/web
```
```bash
npx shadcn@canary add [COMPONENT]
npx shadcn@latest add [COMPONENT]
```
The CLI will figure out what type of component you are adding and install the
correct files to the correct path.
For example, if you run `npx shadcn@canary add button`, the CLI will install the button component under `packages/ui` and update the import path for components in `apps/web`.
For example, if you run `npx shadcn@latest add button`, the CLI will install the button component under `packages/ui` and update the import path for components in `apps/web`.
If you run `npx shadcn@canary add login-01`, the CLI will install the `button`, `label`, `input` and `card` components under `packages/ui` and the `login-form` component under `apps/web/components`.
If you run `npx shadcn@latest add login-01`, the CLI will install the `button`, `label`, `input` and `card` components under `packages/ui` and the `login-form` component under `apps/web/components`.
### Importing components

View File

@@ -3,17 +3,9 @@ title: Next.js 15 + React 19
description: Using shadcn/ui with Next.js 15 and React 19.
---
<Callout className="mb-6 border-blue-600 bg-blue-50 dark:border-blue-900 dark:bg-blue-950 [&_code]:bg-blue-100 dark:[&_code]:bg-blue-900">
<Callout className="">
**Update:** We have added full support for React 19 and Tailwind v4 in the
`canary` release. See the docs for [Tailwind v4](/docs/tailwind-v4) for more
information.
</Callout>
<Callout>
**The following guide applies to any framework that supports React 19**. I
titled this page "Next.js 15 + React 19" to help people upgrading to Next.js
15 find it. We are working with package maintainers to help upgrade to React
19.
`latest` release. **This guide might be outdated. Proceed with caution.**
</Callout>
## TL;DR
@@ -148,7 +140,7 @@ To make it easy for you track the progress of the upgrade, I've created a table
| [react-day-picker](https://www.npmjs.com/package/react-day-picker) | ✅ | Works with flag for npm. Work to upgrade to v9 in progress. |
| [input-otp](https://www.npmjs.com/package/input-otp) | ✅ | |
| [vaul](https://www.npmjs.com/package/vaul) | ✅ | |
| [@radix-ui/react-icons](https://www.npmjs.com/package/@radix-ui/react-icons) | 🚧 | See [PR #194](https://github.com/radix-ui/icons/pull/194) |
| [@radix-ui/react-icons](https://www.npmjs.com/package/@radix-ui/react-icons) | | See [PR #194](https://github.com/radix-ui/icons/pull/194) |
| [cmdk](https://www.npmjs.com/package/cmdk) | ✅ | |
If you have any questions, please [open an issue](https://github.com/shadcn/ui/issues) on GitHub.

View File

@@ -1,208 +0,0 @@
---
title: Styleguide
description: A styleguide for writing documentation in mdx.
---
The OpenAI API provides a simple interface to state-of-the-art AI models for text generation, natural language processing, computer vision, and more. This example generates text output from a prompt, as you might using ChatGPT.
## Analyze image inputs
You can provide image inputs to the model as well. Scan receipts, analyze screenshots, or find objects in the real world with [computer vision](/docs/installation/computer-vision). This is code in a `pre` tag and `npx` command in a `code` tag.
```bash
npm install foo
```
```bash
npx shadcn@latest init
```
```bash
npx shadcn@latest add button
```
```tsx
<Button>Click me</Button>
```
```tsx showLineNumbers
// With line numbers
export default function Home() {
return <div>Hello</div>
}
```
```tsx title="Button.tsx"
export default function Button({ children }: { children: React.ReactNode }) {
return <button>{children}</button>
}
```
This is a code block with a title.
## Line Numbers and Line Highlighting
Draw attention to a particular line of code.
```tsx {4} showLineNumbers
import { useFloating } from "@floating-ui/react"
function MyComponent() {
const { refs, floatingStyles } = useFloating()
return (
<>
<div ref={refs.setReference} />
<div ref={refs.setFloating} style={floatingStyles} />
</>
)
}
```
## Word Highlighting
Draw attention to a particular word or series of characters.
```tsx /floatingStyles/
import { useFloating } from "@floating-ui/react"
function MyComponent() {
const { refs, floatingStyles } = useFloating()
return (
<>
<div ref={refs.setReference} />
<div ref={refs.setFloating} style={floatingStyles} />
</>
)
}
```
How
```tsx title="apps/www/registry/registry-blocks.tsx"
export const blocks = [
// ...
{
name: "dashboard-01",
author: "shadcn (https://ui.shadcn.com)",
title: "Dashboard",
description: "A simple dashboard with a hello world component.",
type: "registry:block",
registryDependencies: ["input", "button", "card"],
dependencies: ["zod"],
files: [
{
path: "blocks/dashboard-01/page.tsx",
type: "registry:page",
target: "app/dashboard/page.tsx",
},
{
path: "blocks/dashboard-01/components/hello-world.tsx",
type: "registry:component",
},
{
path: "blocks/dashboard-01/components/example-card.tsx",
type: "registry:component",
},
{
path: "blocks/dashboard-01/hooks/use-hello-world.ts",
type: "registry:hook",
},
{
path: "blocks/dashboard-01/lib/format-date.ts",
type: "registry:lib",
},
],
categories: ["dashboard"],
},
]
```
```txt
apps
└── web # Your app goes here.
├── app
│ └── page.tsx
├── components
│ └── login-form.tsx
├── components.json
└── package.json
packages
└── ui # Your components and dependencies are installed here.
├── src
│ ├── components
│ │ └── button.tsx
│ ├── hooks
│ ├── lib
│ │ └── utils.ts
│ └── styles
│ └── globals.css
├── components.json
└── package.json
package.json
turbo.json
```
```diff showLineNumbers
- @plugin 'tailwindcss-animate';
+ @import "tw-animate-css";
```
## CSS Variables
```tsx /bg-background/ /text-foreground/
<div className="bg-background text-foreground" />
```
To use CSS variables for theming set `tailwind.cssVariables` to `true` in your `components.json` file.
```json {8} title="components.json"
{
"style": "default",
"rsc": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
```
## Utility classes
```tsx /bg-zinc-950/ /text-zinc-50/ /dark:bg-white/ /dark:text-zinc-950/
<div className="bg-zinc-950 dark:bg-white" />
```
To use utility classes for theming set `tailwind.cssVariables` to `false` in your `components.json` file.
```json {8} title="components.json"
{
"style": "default",
"rsc": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": false
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
```

View File

@@ -15,7 +15,7 @@ To use CSS variables for theming set `tailwind.cssVariables` to `true` in your `
```json {8} title="components.json" showLineNumbers
{
"style": "default",
"style": "new-york",
"rsc": true,
"tailwind": {
"config": "",
@@ -44,7 +44,7 @@ To use utility classes for theming set `tailwind.cssVariables` to `false` in you
```json {8} title="components.json" showLineNumbers
{
"style": "default",
"style": "new-york",
"rsc": true,
"tailwind": {
"config": "",
@@ -52,14 +52,14 @@ To use utility classes for theming set `tailwind.cssVariables` to `false` in you
"baseColor": "neutral",
"cssVariables": false
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
}
```
@@ -163,7 +163,7 @@ Here's the list of variables available for customization:
## Adding new colors
To add new colors, you need to add them to your CSS file and to your `tailwind.config.js` file.
To add new colors, you need to add them to your CSS file under the `:root` and `dark` pseudo-classes. Then, use the `@theme inline` directive to make the colors available as CSS variables.
```css title="app/globals.css" showLineNumbers
:root {

View File

@@ -49,7 +49,7 @@ import { Badge } from "@/components/ui/badge"
```
```tsx
<Badge variant="default |outline | secondary | destructive">Badge</Badge>
<Badge variant="default | outline | secondary | destructive">Badge</Badge>
```
### Link
@@ -57,7 +57,7 @@ import { Badge } from "@/components/ui/badge"
You can use the `asChild` prop to make another component look like a badge. Here's an example of a link that looks like a badge.
```tsx showLineNumbers
import { Link } from "next/link"
import Link from "next/link"
import { Badge } from "@/components/ui/badge"

View File

@@ -180,7 +180,7 @@ To use a custom link component from your routing library, you can use the `asChi
/>
```tsx showLineNumbers {1,8-10}
import { Link } from "next/link"
import Link from "next/link"
...

View File

@@ -71,7 +71,20 @@ import { Button } from "@/components/ui/button"
<Button variant="outline">Button</Button>
```
---
## Cursor
Tailwind v4 [switched](https://tailwindcss.com/docs/upgrade-guide#buttons-use-the-default-cursor) from `cursor: pointer` to `cursor: default` for the button component.
If you want to keep the `cursor: pointer` behavior, add the following code to your CSS file:
```css showLineNumbers title="globals.css"
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
```
## Examples
@@ -265,7 +278,7 @@ To create a button group, use the `ButtonGroup` component. See the [Button Group
You can use the `asChild` prop to make another component look like a button. Here's an example of a link that looks like a button.
```tsx showLineNumbers
import { Link } from "next/link"
import Link from "next/link"
import { Button } from "@/components/ui/button"

View File

@@ -108,12 +108,40 @@ To use the Persian calendar, edit `components/ui/calendar.tsx` and replace `reac
description="A Persian calendar."
/>
## Selected Date (With TimeZone)
The Calendar component accepts a `timeZone` prop to ensure dates are displayed and selected in the user's local timezone.
```tsx showLineNumbers
export function CalendarWithTimezone() {
const [date, setDate] = React.useState<Date | undefined>(undefined)
const [timeZone, setTimeZone] = React.useState<string | undefined>(undefined)
React.useEffect(() => {
setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone)
}, [])
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
timeZone={timeZone}
/>
)
}
```
**Note:** If you notice a selected date offset (for example, selecting the 20th highlights the 19th), make sure the `timeZone` prop is set to the user's local timezone.
**Why client-side?** The timezone is detected using `Intl.DateTimeFormat().resolvedOptions().timeZone` inside a `useEffect` to ensure compatibility with server-side rendering. Detecting the timezone during render would cause hydration mismatches, as the server and client may be in different timezones.
## Examples
### Range Calendar
<ComponentPreview
name="calendar-02"
name="calendar-05"
title="Range Calendar"
description="A calendar showing the current date and range selection."
className="**:[.preview]:h-auto lg:**:[.preview]:h-[450px]"
@@ -153,9 +181,36 @@ This component uses the `chrono-node` library to parse natural language dates.
description="A calendar with natural language picker."
/>
### Form
### Custom Cell Size
<ComponentPreview name="calendar-form" />
<ComponentPreview
name="calendar-18"
title="Custom Cell Size"
description="A calendar with custom cell size that's responsive."
className="**:[.preview]:h-[560px]"
/>
You can customize the size of calendar cells using the `--cell-size` CSS variable. You can also make it responsive by using breakpoint-specific values:
```tsx showLineNumbers
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded-lg border [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/>
```
Or use fixed values:
```tsx showLineNumbers
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded-lg border [--cell-size:2.75rem] md:[--cell-size:3rem]"
/>
```
## Upgrade Guide
@@ -289,7 +344,10 @@ function Calendar({
defaultClassNames.week_number
),
day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day
),
range_start: cn(
@@ -417,3 +475,25 @@ npx shadcn@latest add calendar-02
```
This will install the latest version of the calendar blocks.
## Changelog
### 2025-10-26 Fixed day radius with week numbers
We have fixed an issue where the selected day's left border radius was not applied correctly when week numbers were displayed. The fix ensures that when `showWeekNumber` is enabled, the first day (which is the second child due to the week number column) correctly receives the rounded left border.
To apply this fix, edit `components/ui/calendar.tsx` and update the `day` class in `classNames`:
```tsx showLineNumbers title="components/ui/calendar.tsx" {5-7}
classNames={{
// ... other classNames
day: cn(
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day
),
// ... other classNames
}}
```

View File

@@ -31,7 +31,7 @@ We designed the `chart` component with composition in mind. **You build your cha
```tsx showLineNumbers /ChartContainer/ /ChartTooltipContent/
import { Bar, BarChart } from "recharts"
import { ChartContainer, ChartTooltipContent } from "@/components/ui/charts"
import { ChartContainer, ChartTooltipContent } from "@/components/ui/chart"
export function MyChart() {
return (
@@ -193,7 +193,7 @@ You can now build your chart using Recharts components.
<Callout className="mt-4 bg-amber-50 border-amber-200 dark:bg-amber-950/50 dark:border-amber-950">
**Important:** Remember to set a `min-h-[VALUE]` on the `ChartContainer` component. This is required for the chart be responsive.
**Important:** Remember to set a `min-h-[VALUE]` on the `ChartContainer` component. This is required for the chart to be responsive.
</Callout>
@@ -370,7 +370,7 @@ The chart config is where you define the labels, icons and colors for a chart.
It is intentionally decoupled from chart data.
This allows you to share config and color tokens between charts. It can also works independently for cases where your data or color tokens live remotely or in a different format.
This allows you to share config and color tokens between charts. It can also work independently for cases where your data or color tokens live remotely or in a different format.
```tsx showLineNumbers /ChartConfig/
import { Monitor } from "lucide-react"
@@ -394,7 +394,7 @@ const chartConfig = {
## Theming
Charts has built-in support for theming. You can use css variables (recommended) or color values in any color format, such as hex, hsl or oklch.
Charts have built-in support for theming. You can use css variables (recommended) or color values in any color format, such as hex, hsl or oklch.
### CSS Variables

View File

@@ -56,9 +56,3 @@ import { Checkbox } from "@/components/ui/checkbox"
```tsx
<Checkbox />
```
## Examples
### Form
<ComponentPreview name="checkbox-form-multiple" />

View File

@@ -143,7 +143,3 @@ export function ExampleCombobox() {
You can create a responsive combobox by using the `<Popover />` on desktop and the `<Drawer />` components on mobile.
<ComponentPreview name="combobox-responsive" />
### Form
<ComponentPreview name="combobox-form" />

View File

@@ -94,7 +94,3 @@ This component uses the `chrono-node` library to parse natural language dates.
title="Natural Language Picker"
description="A calendar with natural language picker."
/>
### Form
<ComponentPreview name="date-picker-form" />

View File

@@ -10,7 +10,7 @@ links:
## About
Drawer is built on top of [Vaul](https://github.com/emilkowalski/vaul) by [emilkowalski\_](https://twitter.com/emilkowalski_).
Drawer is built on top of [Vaul](https://github.com/emilkowalski/vaul) by [emilkowalski](https://twitter.com/emilkowalski).
## Installation

View File

@@ -93,3 +93,19 @@ import {
name="dropdown-menu-radio-group"
description="A dropdown menu with radio items."
/>
### Dialog
This example shows how to open a dialog from a dropdown menu.
Use `modal={false}` on the `DropdownMenu` component.
```tsx showLineNumbers
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="outline">Actions</Button>
</DropdownMenuTrigger>
</DropdownMenu>
```
<ComponentPreview name="dropdown-menu-dialog" />

View File

@@ -1,6 +1,6 @@
---
title: Empty
description: Use the Empty component to display a empty state.
description: Use the Empty component to display an empty state.
component: true
---
@@ -57,9 +57,9 @@ import {
<EmptyMedia variant="icon">
<Icon />
</EmptyMedia>
<EmptyTitle>No data</EmptyTitle>
<EmptyDescription>No data found</EmptyDescription>
</EmptyHeader>
<EmptyTitle>No data</EmptyTitle>
<EmptyDescription>No data found</EmptyDescription>
<EmptyContent>
<Button>Add data</Button>
</EmptyContent>
@@ -70,7 +70,7 @@ import {
### Outline
Use the `border` utility class to create a outline empty state.
Use the `border` utility class to create an outline empty state.
<ComponentPreview
name="empty-outline"

View File

@@ -98,6 +98,10 @@ The `Field` family is designed for composing accessible forms. A typical field i
- `FieldContent` is a flex column that groups label and description. Not required if you have no description.
- Wrap related fields with `FieldGroup`, and use `FieldSet` with `FieldLegend` for semantic grouping.
## Form
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form) or [Tanstack Form](/docs/forms/tanstack-form).
## Examples
### Input

View File

@@ -1,10 +1,18 @@
---
title: React Hook Form
title: Form
description: Building forms with React Hook Form and Zod.
links:
doc: https://react-hook-form.com
---
import { InfoIcon } from "lucide-react"
<Callout icon={<InfoIcon />} title="We are not actively developing this component anymore.">
The Form component is an abstraction over the `react-hook-form` library. Going forward, we recommend using the [`<Field />`](/docs/components/field) component to build forms. See the [Form](/docs/forms) documentation for more information.
</Callout>
Forms are tricky. They are one of the most common things you'll build in a web application, but also one of the most complex.
Well-designed HTML forms are:
@@ -119,8 +127,6 @@ npm install @radix-ui/react-label @radix-ui/react-slot react-hook-form @hookform
## Usage
<Steps>
### Create a form schema
Define the shape of your form using a Zod schema. You can read more about using Zod in the [Zod documentation](https://zod.dev).
@@ -233,23 +239,3 @@ export function ProfileForm() {
### Done
That's it. You now have a fully accessible form that is type-safe with client-side validation.
<ComponentPreview
name="input-form"
className="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden"
/>
</Steps>
## Examples
See the following links for more examples on how to use the `<Form />` component with other components:
- [Checkbox](/docs/components/checkbox#form)
- [Date Picker](/docs/components/date-picker#form)
- [Input](/docs/components/input#form)
- [Radio Group](/docs/components/radio-group#form)
- [Select](/docs/components/select#form)
- [Switch](/docs/components/switch#form)
- [Textarea](/docs/components/textarea#form)
- [Combobox](/docs/components/combobox#form)

View File

@@ -4,3 +4,7 @@ description: Here you can find all the components available in the library. We a
---
<ComponentsList />
---
Can't find what you need? Try the [registry directory](/docs/directory) for community-maintained components.

View File

@@ -253,3 +253,9 @@ All other props are passed through to the underlying `<Textarea />` component.
</InputGroupAddon>
</InputGroup>
```
## Changelog
### 2025-10-06 `InputGroup`
Add the `min-w-0` class to the `InputGroup` component. See [diff](https://github.com/shadcn-ui/ui/pull/8341/files#diff-0e2ee95d0050ca4c5d82339df86c54e14a6739dc4638fdda0eec8f73aebc2da9).

View File

@@ -94,10 +94,6 @@ import { Input } from "@/components/ui/input"
description="An input component with a button."
/>
### Form
<ComponentPreview name="input-form" />
## Changelog
### 2025-09-18 Remove `flex` class

View File

@@ -0,0 +1,205 @@
---
title: Native Select
description: A styled native HTML select element with consistent design system integration.
component: true
---
import { InfoIcon } from "lucide-react"
<Callout variant="info" icon={<InfoIcon className="!translate-y-[3px]" />}>
For a styled select component, see the [Select](/docs/components/select)
component.
</Callout>
<ComponentPreview name="native-select-demo" />
## Installation
<CodeTabs>
<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">
```bash
npx shadcn@latest add native-select
```
</TabsContent>
<TabsContent value="manual">
<Steps>
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource name="native-select" title="components/ui/native-select.tsx" />
<Step>Update the import paths to match your project setup.</Step>
</Steps>
</TabsContent>
</CodeTabs>
## Usage
```tsx showLineNumbers
import {
NativeSelect,
NativeSelectOptGroup,
NativeSelectOption,
} from "@/components/ui/native-select"
```
```tsx showLineNumbers
<NativeSelect>
<NativeSelectOption value="">Select a fruit</NativeSelectOption>
<NativeSelectOption value="apple">Apple</NativeSelectOption>
<NativeSelectOption value="banana">Banana</NativeSelectOption>
<NativeSelectOption value="blueberry">Blueberry</NativeSelectOption>
<NativeSelectOption value="grapes" disabled>
Grapes
</NativeSelectOption>
<NativeSelectOption value="pineapple">Pineapple</NativeSelectOption>
</NativeSelect>
```
## Examples
### With Groups
Organize options using `NativeSelectOptGroup` for better categorization.
<ComponentPreview name="native-select-groups" />
```tsx showLineNumbers
<NativeSelect>
<NativeSelectOption value="">Select a food</NativeSelectOption>
<NativeSelectOptGroup label="Fruits">
<NativeSelectOption value="apple">Apple</NativeSelectOption>
<NativeSelectOption value="banana">Banana</NativeSelectOption>
<NativeSelectOption value="blueberry">Blueberry</NativeSelectOption>
</NativeSelectOptGroup>
<NativeSelectOptGroup label="Vegetables">
<NativeSelectOption value="carrot">Carrot</NativeSelectOption>
<NativeSelectOption value="broccoli">Broccoli</NativeSelectOption>
<NativeSelectOption value="spinach">Spinach</NativeSelectOption>
</NativeSelectOptGroup>
</NativeSelect>
```
### Disabled State
Disable individual options or the entire select component.
<ComponentPreview name="native-select-disabled" />
### Invalid State
Show validation errors with the `aria-invalid` attribute and error styling.
<ComponentPreview name="native-select-invalid" />
```tsx showLineNumbers
<NativeSelect aria-invalid="true">
<NativeSelectOption value="">Select a country</NativeSelectOption>
<NativeSelectOption value="us">United States</NativeSelectOption>
<NativeSelectOption value="uk">United Kingdom</NativeSelectOption>
<NativeSelectOption value="ca">Canada</NativeSelectOption>
</NativeSelect>
```
### Form Integration
Use with form libraries like React Hook Form for controlled components.
<ComponentPreview name="native-select-form" />
### Input Group Integration
Combine with `InputGroup` for complex input layouts.
<ComponentPreview name="native-select-input-group" />
## Native Select vs Select
- Use `NativeSelect` when you need native browser behavior, better performance, or mobile-optimized dropdowns.
- Use `Select` when you need custom styling, animations, or complex interactions.
The `NativeSelect` component provides native HTML select functionality with consistent styling that matches your design system.
## Accessibility
- The component maintains all native HTML select accessibility features.
- Screen readers can navigate through options using arrow keys.
- The chevron icon is marked as `aria-hidden="true"` to avoid duplication.
- Use `aria-label` or `aria-labelledby` for additional context when needed.
```tsx showLineNumbers
<NativeSelect aria-label="Choose your preferred language">
<NativeSelectOption value="en">English</NativeSelectOption>
<NativeSelectOption value="es">Spanish</NativeSelectOption>
<NativeSelectOption value="fr">French</NativeSelectOption>
</NativeSelect>
```
## API Reference
### NativeSelect
The main select component that wraps the native HTML select element.
| Prop | Type | Default |
| ----------- | -------- | ------- |
| `className` | `string` | |
All other props are passed through to the underlying `<select>` element.
```tsx
<NativeSelect>
<NativeSelectOption value="option1">Option 1</NativeSelectOption>
<NativeSelectOption value="option2">Option 2</NativeSelectOption>
</NativeSelect>
```
### NativeSelectOption
Represents an individual option within the select.
| Prop | Type | Default |
| ----------- | --------- | ------- |
| `value` | `string` | |
| `disabled` | `boolean` | `false` |
| `className` | `string` | |
All other props are passed through to the underlying `<option>` element.
```tsx
<NativeSelectOption value="apple">Apple</NativeSelectOption>
<NativeSelectOption value="banana" disabled>
Banana
</NativeSelectOption>
```
### NativeSelectOptGroup
Groups related options together for better organization.
| Prop | Type | Default |
| ----------- | --------- | ------- |
| `label` | `string` | |
| `disabled` | `boolean` | `false` |
| `className` | `string` | |
All other props are passed through to the underlying `<optgroup>` element.
```tsx
<NativeSelectOptGroup label="Fruits">
<NativeSelectOption value="apple">Apple</NativeSelectOption>
<NativeSelectOption value="banana">Banana</NativeSelectOption>
</NativeSelectOptGroup>
```

View File

@@ -7,7 +7,10 @@ links:
api: https://www.radix-ui.com/docs/primitives/components/navigation-menu#api-reference
---
<ComponentPreview name="navigation-menu-demo" />
<ComponentPreview
name="navigation-menu-demo"
className="[&_.preview]:!items-start [&_.preview]:p-4 [&_.preview]:pt-8 md:[&_.preview]:pt-16"
/>
## Installation
@@ -83,7 +86,7 @@ import {
You can use the `asChild` prop to make another component look like a navigation menu trigger. Here's an example of a link that looks like a navigation menu trigger.
```tsx showLineNumbers title="components/example-navigation-menu.tsx"
import { Link } from "next/link"
import Link from "next/link"
export function NavigationMenuDemo() {
return (

View File

@@ -69,9 +69,3 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
</div>
</RadioGroup>
```
## Examples
### Form
<ComponentPreview name="radio-group-form" />

View File

@@ -84,7 +84,3 @@ import {
name="select-scrollable"
description="A select component with a scrollable list of options."
/>
### Form
<ComponentPreview name="select-form" />

View File

@@ -10,7 +10,7 @@ links:
## About
Sonner is built and maintained by [emilkowalski\_](https://twitter.com/emilkowalski_).
Sonner is built and maintained by [emilkowalski](https://twitter.com/emilkowalski).
## Installation
@@ -68,7 +68,7 @@ npm install sonner next-themes
<Step>Add the Toaster component</Step>
```tsx title="app/layout.tsx" {1,9}
```tsx showLineNumbers title="app/layout.tsx" {1,8}
import { Toaster } from "@/components/ui/sonner"
export default function RootLayout({ children }) {
@@ -76,8 +76,8 @@ export default function RootLayout({ children }) {
<html lang="en">
<head />
<body>
<main>{children}</main>
<Toaster />
<main>{children}</main>
</body>
</html>
)
@@ -99,3 +99,56 @@ import { toast } from "sonner"
```tsx
toast("Event has been created.")
```
## Examples
<ComponentPreview name="sonner-types" />
## Changelog
### 2025-10-13 Icons
We've updated the Sonner component to use icons from `lucide`. Update your `sonner.tsx` file to use the new icons.
```tsx showLineNumbers title="components/ui/sonner.tsx" {3-9,20-26}
"use client"
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }
```

View File

@@ -56,9 +56,3 @@ import { Switch } from "@/components/ui/switch"
```tsx
<Switch />
```
## Examples
### Form
<ComponentPreview name="switch-form" />

View File

@@ -79,7 +79,3 @@ import { Textarea } from "@/components/ui/textarea"
name="textarea-with-button"
description="A textarea with a button"
/>
### Form
<ComponentPreview name="textarea-form" />

View File

@@ -7,10 +7,7 @@ links:
api: https://www.radix-ui.com/docs/primitives/components/toggle-group#api-reference
---
<ComponentPreview
name="toggle-group-demo"
description="A toggle group with three items."
/>
<ComponentPreview name="toggle-group-spacing" />
## Installation
@@ -66,13 +63,6 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
## Examples
### Default
<ComponentPreview
name="toggle-group-demo"
description="A toggle group with three items."
/>
### Outline
<ComponentPreview
@@ -107,3 +97,42 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
name="toggle-group-disabled"
description="A disabled toggle group."
/>
### Spacing
Use `spacing={2}` to add spacing between toggle group items.
<ComponentPreview
name="toggle-group-spacing"
description="A toggle group with spacing."
/>
## API Reference
### ToggleGroup
The main component that wraps toggle group items.
| Prop | Type | Default |
| ----------- | --------------------------- | ----------- |
| `type` | `"single" \| "multiple"` | `"single"` |
| `variant` | `"default" \| "outline"` | `"default"` |
| `size` | `"default" \| "sm" \| "lg"` | `"default"` |
| `spacing` | `number` | `0` |
| `className` | `string` | |
```tsx
<ToggleGroup type="single" variant="outline" size="sm">
<ToggleGroupItem value="a">A</ToggleGroupItem>
<ToggleGroupItem value="b">B</ToggleGroupItem>
</ToggleGroup>
```
### ToggleGroupItem
Individual toggle items within a toggle group. Remember to add an `aria-label` to each item for accessibility.
| Prop | Type | Default |
| ----------- | -------- | -------- |
| `value` | `string` | Required |
| `className` | `string` | |

View File

@@ -0,0 +1,45 @@
---
title: Forms
description: Build forms with React and shadcn/ui.
---
import { ClipboardListIcon, InfoIcon } from "lucide-react"
## Pick Your Framework
Start by selecting your framework. Then follow the instructions to learn how to build forms with shadcn/ui and the form library of your choice.
<div className="mt-8 grid gap-4 sm:grid-cols-2 sm:gap-6">
<LinkedCard href="/docs/forms/react-hook-form">
<ClipboardListIcon className="size-10" />
<p className="mt-2 font-medium">React Hook Form</p>
</LinkedCard>
<LinkedCard href="/docs/forms/tanstack-form">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="size-10"
fill="currentColor"
>
<path d="M6.93 13.688a.343.343 0 0 1 .468.132l.063.106c.48.851.98 1.66 1.5 2.426a35.65 35.65 0 0 0 2.074 2.742.345.345 0 0 1-.039.484l-.074.066c-2.543 2.223-4.191 2.665-4.953 1.333-.746-1.305-.477-3.672.808-7.11a.344.344 0 0 1 .153-.18ZM17.75 16.3a.34.34 0 0 1 .395.27l.02.1c.628 3.286.187 4.93-1.325 4.93-1.48 0-3.36-1.402-5.649-4.203a.327.327 0 0 1-.074-.222c0-.188.156-.34.344-.34h.121a32.984 32.984 0 0 0 2.809-.098c1.07-.086 2.191-.23 3.359-.437zm.871-6.977a.353.353 0 0 1 .445-.21l.102.034c3.262 1.11 4.504 2.332 3.719 3.664-.766 1.305-2.993 2.254-6.684 2.848a.362.362 0 0 1-.238-.047.343.343 0 0 1-.125-.476l.062-.106a34.07 34.07 0 0 0 1.367-2.523c.477-.989.93-2.051 1.352-3.184zM7.797 8.34a.362.362 0 0 1 .238.047.343.343 0 0 1 .125.476l-.062.106a34.088 34.088 0 0 0-1.367 2.523c-.477.988-.93 2.051-1.352 3.184a.353.353 0 0 1-.445.21l-.102-.034C1.57 13.742.328 12.52 1.113 11.188 1.88 9.883 4.106 8.934 7.797 8.34Zm5.281-3.984c2.543-2.223 4.192-2.664 4.953-1.332.746 1.304.477 3.671-.808 7.109a.344.344 0 0 1-.153.18.343.343 0 0 1-.468-.133l-.063-.106a34.64 34.64 0 0 0-1.5-2.426 35.65 35.65 0 0 0-2.074-2.742.345.345 0 0 1 .039-.484ZM7.285 2.274c1.48 0 3.364 1.402 5.649 4.203a.349.349 0 0 1 .078.218.348.348 0 0 1-.348.344l-.117-.004a34.584 34.584 0 0 0-2.809.102 35.54 35.54 0 0 0-3.363.437.343.343 0 0 1-.394-.273l-.02-.098c-.629-3.285-.188-4.93 1.324-4.93Zm2.871 5.812h3.688a.638.638 0 0 1 .55.316l1.848 3.22a.644.644 0 0 1 0 .628l-1.847 3.223a.638.638 0 0 1-.551.316h-3.688a.627.627 0 0 1-.547-.316L7.758 12.25a.644.644 0 0 1 0-.629L9.61 8.402a.627.627 0 0 1 .546-.316Zm3.23.793a.638.638 0 0 1 .552.316l1.39 2.426a.644.644 0 0 1 0 .629l-1.39 2.43a.638.638 0 0 1-.551.316h-2.774a.627.627 0 0 1-.546-.316l-1.395-2.43a.644.644 0 0 1 0-.629l1.395-2.426a.627.627 0 0 1 .546-.316Zm-.491.867h-1.79a.624.624 0 0 0-.546.316l-.899 1.56a.644.644 0 0 0 0 .628l.899 1.563a.632.632 0 0 0 .547.316h1.789a.632.632 0 0 0 .547-.316l.898-1.563a.644.644 0 0 0 0-.629l-.898-1.558a.624.624 0 0 0-.547-.317Zm-.477.828c.227 0 .438.121.547.317l.422.73a.625.625 0 0 1 0 .629l-.422.734a.627.627 0 0 1-.547.317h-.836a.632.632 0 0 1-.547-.317l-.422-.734a.625.625 0 0 1 0-.629l.422-.73a.632.632 0 0 1 .547-.317zm-.418.817a.548.548 0 0 0-.473.273.547.547 0 0 0 0 .547.544.544 0 0 0 .473.27.544.544 0 0 0 .473-.27.547.547 0 0 0 0-.547.548.548 0 0 0-.473-.273Zm-4.422.546h.98M18.98 7.75c.391-1.895.477-3.344.223-4.398-.148-.63-.422-1.137-.84-1.508-.441-.39-1-.582-1.625-.582-1.035 0-2.12.472-3.281 1.367a14.9 14.9 0 0 0-1.473 1.316 1.206 1.206 0 0 0-.136-.144c-1.446-1.285-2.66-2.082-3.7-2.39-.617-.184-1.195-.2-1.722-.024-.559.187-1.004.574-1.317 1.117-.515.894-.652 2.074-.46 3.527.078.59.214 1.235.402 1.934a1.119 1.119 0 0 0-.215.047C3.008 8.62 1.71 9.269.926 10.015c-.465.442-.77.938-.883 1.481-.113.578 0 1.156.312 1.7.516.894 1.465 1.597 2.817 2.155.543.223 1.156.426 1.844.61a1.023 1.023 0 0 0-.07.226c-.391 1.891-.477 3.344-.223 4.395.148.629.425 1.14.84 1.508.44.39 1 .582 1.625.582 1.035 0 2.12-.473 3.28-1.364.477-.37.973-.816 1.489-1.336a1.2 1.2 0 0 0 .195.227c1.446 1.285 2.66 2.082 3.7 2.39.617.184 1.195.2 1.722.024.559-.187 1.004-.574 1.317-1.117.515-.894.652-2.074.46-3.527a14.941 14.941 0 0 0-.425-2.012 1.225 1.225 0 0 0 .238-.047c1.828-.61 3.125-1.258 3.91-2.004.465-.441.77-.937.883-1.48.113-.578 0-1.157-.313-1.7-.515-.894-1.464-1.597-2.816-2.156a14.576 14.576 0 0 0-1.906-.625.865.865 0 0 0 .059-.195z" />
</svg>
<p className="mt-2 font-medium">TanStack Form</p>
</LinkedCard>
<LinkedCard href="#" className="border border-dashed bg-transparent">
<svg
role="img"
viewBox="0 0 24 24"
className="size-10"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>React</title>
<path
d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"
/>
</svg>
<p className="mt-2 font-medium">useActionState</p>
<p className="text-muted-foreground mt-1 text-xs">(Coming Soon)</p>
</LinkedCard>
</div>

View File

@@ -0,0 +1,3 @@
{
"pages": ["react-hook-form", "tanstack-form"]
}

View File

@@ -0,0 +1,397 @@
---
title: Next.js
description: Build forms in React using useActionState and Server Actions.
---
import { InfoIcon } from "lucide-react"
In this guide, we will take a look at building forms with Next.js using `useActionState` and Server Actions. We'll cover building forms, validation, pending states, accessibility, and more.
## Demo
We are going to build the following form with a simple text input and a textarea. On submit, we'll use a server action to validate the form data and update the form state.
<ComponentPreview
name="form-next-demo"
className="[&_.preview]:h-[700px] [&_pre]:!h-[700px]"
/>
<Callout icon={<InfoIcon />}>
**Note:** The examples on this page intentionally disable browser validation
to show how schema validation and form errors work in server actions.
</Callout>
## Approach
This form leverages Next.js and React's built-in capabilities for form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
- Uses Next.js `<Form />` component for navigation and progressive enhancement.
- `<Field />` components for building accessible forms.
- `useActionState` for managing form state and errors.
- Handles loading states with pending prop.
- Server Actions for handling form submissions.
- Server-side validation using Zod.
## Anatomy
Here's a basic example of a form using the `<Field />` component.
```tsx showLineNumbers
<Form action={formAction}>
<FieldGroup>
<Field data-invalid={!!formState.errors?.title?.length}>
<FieldLabel htmlFor="title">Bug Title</FieldLabel>
<Input
id="title"
name="title"
defaultValue={formState.values.title}
disabled={pending}
aria-invalid={!!formState.errors?.title?.length}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{formState.errors?.title && (
<FieldError>{formState.errors.title[0]}</FieldError>
)}
</Field>
</FieldGroup>
<Button type="submit">Submit</Button>
</Form>
```
## Usage
### Create a form schema
We'll start by defining the shape of our form using a Zod schema in a `schema.ts` file.
<Callout icon={<InfoIcon />}>
**Note:** This example uses `zod v3` for schema validation, but you can
replace it with any other schema validation library. Make sure your schema
library conforms to the Standard Schema specification.
</Callout>
```tsx showLineNumbers title="schema.ts"
import { z } from "zod"
export const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
```
### Define the form state type
Next, we'll create a type for our form state that includes values, errors, and success status. This will be used to type the form state on the client and server.
```tsx showLineNumbers title="schema.ts"
import { z } from "zod"
export type FormState = {
values?: z.infer<typeof formSchema>
errors: null | Partial<Record<keyof z.infer<typeof formSchema>, string[]>>
success: boolean
}
```
**Important:** We define the schema and the `FormState` type in a separate file so we can import them into both the client and server components.
### Create the Server Action
A server action is a function that runs on the server and can be called from the client. We'll use it to validate the form data and update the form state.
<ComponentSource
src="/registry/new-york-v4/examples/form-next-demo-action.ts"
title="actions.ts"
/>
**Note:** We're returning `values` for error cases. This is because we want to keep the user submitted values in the form state. For success cases, we're returning empty values to reset the form.
### Build the form
We can now build the form using the `<Field />` component. We'll use the `useActionState` hook to manage the form state, server action, and pending state.
<ComponentSource
src="/registry/new-york-v4/examples/form-next-demo.tsx"
title="form.tsx"
/>
### Done
That's it. You now have a fully accessible form with client and server-side validation.
When you submit the form, the `formAction` function will be called on the server. The server action will validate the form data and update the form state.
If the form data is invalid, the server action will return the errors to the client. If the form data is valid, the server action will return the success status and update the form state.
## Pending States
Use the `pending` prop from `useActionState` to show loading indicators and disable form inputs.
```tsx showLineNumbers {11,26-34}
"use client"
import * as React from "react"
import Form from "next/form"
import { Spinner } from "@/components/ui/spinner"
import { bugReportFormAction } from "./actions"
export function BugReportForm() {
const [formState, formAction, pending] = React.useActionState(
bugReportFormAction,
{
errors: null,
success: false,
}
)
return (
<Form action={formAction}>
<FieldGroup>
<Field data-disabled={pending}>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input id="name" name="name" disabled={pending} />
</Field>
<Field>
<Button type="submit" disabled={pending}>
{pending && <Spinner />} Submit
</Button>
</Field>
</FieldGroup>
</Form>
)
}
```
## Disabled States
### Submit Button
To disable the submit button, use the `pending` prop on the button's `disabled` prop.
```tsx showLineNumbers
<Button type="submit" disabled={pending}>
{pending && <Spinner />} Submit
</Button>
```
### Field
To apply a disabled state and styling to a `<Field />` component, use the `data-disabled` prop on the `<Field />` component.
```tsx showLineNumbers
<Field data-disabled={pending}>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input id="name" name="name" disabled={pending} />
</Field>
```
## Validation
### Server-side Validation
Use `safeParse()` on your schema in your server action to validate the form data.
```tsx showLineNumbers title="actions.ts" {12-20}
"use server"
export async function bugReportFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
const result = formSchema.safeParse(values)
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
return {
errors: null,
success: true,
}
}
```
### Business Logic Validation
You can add additional custom validation logic in your server action.
Make sure to return the values on validation errors. This is to ensure that the form state maintains the user's input.
```tsx showLineNumbers title="actions.ts" {22-35}
"use server"
export async function bugReportFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
const result = formSchema.safeParse(values)
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// Check if email already exists in database.
const existingUser = await db.user.findUnique({
where: { email: result.data.email },
})
if (existingUser) {
return {
values,
success: false,
errors: {
email: ["This email is already registered"],
},
}
}
return {
errors: null,
success: true,
}
}
```
## Displaying Errors
Display errors next to the field using `<FieldError />`. Make sure to add the `data-invalid` prop to the `<Field />` component and `aria-invalid` prop to the input.
```tsx showLineNumbers
<Field data-invalid={!!formState.errors?.email?.length}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
name="email"
type="email"
aria-invalid={!!formState.errors?.email?.length}
/>
{formState.errors?.email && (
<FieldError>{formState.errors.email[0]}</FieldError>
)}
</Field>
```
## Resetting the Form
When you submit a form with a server action, React will automatically reset the form state to the initial values.
### Reset on Success
To reset the form on success, you can omit the `values` from the server action and React will automatically reset the form state to the initial values. This is standard React behavior.
```tsx showLineNumbers title="actions.ts" {22-26}
export async function demoFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
// Validation.
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// Business logic.
callYourDatabaseOrAPI(values)
// Omit the values on success to reset the form state.
return {
errors: null,
success: true,
}
}
```
### Preserve on Validation Errors
To prevent the form from being reset on failure, you can return the values in the server action. This is to ensure that the form state maintains the user's input.
```tsx showLineNumbers title="actions.ts" {12-17}
export async function demoFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
// Validation.
if (!result.success) {
return {
// Return the values on validation errors.
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
}
```
## Complex Forms
Here is an example of a more complex form with multiple fields and validation.
<ComponentPreview
name="form-next-complex"
className="[&_.preview]:h-[1100px] [&_pre]:!h-[1100px]"
hideCode
/>
### Schema
<ComponentSource
src="/registry/new-york-v4/examples/form-next-complex-schema.ts"
title="schema.ts"
/>
### Form
<ComponentSource
src="/registry/new-york-v4/examples/form-next-complex.tsx"
title="form.tsx"
/>
### Server Action
<ComponentSource
src="/registry/new-york-v4/examples/form-next-complex-action.ts"
title="actions.ts"
/>

View File

@@ -0,0 +1,629 @@
---
title: React Hook Form
description: Build forms in React using React Hook Form and Zod.
links:
doc: https://react-hook-form.com
---
import { InfoIcon } from "lucide-react"
In this guide, we will take a look at building forms with React Hook Form. We'll cover building forms with the `<Field />` component, adding schema validation using Zod, error handling, accessibility, and more.
## Demo
We are going to build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
<Callout icon={<InfoIcon />}>
**Note:** For the purpose of this demo, we have intentionally disabled browser
validation to show how schema validation and form errors work in React Hook
Form. It is recommended to add basic browser validation in your production
code.
</Callout>
<ComponentPreview
name="form-rhf-demo"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
## Approach
This form leverages React Hook Form for performant, flexible form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
- Uses React Hook Form's `useForm` hook for form state management.
- `<Controller />` component for controlled inputs.
- `<Field />` components for building accessible forms.
- Client-side validation using Zod with `zodResolver`.
## Anatomy
Here's a basic example of a form using the `<Controller />` component from React Hook Form and the `<Field />` component.
```tsx showLineNumbers {5-18}
<Controller
name="title"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
<Input
{...field}
id={field.name}
aria-invalid={fieldState.invalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
## Form
### Create a form schema
We'll start by defining the shape of our form using a Zod schema
<Callout icon={<InfoIcon />}>
**Note:** This example uses `zod v3` for schema validation, but you can
replace it with any other Standard Schema validation library supported by
React Hook Form.
</Callout>
```tsx showLineNumbers title="form.tsx"
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
```
### Setup the form
Next, we'll use the `useForm` hook from React Hook Form to create our form instance. We'll also add the Zod resolver to validate the form data.
```tsx showLineNumbers title="form.tsx" {17-23}
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
export function BugReportForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
description: "",
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
// Do something with the form values.
console.log(data)
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* ... */}
{/* Build the form here */}
{/* ... */}
</form>
)
}
```
### Build the form
We can now build the form using the `<Controller />` component from React Hook Form and the `<Field />` component.
<ComponentSource
src="/registry/new-york-v4/examples/form-rhf-demo.tsx"
title="form.tsx"
/>
### Done
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the `onSubmit` function will be called with the validated form data. If the form data is invalid, React Hook Form will display the errors next to each field.
## Validation
### Client-side Validation
React Hook Form validates your form data using the Zod schema. Define a schema and pass it to the `resolver` option of the `useForm` hook.
```tsx showLineNumbers title="example-form.tsx" {5-8,12}
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
const formSchema = z.object({
title: z.string(),
description: z.string().optional(),
})
export function ExampleForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
description: "",
},
})
}
```
### Validation Modes
React Hook Form supports different validation modes.
```tsx showLineNumbers title="form.tsx" {3}
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: "onChange",
})
```
| Mode | Description |
| ------------- | -------------------------------------------------------- |
| `"onChange"` | Validation triggers on every change. |
| `"onBlur"` | Validation triggers on blur. |
| `"onSubmit"` | Validation triggers on submit (default). |
| `"onTouched"` | Validation triggers on first blur, then on every change. |
| `"all"` | Validation triggers on blur and change. |
## Displaying Errors
Display errors next to the field using `<FieldError />`. For styling and accessibility:
- Add the `data-invalid` prop to the `<Field />` component.
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
```tsx showLineNumbers title="form.tsx" {5,11,13}
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
{...field}
id={field.name}
type="email"
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
## Working with Different Field Types
### Input
- For input fields, spread the `field` object onto the `<Input />` component.
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-rhf-input"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
For simple text inputs, spread the `field` object onto the input.
```tsx showLineNumbers title="form.tsx" {5,7,8}
<Controller
name="name"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
<Input {...field} id={field.name} aria-invalid={fieldState.invalid} />
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
### Textarea
- For textarea fields, spread the `field` object onto the `<Textarea />` component.
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-rhf-textarea"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
For textarea fields, spread the `field` object onto the textarea.
```tsx showLineNumbers title="form.tsx" {5,10,18}
<Controller
name="about"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-textarea-about">More about you</FieldLabel>
<Textarea
{...field}
id="form-rhf-textarea-about"
aria-invalid={fieldState.invalid}
placeholder="I'm a software engineer..."
className="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us personalize
your experience.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
### Select
- For select components, use `field.value` and `field.onChange` on the `<Select />` component.
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-rhf-select"
className="sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {5,13,22}
<Controller
name="language"
control={form.control}
render={({ field, fieldState }) => (
<Field orientation="responsive" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="form-rhf-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldContent>
<Select
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger
id="form-rhf-select-language"
aria-invalid={fieldState.invalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</Field>
)}
/>
```
### Checkbox
- For checkbox arrays, use `field.value` and `field.onChange` with array manipulation.
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
<ComponentPreview
name="form-rhf-checkbox"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {10,15,20-22,38}
<Controller
name="tasks"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you&apos;ve created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={fieldState.invalid}
>
<Checkbox
id={`form-rhf-checkbox-${task.id}`}
name={field.name}
aria-invalid={fieldState.invalid}
checked={field.value.includes(task.id)}
onCheckedChange={(checked) => {
const newValue = checked
? [...field.value, task.id]
: field.value.filter((value) => value !== task.id)
field.onChange(newValue)
}}
/>
<FieldLabel
htmlFor={`form-rhf-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldSet>
)}
/>
```
### Radio Group
- For radio groups, use `field.value` and `field.onChange` on the `<RadioGroup />` component.
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-rhf-radiogroup"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {12-13,17,25,31}
<Controller
name="plan"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
{plans.map((plan) => (
<FieldLabel key={plan.id} htmlFor={`form-rhf-radiogroup-${plan.id}`}>
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-rhf-radiogroup-${plan.id}`}
aria-invalid={fieldState.invalid}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldSet>
)}
/>
```
### Switch
- For switches, use `field.value` and `field.onChange` on the `<Switch />` component.
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-rhf-switch"
className="sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {5,13,18-19}
<Controller
name="twoFactor"
control={form.control}
render={({ field, fieldState }) => (
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="form-rhf-switch-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldContent>
<Switch
id="form-rhf-switch-twoFactor"
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
aria-invalid={fieldState.invalid}
/>
</Field>
)}
/>
```
### Complex Forms
Here is an example of a more complex form with multiple fields and validation.
<ComponentPreview
name="form-rhf-complex"
className="sm:[&_.preview]:h-[1300px] sm:[&_pre]:!h-[1300px]"
chromeLessOnMobile
/>
## Resetting the Form
Use `form.reset()` to reset the form to its default values.
```tsx showLineNumbers
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
```
## Array Fields
React Hook Form provides a `useFieldArray` hook for managing dynamic array fields. This is useful when you need to add or remove fields dynamically.
<ComponentPreview
name="form-rhf-array"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
### Using useFieldArray
Use the `useFieldArray` hook to manage array fields. It provides `fields`, `append`, and `remove` methods.
```tsx showLineNumbers title="form.tsx" {8-11}
import { useFieldArray, useForm } from "react-hook-form"
export function ExampleForm() {
const form = useForm({
// ... form config
})
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "emails",
})
}
```
### Array Field Structure
Wrap your array fields in a `<FieldSet />` with a `<FieldLegend />` and `<FieldDescription />`.
```tsx showLineNumbers title="form.tsx"
<FieldSet className="gap-4">
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
</FieldSet>
```
### Controller Pattern for Array Items
Map over the `fields` array and use `<Controller />` for each item. **Make sure to use `field.id` as the key**.
```tsx showLineNumbers title="form.tsx"
{
fields.map((field, index) => (
<Controller
key={field.id}
name={`emails.${index}.address`}
control={form.control}
render={({ field: controllerField, fieldState }) => (
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
<FieldContent>
<InputGroup>
<InputGroupInput
{...controllerField}
id={`form-rhf-array-email-${index}`}
aria-invalid={fieldState.invalid}
placeholder="name@example.com"
type="email"
autoComplete="email"
/>
{/* Remove button */}
</InputGroup>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldContent>
</Field>
)}
/>
))
}
```
### Adding Items
Use the `append` method to add new items to the array.
```tsx showLineNumbers title="form.tsx"
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ address: "" })}
disabled={fields.length >= 5}
>
Add Email Address
</Button>
```
### Removing Items
Use the `remove` method to remove items from the array. Add the remove button conditionally.
```tsx showLineNumbers title="form.tsx"
{
fields.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => remove(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)
}
```
### Array Validation
Use Zod's `array` method to validate array fields.
```tsx showLineNumbers title="form.tsx"
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("Enter a valid email address."),
})
)
.min(1, "Add at least one email address.")
.max(5, "You can add up to 5 email addresses."),
})
```

View File

@@ -0,0 +1,698 @@
---
title: TanStack Form
description: Build forms in React using TanStack Form and Zod.
links:
doc: https://tanstack.com/form
---
import { InfoIcon } from "lucide-react"
This guide explores how to build forms using TanStack Form. You'll learn to create forms with the `<Field />` component, implement schema validation with Zod, handle errors, and ensure accessibility.
## Demo
We'll start by building the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
<Callout icon={<InfoIcon />}>
**Note:** For the purpose of this demo, we have intentionally disabled browser
validation to show how schema validation and form errors work in TanStack
Form. It is recommended to add basic browser validation in your production
code.
</Callout>
<ComponentPreview
name="form-tanstack-demo"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
## Approach
This form leverages TanStack Form for powerful, headless form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
- Uses TanStack Form's `useForm` hook for form state management.
- `form.Field` component with render prop pattern for controlled inputs.
- `<Field />` components for building accessible forms.
- Client-side validation using Zod.
- Real-time validation feedback.
## Anatomy
Here's a basic example of a form using TanStack Form with the `<Field />` component.
```tsx showLineNumbers {15-31}
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="title"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
</FieldGroup>
<Button type="submit">Submit</Button>
</form>
```
## Form
### Create a schema
We'll start by defining the shape of our form using a Zod schema.
<Callout icon={<InfoIcon />}>
**Note:** This example uses `zod v3` for schema validation. TanStack Form
integrates seamlessly with Zod and other Standard Schema validation libraries
through its validators API.
</Callout>
```tsx showLineNumbers title="form.tsx"
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
```
### Setup the form
Use the `useForm` hook from TanStack Form to create your form instance with Zod validation.
```tsx showLineNumbers title="form.tsx" {10-21}
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
const formSchema = z.object({
// ...
})
export function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast.success("Form submitted successfully")
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
{/* ... */}
</form>
)
}
```
We are using `onSubmit` to validate the form data here. TanStack Form supports other validation modes, which you can read about in the [documentation](https://tanstack.com/form/latest/docs/framework/react/guides/dynamic-validation).
### Build the form
We can now build the form using the `form.Field` component from TanStack Form and the `<Field />` component.
<ComponentSource
src="/registry/new-york-v4/examples/form-tanstack-demo.tsx"
title="form.tsx"
/>
### Done
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the `onSubmit` function will be called with the validated form data. If the form data is invalid, TanStack Form will display the errors next to each field.
## Validation
### Client-side Validation
TanStack Form validates your form data using the Zod schema. Validation happens in real-time as the user types.
```tsx showLineNumbers title="form.tsx" {13-15}
import { useForm } from "@tanstack/react-form"
const formSchema = z.object({
// ...
})
export function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return <form onSubmit={/* ... */}>{/* ... */}</form>
}
```
### Validation Modes
TanStack Form supports different validation strategies through the `validators` option:
| Mode | Description |
| ------------ | ------------------------------------ |
| `"onChange"` | Validation triggers on every change. |
| `"onBlur"` | Validation triggers on blur. |
| `"onSubmit"` | Validation triggers on submit. |
```tsx showLineNumbers title="form.tsx" {6-9}
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
onChange: formSchema,
onBlur: formSchema,
},
})
```
## Displaying Errors
Display errors next to the field using `<FieldError />`. For styling and accessibility:
- Add the `data-invalid` prop to the `<Field />` component.
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
```tsx showLineNumbers title="form.tsx" {4,18}
<form.Field
name="email"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
type="email"
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
```
## Working with Different Field Types
### Input
- For input fields, use `field.state.value` and `field.handleChange` on the `<Input />` component.
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-tanstack-input"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,11-14,22}
<form.Field
name="username"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-input-username">Username</FieldLabel>
<Input
id="form-tanstack-input-username"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="shadcn"
autoComplete="username"
/>
<FieldDescription>
This is your public display name. Must be between 3 and 10 characters.
Must only contain letters, numbers, and underscores.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
```
### Textarea
- For textarea fields, use `field.state.value` and `field.handleChange` on the `<Textarea />` component.
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-tanstack-textarea"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,13-16,24}
<form.Field
name="about"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-textarea-about">
More about you
</FieldLabel>
<Textarea
id="form-tanstack-textarea-about"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="I'm a software engineer..."
className="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us personalize
your experience.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
```
### Select
- For select components, use `field.state.value` and `field.handleChange` on the `<Select />` component.
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-tanstack-select"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,18-19,23}
<form.Field
name="language"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</Field>
)
}}
/>
```
### Checkbox
- For checkbox, use `field.state.value` and `field.handleChange` on the `<Checkbox />` component.
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
- For checkbox arrays, use `mode="array"` on the `<form.Field />` component and TanStack Form's array helpers.
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
<ComponentPreview
name="form-tanstack-checkbox"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {12,17,22-24,44}
<form.Field
name="tasks"
mode="array"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you&apos;ve created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={isInvalid}
>
<Checkbox
id={`form-tanstack-checkbox-${task.id}`}
name={field.name}
aria-invalid={isInvalid}
checked={field.state.value.includes(task.id)}
onCheckedChange={(checked) => {
if (checked) {
field.pushValue(task.id)
} else {
const index = field.state.value.indexOf(task.id)
if (index > -1) {
field.removeValue(index)
}
}
}}
/>
<FieldLabel
htmlFor={`form-tanstack-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
/>
```
### Radio Group
- For radio groups, use `field.state.value` and `field.handleChange` on the `<RadioGroup />` component.
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-tanstack-radiogroup"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {21,29,35}
<form.Field
name="plan"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
{plans.map((plan) => (
<FieldLabel
key={plan.id}
htmlFor={`form-tanstack-radiogroup-${plan.id}`}
>
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-tanstack-radiogroup-${plan.id}`}
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
/>
```
### Switch
- For switches, use `field.state.value` and `field.handleChange` on the `<Switch />` component.
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-tanstack-switch"
className="sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,14,19-21}
<form.Field
name="twoFactor"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-switch-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Switch
id="form-tanstack-switch-twoFactor"
name={field.name}
checked={field.state.value}
onCheckedChange={field.handleChange}
aria-invalid={isInvalid}
/>
</Field>
)
}}
/>
```
### Complex Forms
Here is an example of a more complex form with multiple fields and validation.
<ComponentPreview
name="form-tanstack-complex"
className="sm:[&_.preview]:h-[1100px] sm:[&_pre]:!h-[1100px]"
chromeLessOnMobile
/>
## Resetting the Form
Use `form.reset()` to reset the form to its default values.
```tsx showLineNumbers
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
```
## Array Fields
TanStack Form provides powerful array field management with `mode="array"`. This allows you to dynamically add, remove, and update array items with full validation support.
<ComponentPreview
name="form-tanstack-array"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
This example demonstrates managing multiple email addresses with array fields. Users can add up to 5 email addresses, remove individual addresses, and each address is validated independently.
### Array Field Structure
Use `mode="array"` on the parent field to enable array field management.
```tsx showLineNumbers title="form.tsx" {3,12-14}
<form.Field
name="emails"
mode="array"
children={(field) => {
return (
<FieldSet>
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup>
{field.state.value.map((_, index) => (
// Nested field for each array item
))}
</FieldGroup>
</FieldSet>
)
}}
/>
```
### Nested Fields
Access individual array items using bracket notation: `fieldName[index].propertyName`. This example uses `InputGroup` to display the remove button inline with the input.
```tsx showLineNumbers title="form.tsx"
<form.Field
name={`emails[${index}].address`}
children={(subField) => {
const isSubFieldInvalid =
subField.state.meta.isTouched && !subField.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isSubFieldInvalid}>
<FieldContent>
<InputGroup>
<InputGroupInput
id={`form-tanstack-array-email-${index}`}
name={subField.name}
value={subField.state.value}
onBlur={subField.handleBlur}
onChange={(e) => subField.handleChange(e.target.value)}
aria-invalid={isSubFieldInvalid}
placeholder="name@example.com"
type="email"
/>
{field.state.value.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => field.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
{isSubFieldInvalid && (
<FieldError errors={subField.state.meta.errors} />
)}
</FieldContent>
</Field>
)
}}
/>
```
### Adding Items
Use `field.pushValue(item)` to add items to an array field. You can disable the button when the array reaches its maximum length.
```tsx showLineNumbers title="form.tsx"
<Button
type="button"
variant="outline"
size="sm"
onClick={() => field.pushValue({ address: "" })}
disabled={field.state.value.length >= 5}
>
Add Email Address
</Button>
```
### Removing Items
Use `field.removeValue(index)` to remove items from an array field. You can conditionally show the remove button only when there's more than one item.
```tsx showLineNumbers title="form.tsx"
{
field.state.value.length > 1 && (
<InputGroupButton
onClick={() => field.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
)
}
```
### Array Validation
Validate array fields using Zod's array methods.
```tsx showLineNumbers title="form.tsx"
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("Enter a valid email address."),
})
)
.min(1, "Add at least one email address.")
.max(5, "You can add up to 5 email addresses."),
})
```

View File

@@ -18,7 +18,7 @@ npx create-tsrouter-app@latest my-app --template file-router --tailwind --add-on
You can now start adding components to your project.
```bash
npx shadcn@canary add button
npx shadcn@latest add button
```
The command above will add the `Button` component to your project. You can then import it like this:

View File

@@ -7,109 +7,18 @@ description: Install and configure shadcn/ui for TanStack Start.
### Create project
Start by creating a new TanStack Start project by following the [Build a Project from Scratch](https://tanstack.com/start/latest/docs/framework/react/build-from-scratch) guide on the TanStack Start website.
**Do not add Tailwind yet. We'll install Tailwind v4 in the next step.**
### Add Tailwind
Install `tailwindcss` and its dependencies.
Run the following command to create a new TanStack Start project with shadcn/ui:
```bash
npm install tailwindcss @tailwindcss/postcss postcss
npm create @tanstack/start@latest --tailwind --add-ons shadcn
```
### Create postcss.config.ts
Create a `postcss.config.ts` file at the root of your project.
```ts title="postcss.config.ts" showLineNumbers
export default {
plugins: {
"@tailwindcss/postcss": {},
},
}
```
### Create `app/styles/app.css`
Create an `app.css` file in the `app/styles` directory and import `tailwindcss`
```css title="app/styles/app.css"
@import "tailwindcss" source("../");
```
### Import `app.css`
```tsx title="app/routes/__root.tsx" showLineNumbers {5,21-26} showLineNumbers
import type { ReactNode } from "react"
import { createRootRoute, Outlet } from "@tanstack/react-router"
import { Meta, Scripts } from "@tanstack/start"
import appCss from "@/styles/app.css?url"
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: "utf-8",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "TanStack Start Starter",
},
],
links: [
{
rel: "stylesheet",
href: appCss,
},
],
}),
component: RootComponent,
})
```
### Edit tsconfig.json file
Add the following code to the `tsconfig.json` file to resolve paths.
```ts title="tsconfig.json" showLineNumbers {9-12}
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"module": "ESNext",
"target": "ES2022",
"skipLibCheck": true,
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"@/*": ["./app/*"]
}
}
}
```
### Run the CLI
Run the `shadcn` init command to setup your project:
```bash
npx shadcn@canary init
```
This will create a `components.json` file in the root of your project and configure CSS variables inside `app/styles/app.css`.
### That's it
### Add Components
You can now start adding components to your project.
```bash
npx shadcn@canary add button
npx shadcn@latest add button
```
The command above will add the `Button` component to your project. You can then import it like this:
@@ -117,10 +26,7 @@ The command above will add the `Button` component to your project. You can then
```tsx title="app/routes/index.tsx" showLineNumbers {1,6}
import { Button } from "@/components/ui/button"
function Home() {
const router = useRouter()
const state = Route.useLoaderData()
function App() {
return (
<div>
<Button>Click me</Button>
@@ -130,3 +36,9 @@ function Home() {
```
</Steps>
If you want to add all `shadcn/ui` components, you can run the following command:
```bash
npx shadcn@latest add --all
```

View File

@@ -4,6 +4,7 @@
"(root)",
"changelog",
"components",
"forms",
"installation",
"dark-mode",
"registry"

View File

@@ -103,7 +103,7 @@ You can read more about the registry item schema and file types in the [registry
### Install the shadcn CLI
```bash
npm install shadcn@canary
npm install shadcn@latest
```
### Add a build script

View File

@@ -1,5 +1,5 @@
---
title: Index
title: Add a Registry
description: Open Source Registry Index
---
@@ -11,16 +11,9 @@ You can see the full list at [https://ui.shadcn.com/r/registries.json](https://u
## Adding a Registry
You can submit a PR to add a registry to the index by adding it to the [registries.json](https://github.com/shadcn-ui/ui/blob/main/apps/v4/public/r/registries.json) file.
You can open an issue to add a registry to the index by filling out the [registry directory issue template](https://github.com/shadcn-ui/ui/issues/new?template=registry_directory.yml).
Here's an example of how to add a registry to the index:
```json title="registries.json" showLineNumbers
{
"@acme": "https://registry.acme.com/r/{name}.json",
"@example": "https://example.com/r/{name}"
}
```
Once you have submitted your issue, it will be validated and reviewed by the team.
### Requirements
@@ -65,15 +58,3 @@ Here's an example of a valid registry:
]
}
```
### Validation
At the root of the `shadcn/ui` project, you can run the following command to validate the `registries.json` file.
```bash
pnpm validate:registries
```
This will validate the registries.json file and output any errors.
Once you have submitted your PR, it will be validated and reviewed by the team.

View File

@@ -1,21 +1,25 @@
import { dirname } from "path"
import { fileURLToPath } from "url"
import { FlatCompat } from "@eslint/eslintrc"
import { defineConfig, globalIgnores } from "eslint/config"
import nextVitals from "eslint-config-next/core-web-vitals"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
const eslintConfig = [
...compat.config({
extends: ["next/core-web-vitals", "next/typescript"],
const eslintConfig = defineConfig([
...nextVitals,
globalIgnores([
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
".source/**",
]),
{
rules: {
"@next/next/no-duplicate-head": "off",
"react-hooks/incompatible-library": "off",
"react-hooks/purity": "off",
"@next/next/no-html-link-for-pages": "off",
"@next/next/no-img-element": "off",
"@typescript-eslint/no-unused-vars": "off",
},
}),
]
},
])
export default eslintConfig

View File

@@ -1,11 +0,0 @@
import { useEffect, useState } from "react"
export function useIsMac() {
const [isMac, setIsMac] = useState(true)
useEffect(() => {
setIsMac(navigator.platform.toUpperCase().includes("MAC"))
}, [])
return isMac
}

View File

@@ -61,7 +61,10 @@ const Layout = ({
}
})
const attrs = !value ? ["layout-fixed", "layout-full"] : Object.values(value)
const attrs = React.useMemo(
() => (!value ? ["layout-fixed", "layout-full"] : Object.values(value)),
[value]
)
const applyLayout = React.useCallback(
(layout: Layout) => {

Some files were not shown because too many files have changed in this diff Show More