Reference

Engineering

The component contract, repository boundaries, motion rules, and documentation parity.

Every component follows the contract below.

Component contract

  • Base UI is the canonical behavior layer for focus management, keyboard navigation, roving focus, dialogs, popups, collision handling, dismissal, and ARIA wiring. Calendar uses react-day-picker because Base UI has no calendar primitive.
  • Components forward props and refs, support controlled and uncontrolled state where relevant, and allow element replacement when the underlying element may reasonably change.
  • Variants live beside the component in a .variants.ts file and use CVA.
  • State is exposed on the element through attributes such as data-state, data-variant, and data-loading, so styling and tests read the DOM.
  • Components use semantic theme tokens by default. Direct palette ramps are reserved for status accents and fixed neutral control states, as documented in the root contract.
  • Add "use client" when client APIs, hooks, context, or interaction require it. Table uses client context even though its presentation parts have no interactive state.
  • A panel of rows (select, searchable menu, command palette) opens with a row already active: the current selection, or the most recent one when several are chosen, otherwise the first row. Typing moves it to the top match. In the selects, hovering moves the active row; in the command palette hover only highlights, and a press moves the selection. The pointer leaving the panel keeps the active row. It carries data-panel-active, or Base UI's data-highlighted where the primitive owns the highlight. autoFocusFirstItem={false} opts out for menu-like uses.

Motion contract

Hover, focus, simple fades, and ordinary press feedback are CSS-first. Button and Toggle use Motion for spring presses. Motion also handles retained exits such as Morph's outgoing content, gestures, springs, and layout animation. Base UI popups can use CSS exits through their presence lifecycle.

Transitions name their properties and prefer compositor-friendly motion. Collapsible's height transition and Sidebar's shell dimensions and offcanvas position are accepted layout-motion exceptions. CSS durations use the shared motion tokens; JavaScript duration and easing defaults mirror those tokens and must be updated together. CSS overrides do not change JavaScript animations, so reduced motion is handled separately in both.

Shared loading indicators

Loading indicators are caller-supplied React nodes. LoaderProvider installs automatically with a consuming component and reaches React portals. Component loader props override it; existing custom pending icons override both. Omitted values inherit; null hides the indicator. AsciiLoader is the fallback. Alternative loaders and their settings belong to the caller.

import { LoaderProvider } from "@/lib/loader";
import { Button } from "@/components/ui/button";
import { ClassicLoader } from "@/components/ui/classic-loader";
import { MatrixLoader } from "@/components/ui/matrix-loader";

<LoaderProvider loader={<ClassicLoader speed={1} />}>
  <Button loading>Save</Button>
  <Button loading loader={<MatrixLoader />}>Custom</Button>
</LoaderProvider>

Install classic-loader and matrix-loader separately through the CLI to use this example. Library implementations use LoadingIndicator from registry/lib/loader; the shared helper does not import Classic or maintain a variant/speed API. The helper renders the node inside a fixed-size decorative wrapper; the consuming component owns announcements.

Repository boundaries

Three related surfaces serve different jobs:

SurfaceMeaning
Registry itemA source bundle installed with shadcn add
Export or subcomponentAn API bundled inside a registry item, such as SelectSortTrigger inside select
Demo entryInteractive documentation for a complete item or one particular composition

An exported helper does not automatically need another install item, and a demo does not define the registry boundary.

registry/     Portable library source
  lib/        Shared utilities and motion presets
  ui/         Installable component source
inspector/    Data-driven demos, settings, and schemas
stories/      Fumadocs demo adapter
content/docs/ Routed MDX documentation
app/          Next.js routes; (site)/ holds the overview and docs pages
components/   Site shell (rail, page frame) and landing figures
_incoming/    Temporary staging; never imported
registry.json Install metadata and dependency graph
public/r/     Built registry (pnpm registry:build), served at /r/<name>.json

The dependency direction is app/content/stories → inspector → registry. Portable code must never import from the documentation or demo layers.

Shared foundation

These registry items are installed transitively rather than shown as standalone component pages:

ItemResponsibility
4af-themeSemantic colors, typography, radii, elevation, keyframes, and motion durations
cnClass-name composition with clsx and tailwind-merge
slotSmall local helper for asChild composition
motionShared spring and easing presets
loaderCaller-supplied loading slots and provider defaults, with AsciiLoader as the fallback
use-actionRun, pending, outcome, and revert state shared by Action Button and asynchronous menu rows
table-of-contents-scrollActive-entry centering, endpoint padding, and overflow-edge state for constrained contents rails
table-of-contents-trackingHeading activation, visibility observation, and click-to-scroll settling shared by both contents variants

Use the matching action component or menu row for standard async controls. For a custom control, useAction provides the shared pending, outcome, and reset behavior directly.

cn uses Tailwind Merge 3 for Tailwind 4 utilities, including custom-property forms such as duration-(--duration-fast) and w-(--sidebar-width). The theme's shadow-border, shadow-raised, shadow-overlay, and shadow-overlay-drop are registered as shadow sizes, so a consumer's later shadow-none replaces them while independent shadow colors survive.

Documentation parity

Every installable UI item has one component page containing its demo, settings, installation, usage, and component-specific guidance. The generated component catalog and overview count read registry.json directly.

Pages load their demo entries through the shared adapter and use the shared Install and Usage reference. Foundation items such as the theme, cn, slot, and motion presets install as dependencies and have no demos of their own. Append .md to any documentation URL to read it as Markdown.

The demo layer also contains example helpers for Action Button, Badge cycling, Command Palette, Morph, Select fruit icons, Sidebar, and Table of Contents. They hold demo state, sample data, or page composition and are not installable. inspector/ holds demo infrastructure only and has no routes of its own.

The site documents the library; registry.json and the source are authoritative.

Working on the repository

pnpm dev
pnpm typecheck
pnpm lint
pnpm test
pnpm registry:validate
pnpm registry:build

Never run pnpm build while pnpm dev is running. They share .next, and a production build can remove CSS chunks that the development server is still serving.

When a component is added or renamed, update registry.json, its demo entry when it has one, and its content/docs/ page. When public API or behavior changes, update the component page's API/notes and lib/component-reference.ts. Update COMPONENTS.md and the agent catalog when the installable catalog or composition guidance changes. Navigation, the component count, and the public catalog are derived from those sources; registry.json is the installable catalog, so don't keep a second hard-coded list.