UI Contribution Guide
This guide applies to all UI work in apps/frontend/src/app/ui/ and packages/design-tokens/.
It also serves as the canonical reference for AI agents extending the design system.
Before you write any code
- Check INVENTORY.md — Is there an Approved component that already does what you need?
apps/frontend/src/app/ui/INVENTORY.md - Check Storybook — Browse
pnpm --filter frontend run storybookfor visual reference. - Check
ui/barrel exports — Import from the barrel:import { Button, Card } from '@/app/ui'
If an Approved component exists: use it, extend it, do not duplicate it.
Adding a new shared component
New shared components require all of the following in the same PR:
src/app/ui/<category>/<ComponentName>/
<ComponentName>.tsx ← Component implementation
<ComponentName>.stories.tsx ← Storybook stories (required)
index.ts ← Barrel export
Plus:
- Jest tests covering key states (in
__tests__/or colocated) - Status label added to
INVENTORY.md - Entry in the relevant
index.tsbarrel
Do NOT ship a new shared component without stories. A component without a story is not discoverable.
Modifying an existing component
- Read the component file before modifying it.
- Check all callsites before changing prop names or signatures.
- If you break a prop API, update every callsite in the same PR.
- Update the matching
*.stories.tsxto reflect changed API. - Run targeted tests:
pnpm --filter frontend run test -- --testPathPatterns="<ComponentName>" - Run type-check:
npx tsc --noemit
Writing a story
Stories live beside the component (colocated):
Button.tsx
Button.stories.tsx ← same directory
Story structure:
import type { Meta, StoryObj } from '@storybook/react';
const meta = {
title: '<Category>/<ComponentName>',
component: MyComponent,
tags: ['autodocs'], // enables automatic docs page
parameters: {
docs: { description: { component: '...' } },
},
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
// One story per distinct state / variant
export const Default: Story = { args: { ... } };
export const Disabled: Story = { args: { isDisabled: true, ... } };
Required stories for every component:
- Default state
- All variants (if applicable)
- Disabled / loading / error states (if applicable)
- Interactive story for components with state changes
Accessibility requirements
Every shared component must meet these minimum bars:
| Requirement | Check |
|---|---|
| Semantic HTML elements | No <div role="button"> — use <button> |
| Keyboard navigation | All interactive elements reachable via Tab |
| Focus visible | :focus-visible ring on all interactive elements |
| ARIA labels | All icon-only buttons have aria-label |
| Form labels | All inputs associated with a <label> |
aria-expanded | Accordions and dropdowns declare their state |
role="dialog" aria-modal | Modals declare themselves as dialogs |
aria-hidden="true" | Decorative icons and SVGs carry this |
| Color alone | Never convey information with color alone |
Run the Storybook accessibility addon checks:
pnpm --filter frontend run storybook
# Select a story → A11y tab in the addons panel
Typography rules
- Use
--font-satoshi(viafont-satoshiTailwind class orvar(--font-satoshi)in CSS). - Never use
--font-groteskor--grotesk-font— these aliases are removed. - Use the
<Text>component or.text-*Tailwind classes for all type variants. - Never hardcode
font-familyin component files. Use CSS variables or Tailwind classes.
Token rules
- Never hardcode hex colors in component files. Use
--color-*CSS vars or Tailwind token classes. - Never hardcode pixel values for spacing. Use Tailwind spacing utilities (
p-4,gap-6) or--spacing-*vars. - Never hardcode border-radius values. Use
rounded-2xl(the defaultradius.xl) or the appropriate Tailwind class. - New tokens go in
packages/design-tokens/src/with semantic naming. See Design Tokens.
Commit format
feat(frontend): add <ComponentName> component with stories and tests
fix(frontend): fix keyboard accessibility in <ComponentName>
refactor(frontend): migrate <ComponentName> off react-bootstrap
chore(frontend): add story coverage for <ComponentName>
Scope is always frontend for web UI work, mobile for mobile theme work, repo for token package changes.
For AI agents
When asked to add or modify UI in apps/frontend:
- Read
src/app/ui/INVENTORY.mdto check component status. - Use only Approved components for new UI.
- Do not use
react-bootstrapclasses or imports. - Do not use
--font-groteskor--grotesk-font. - Do not hardcode hex values. Use CSS token variables.
- Colocate stories beside the component.
- Run
npx tsc --noemitandpnpm --filter frontend run lintbefore finishing.