Patties UI — shadcn-compatible component library
Rebuild the full shadcn/ui catalog as Patties-native components: copy-in source (no runtime package), Bun-buildable, edge-safe, and SSR-first with explicit c…
Status: complete. All five phases (0–4) are implemented and archived. The full 60-component catalog is registered in
packages/patties-ui/src/registry.tswithstatus: "completed"and has a stamped template underpackages/patties-ui/templates/. This overview is retained as the catalog reference; the per-component specs live alongside it underphase-0/…phase-4/.
Mission
Rebuild the full shadcn/ui catalog as Patties-native components: copy-in source (no runtime package), Bun-buildable, edge-safe, and SSR-first with explicit client-island boundaries.
This is not a fork of shadcn/ui. It is a parallel catalog with the same API surface and visual design, retuned for Patties.
Catalog scope. Upstream shadcn (per
llms.txt, fetched 2026-05-24) lists 59 components and no longer shipsForm— it has been superseded byField(see 26-field). Patties keeps a 27-form spec for users migrating from the older react-hook-form recipe, markedcompatin the index below.
The retuning:
- Static parts render on the server with zero client JS by default.
- Interactive parts are marked as Patties islands (see framework/06-client-islands) and hydrate independently.
- All dependencies are Bun-compatible and WinterCG-safe (no Node-only built-ins, no
processaccess at module top-level). - Source is delivered by the Patties CLI (
bun patties add <component>) intoapp/components/ui/<name>.tsx— never imported from a versioned npm package.
Pillars
- Copy-in, not install. Components live in the user's repo. They can edit freely. The CLI just stamps source files and patches
package.jsonpeer deps. - SSR by default, island by opt-in. Every component file declares an
island: true | false | "subtree"export so the build can decide whether to ship the component to the client."subtree"means the component itself is static but inherits an island root from its parent (see Button). Components with state, refs todocument, or event handlers default toisland: true. - Tailwind v4 + CSS variables. Same theming model as upstream shadcn —
--background,--foreground,--primary, etc. — wired through@theme inlineinapp.css. - Radix-class primitives, vendor-neutral. We adopt
@radix-ui/*headless primitives where shadcn does. Each spec documents the exact peer-dep set; the CLI installs them on demand. - JSON-serializable props at island boundaries. Per framework spec 06, callbacks crossing the SSR→island boundary are forbidden. Each component spec calls out which props must be local to the island.
- No
forwardRefboilerplate. Patties targets React 19+; refs are passed as plain props. Component specs use the React-19 style.
Delivery layout
app/components/ui/
accordion.tsx
alert.tsx
…
app/components/ui/_internal/ # shared helpers (cn, slot, variants)
app/styles/tokens.css # CSS variables — generated by CLI
_internal/cn.ts depends on clsx and tailwind-merge; the CLI installs both the first time any component is added.
Default icon set. Components that ship an icon (chevrons, checks, close, ellipsis) use lucide-react for visual parity with upstream shadcn. Userland can swap icons by editing the stamped source — no central icon registry. Specs list lucide-react only when the file references it directly.
The CLI command (see cli/draft):
bun patties add <component> [...components]
bun patties add --all
stamps:
- The component source file(s) from this spec catalog.
- Any missing helpers into
_internal/. - Any missing peer deps (
@radix-ui/react-*,class-variance-authority,tailwind-merge,lucide-react) intopackage.json. - Token CSS (idempotent merge into
app/styles/tokens.css).
Spec template
Each component spec is one file. Required sections:
frontmatter—spec,title,status,island,peer_deps,last_reviewed.## Purpose— one paragraph, what it is and what shadcn component it mirrors.## Island model—island: true | false | "subtree", with reasoning. ("subtree"= a static outer shell with one nested island, e.g. a Card that wraps an interactive button.)## Peer dependencies— exact npm package names and version constraints.## Public API— the exported components and their props (TS-shape; no narration).## Patties adjustments— what differs from upstream shadcn: removedforwardRef, addedislandexport, replaceduseThemehook, etc.## Acceptance criteria— concrete, testable bullets.
Phase plan
The catalog was staged into five phases, each its own folder, picked up in numeric order within a phase. All five are now implemented and archived under agent_specs/ui/archive/.
| Phase | Theme | Scope |
|---|---|---|
| phase-0 | Foundation | bun patties add CLI command, _internal/ helpers, tokens.css merge, peer-dep patcher, island export convention, snapshot + hydration test harness. No components. |
| phase-1 | Zero-JS primitives (island: no) | 18 components — Alert, Aspect Ratio, Badge, Breadcrumb, Button Group, Card, Direction, Empty, Input Group, Item, Kbd, Label, Pagination, Separator, Skeleton, Spinner, Table, Typography. |
| phase-2 | Form primitives (island: yes ⇣) + Button subtree | 12 components — Button, Checkbox, Field, Form (compat), Input, Native Select, Progress, Radio Group, Slider, Switch, Textarea, Toggle. Proves the SSR-first / island-on-opt-in path end-to-end. |
| phase-3 | Radix-backed islands | 21 components — Accordion, Alert Dialog, Avatar, Collapsible, Context Menu, Dialog, Drawer, Dropdown Menu, Hover Card, Input OTP, Menubar, Navigation Menu, Popover, Scroll Area, Select, Sheet, Sonner, Tabs, Toast, Toggle Group, Tooltip. |
| phase-4 | Recipes & heavy islands | 9 components — Calendar, Carousel, Chart, Combobox, Command, Data Table, Date Picker, Resizable, Sidebar. |
A component's spec lives in exactly one phase folder. All phase folders now sit under archive/; the index below links to each spec's location.
Component index
Kind: primitive = thin wrapper around a single Radix/headless library; recipe = composed multi-piece pattern stamped as editable source; provider = React context only. Island: no = zero JS; yes = hydrates; subtree = static, inherits parent island; yes ⇣ = hydrates by default but downgrades to zero JS in the documented nativeForm / uncontrolled path.
| # | Component | Kind | Island | shadcn parity |
|---|---|---|---|---|
| 01 | Accordion | primitive | yes | full |
| 02 | Alert | primitive | no | full |
| 03 | Alert Dialog | primitive | yes | full |
| 04 | Aspect Ratio | primitive | no | full |
| 05 | Avatar | primitive | yes | full |
| 06 | Badge | primitive | no | full |
| 07 | Breadcrumb | primitive | no | full |
| 08 | Button | primitive | subtree | full |
| 09 | Button Group | primitive | no | full |
| 10 | Calendar | primitive | yes | full |
| 11 | Card | primitive | no | full |
| 12 | Carousel | primitive | yes | full |
| 13 | Chart | primitive | yes | full |
| 14 | Checkbox | primitive | yes ⇣ | full |
| 15 | Collapsible | primitive | yes | full |
| 16 | Combobox | recipe | yes | full |
| 17 | Command | primitive | yes | full |
| 18 | Context Menu | primitive | yes | full |
| 19 | Data Table | recipe | yes | full |
| 20 | Date Picker | recipe | yes | full |
| 21 | Dialog | primitive | yes | full |
| 22 | Direction | provider | no | full |
| 23 | Drawer | primitive | yes | full |
| 24 | Dropdown Menu | primitive | yes | full |
| 25 | Empty | primitive | no | full |
| 26 | Field | primitive | yes | full |
| 27 | Form | recipe | yes | compat (dropped upstream in favor of Field) |
| 28 | Hover Card | primitive | yes | full |
| 29 | Input | primitive | yes ⇣ | full |
| 30 | Input Group | primitive | no | full |
| 31 | Input OTP | primitive | yes | full |
| 32 | Item | primitive | no | full |
| 33 | Kbd | primitive | no | full |
| 34 | Label | primitive | no | full |
| 35 | Menubar | primitive | yes | full |
| 36 | Native Select | primitive | yes ⇣ | full |
| 37 | Navigation Menu | primitive | yes | full |
| 38 | Pagination | primitive | no | full |
| 39 | Popover | primitive | yes | full |
| 40 | Progress | primitive | yes ⇣ | full |
| 41 | Radio Group | primitive | yes ⇣ | full |
| 42 | Resizable | primitive | yes | full |
| 43 | Scroll Area | primitive | yes | full |
| 44 | Select | primitive | yes | full |
| 45 | Separator | primitive | no | full |
| 46 | Sheet | primitive | yes | full |
| 47 | Sidebar | recipe | yes | full |
| 48 | Skeleton | primitive | no | full |
| 49 | Slider | primitive | yes ⇣ | full |
| 50 | Sonner | primitive | yes | full |
| 51 | Spinner | primitive | no | full |
| 52 | Switch | primitive | yes ⇣ | full |
| 53 | Table | primitive | no | full |
| 54 | Tabs | primitive | yes | full |
| 55 | Textarea | primitive | yes ⇣ | full |
| 56 | Toast | primitive | yes | superseded by Sonner; kept for parity |
| 57 | Toggle | primitive | yes ⇣ | full |
| 58 | Toggle Group | primitive | yes | full |
| 59 | Tooltip | primitive | yes | full |
| 60 | Typography | primitive | no | full |
Cross-cutting acceptance criteria
These apply to every component below. Component-level acceptance criteria assume these as a baseline:
- The component file exports a constant
island: boolean | "subtree"for the build pipeline. - The component file passes
bun build --target=browserwith no Node-builtin warnings. - Non-JSON props (functions, Dates, Maps, Sets, BigInt) at an island root are flagged by the build per framework/06-client-islands. Runtime enforcement is a follow-up against that spec.
- Tailwind classes resolve under Tailwind v4 with the Patties default
tokens.css. - Dark mode flips by toggling
class="dark"on<html>; no JS theming dependency. - No top-level access to
window,document,process, orglobalThis.navigator. All such access is inside event handlers oruseEffect. bun testruns a snapshot of the static SSR HTML and a hydration round-trip test for any island component.