Using upstream shadcn/ui
Keep shadcn/ui's real components and npx shadcn-style ergonomics inside Patties: stamp the canonical registry verbatim, and let the page hydrate as one React tree.
Quick start
New project:
bunx create-patties@latest my-app --ui shadcn
Existing project (needs patties-ui installed as a dev dependency):
bunx patties ui init --shadcn # @/* alias + app/lib/utils.ts + tokens
bunx patties add @shadcn/dialog # stamps dialog.tsx and its button.tsx dependency
bun install # patties add edits package.json, never installs
Then opt the page that uses the components into whole-tree hydration:
// app/routes/settings.tsx
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
export const hydrate = true;
export default function Settings() {
const [open, setOpen] = React.useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>Edit</DialogTrigger>
<DialogContent>…</DialogContent>
</Dialog>
);
}
Why hydration matters
Islands hydrate one component at a time and pass props as JSON, so function props (onOpenChange) and context shared by composed parts (DialogTrigger + DialogContent) cannot cross the boundary. A page exporting hydrate = true hydrates its whole subtree with one hydrateRoot, so upstream components run exactly as written and "use client" is a harmless no-op.
| Page kind | Client JS | Hydration | shadcn/ui usage |
|---|---|---|---|
| Default | none | none | static markup only |
| Island page | island chunks | one root per island, JSON props | interactive parts must be wrapped in an island |
hydrate = true | the page's chunk | one root over #root | verbatim — function props and context work |
On a hydrated page, <Island> wrappers are redundant; patties doctor warns when it finds both.
What patties add @shadcn/<name> does
- Fetches the registry item from
https://ui.shadcn.com/r/styles/new-york-v4(override withui.registries["@shadcn"]inpatties.config.tsto use a mirror or a pinned copy). Any shadcn item URL or local.jsonworks too. - Writes file contents verbatim. The only rewrite is the one the shadcn CLI makes:
@/registry/<style>/ui/xbecomes@/components/ui/x(likewiselib,hooks,components). - Puts
registry:uifiles inapp/components/ui/, hooks inapp/hooks/, libs inapp/lib/. - Resolves
registryDependencies(and sibling imports upstream forgot to declare) first, each stamped once. - Adds npm dependencies — including imported ones upstream left undeclared — to
package.json, pinned to^<current>. It never runs an install. - Merges
cssVarsintoapp/styles/tokens.cssas ashadcn-<name>block. - Keeps the
addguarantees: HTTPS only (unless--allow-insecure), source preview and confirmation (or--yes), schema validation, nothing executed.
The @/ alias
Upstream source imports through @/. patties ui init --shadcn adds "paths": { "@/*": ["./app/*"] } to tsconfig.json; Bun honours it at runtime and in Bun.build. Keep ui.componentsDir at its default app/components/ui so @/components/ui/… resolves.
Bundle cost
A hydrated page ships its whole component tree, which is the price of the carve-out. React and shared components are code-split across pages, and patties build prints each hydrated route's client bytes (entry chunk plus everything it imports) so the cost stays visible. Pages that don't need interactivity should stay zero-JS or island-based.
Determinism
The client re-renders with the props the server used (route params plus loader() data from the bootstrap payload). Avoid Math.random() or Date.now() during render; use useId for ids, which shadcn/ui already does.