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 kindClient JSHydrationshadcn/ui usage
Defaultnonenonestatic markup only
Island pageisland chunksone root per island, JSON propsinteractive parts must be wrapped in an island
hydrate = truethe page's chunkone root over #rootverbatim — 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 with ui.registries["@shadcn"] in patties.config.ts to use a mirror or a pinned copy). Any shadcn item URL or local .json works too.
  • Writes file contents verbatim. The only rewrite is the one the shadcn CLI makes: @/registry/<style>/ui/x becomes @/components/ui/x (likewise lib, hooks, components).
  • Puts registry:ui files in app/components/ui/, hooks in app/hooks/, libs in app/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 cssVars into app/styles/tokens.css as a shadcn-<name> block.
  • Keeps the add guarantees: 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.

Patties documentation · Licensed under MIT. · Design specs