The template's visual identity lives in tokens (packages/ui/src/styles/globals.css), and every surface composes from owned source in packages/ui. apps/web/PRODUCT.md holds the principles and anti-references; read it before any UI work. This page says which tool each job uses, so a project built from the template keeps one design language as it grows.
Three registers
| Register | Surfaces | Authority | Agent skills |
|---|---|---|---|
| Operate | dashboard, todos, files, settings, auth | shadcn primitives and tokens; apps/web/PRODUCT.md | ui-catalog, web-design-guidelines |
| Persuade | landing, marketing, pricing | docs/landing.md; the same tokens through landing.css aliases | design-taste-frontend, redesign-existing-projects, stageshow |
| Developer docs | /docs | Fumadocs headless (fumadocs-core + fumadocs-mdx) with our own components | docs-and-changelog |
An agent picks the register first (ui-catalog step 1), then loads only that register's skills. Taste guidance never reaches an Operate screen.
Sources by job
| Job | Source | Skill |
|---|---|---|
| Primitive, form, overlay, empty, toast | shadcn/ui, installed in packages/ui/src/components | shadcn, ui-catalog |
| Free composites (tree, timeline, pickers) | ReUI free registry (@reui); Pro blocks stay out | ui-catalog |
| Chat, streaming, tool cards | the installed AI/chat set, then AI Elements | ai-assistant |
| Charts, sparklines, KPI cards | Bklit UI, the @bklit shadcn registry (MIT, source copied into the app) | bklit-ui, bklit-ui-design, bklit-ui-migrate |
| Product illustrations (faux UI) | Stageshow, kit vendored in apps/web/src/platform/stageshow/ | stageshow |
| Loading states (skeletons) | skullet through MeasuredSkeleton (apps/web/src/platform/skeleton/) and samples | ui-catalog |
| Motion on an existing control | transitions.dev recipes | transitions-dev, emil-design-eng, review-animations |
| Native (Expo) | SwiftUI / Jetpack Compose through @expo/ui | expo-ui, expo-native-ui |
Three rules with no alternative
- Charts come from Bklit UI only. Every chart, sparkline and KPI or stat card on the web is a Bklit component, installed with
pnpm dlx shadcn@latest add @bklit/<item>inpackages/ui. No other chart library (Recharts, Chart.js, Nivo, Victory, visx, ECharts, ApexCharts, Highcharts, Tremor and the like) is imported inapps/web/srcorpackages/ui/srcor listed in theirpackage.json;scripts/web-chart-library.test.mjsfails if one is. d3 stays allowed, because Bklit components call it. An existing chart moves over withbklit-ui-migrate. - Product illustrations are built with Stageshow only. Faux product UI on landing, marketing and docs pages (stills, feature cards, cursor demos, bento grids) starts from a Stageshow gold example, uses the kit in
apps/web/src/platform/stageshow/(copy more parts from.agents/skills/stageshow/kitwhen a scene needs them) and ships only after its loop passesfilm.mjs --gate. A chart inside a scene is a Bklit chart placed on the stage, since Stageshow takes the app's own components. Review holds this rule. - Loading skeletons are skullet, through
MeasuredSkeletononly. Every loading skeleton in the web app isMeasuredSkeletonfrom@/platform/skeleton/measured-skeleton: passdataand asampleof the same shape from the feature's<feature>.samples.tsand render from the argument ofchildren, or passloadingover content already on screen (a refetch). The wrapper adds the translated label; the provider in the signed-in layout sets the reveal and the theme, and the colours are the--skullet-*tokens. Onlyapps/web/src/platform/skeleton/importsskullet. No hand-madeanimate-pulseboxes, no shadcnSkeletonorSidebarMenuSkeleton, and no other skeleton library (react-loading-skeleton, react-content-loader, MUI, Chakra and the like) inapps/web/srcorpackages/ui/srcor theirpackage.json. Skeletons render only behind sign-in, so the skullet chunk stays off public pages (W15): a public page renders its data from the route loader, or aSpinnerfor a pending action.scripts/web-skeletons.test.mjschecks all of this, including that no route outsideroutes/_auth/reaches the wrapper through its imports.
Bklit draws DOM and SVG for React on the web and has no React Native build, so the chart rule stops at apps/web and packages/ui. A chart in the Expo app uses victory-native, which draws with Skia, Reanimated and Gesture Handler, all already in apps/mobile; add it with expo install with the first mobile chart (it needs a newer Skia than the app pins today). An iOS-only screen may use Chart from @expo/ui/swift-ui (Swift Charts) instead.
Skullet measures the DOM and ships React, Vue, Svelte and web-component builds, but none for React Native, so the skeleton rule stops at the web too. A loading list in the Expo app uses SkeletonRows from apps/mobile/src/design (a still placeholder with the metrics of a SettingRow group and a spoken label).
The shadcn skeleton.tsx stays in packages/ui because the vendored shadcn sidebar (SidebarMenuSkeleton) and the ReUI emoji picker draw their own loading state with it; app code does not import either. The wrapper covers skullet's Skeleton only: a Suspense fallback (SkeletonSuspense) or a placeholder box (SkeletonBox) gets its own wrapper in platform/skeleton/ when a screen first needs one, never a direct import. Passing id keeps a skeleton's shape in localStorage; the change that first does so also calls forgetShapes() on sign-out.
Two surfaces predate these rules: the dashboard KPI cards (dashboard.stat-card.tsx, plain number cards) and the hand-built fragments of the landing feature board (landing.feature-*.tsx). Do not copy them as patterns; move them to Bklit and Stageshow when you rework them.
Rules that keep it consistent
- Tokens, not values. A component reads
--primary,--card,--radiusand the rest; a registry's own colors and fonts are mapped onto them, never copied. A kit with its own token layer (Stageshow's--ss-*, Bklit's chart colors) is aliased to ours in one place. - Owned source. Registry items are copied into
packages/ui(or a feature'splatform/folder) and exported one subpath at a time. Diff before accepting an overwrite: local files carry React Compiler and test edits. - Every state ships. Loading, empty and error are part of the screen, because developers copy what the template shows.
- A skeleton is drawn from the real component. A loading state is measured from a sample of the component it stands in for (
<feature>.samples.ts), never hand-built from boxes, so nothing moves when the data lands. - Honest illustrations. Faux UI on landing, marketing and docs pages uses the product's own strings from the locale catalogs, shows only features the template has, and is labelled as an illustration. A Stageshow scene passes
film.mjs --gatebefore it ships. - Motion respects the reader. Every animation has a reduced-motion state, pauses off screen, and starts when it can be seen.
- Weight is measured. A new kit or registry item records its gzip delta on the route it lands in, in
docs/measurements/.