# ADR-0017: shadcn/ui for infrastructure, hand-built for brand - **Status:** Accepted - **Date:** 2026-08-12 ## Context The component layer started as four hand-rolled primitives in `@sport/ui` (Button, Input, Badge, Skeleton). That was enough while the surface was static text and links. It stopped being enough the moment real interaction arrived: the mobile filter panel needs focus trapping and scroll locking, the product editor needs tabs, row actions need a menu, and the media picker needs a modal. Writing those by hand means writing focus management, `aria-modal` wiring, escape handling, portal placement and collision detection. That work is well-understood, easy to get subtly wrong, and worth nothing to this store's customers — nobody chooses a running jacket because the dialog traps focus correctly. They only notice when it doesn't. The opposite is true of the product card, the header, the hero and the PDP. Those _are_ the store. A generic card component would make them look like every other template on the internet, which is precisely the outcome the visual direction exists to avoid. ## Decision **Two tiers, split on whether a component carries brand.** `packages/ui/src/components/ui/` holds shadcn/ui components, owned as source in this repo and shared by the storefront and the admin: Button, Input, Dialog, Sheet, DropdownMenu, Tabs, plus Badge and Skeleton. Registry components are copied in essentially unedited so that anything else from shadcn drops in later. `apps/storefront/src/components/commerce/` holds everything that carries the brand, built by hand: hero, mega menu, site header, product card, product gallery, product detail, filter sheet. These are storefront-only and are never promoted to `@sport/ui`, per the existing rule in architecture §4. **A semantic token layer adapts shadcn to the palette.** shadcn writes against role tokens (`--background`, `--primary`, `--ring`, `--radius`); the store's palette is `ink`/`volt`. `packages/config/tailwind/theme.css` maps one onto the other, so a registry component already looks like this store before it is touched. Note that shadcn's `--accent` is a hover surface, not a brand accent — it maps to `ink-100`, and volt stays applied deliberately. **Supporting libraries, each for one job.** Lucide for icons, Motion for the hero's entrance, Embla for the PDP gallery's swipe. ## Consequences The dialog/sheet/menu behaviour we would otherwise have written badly is now correct and not our problem. `asChild` removed a real defect class: `` wrapping `