Component Architecture: Building UI That Scales With Your Team
A component library is not a design system. A design system is the agreement on how decisions get made.
When a product is maintained by one developer, consistency comes from memory. When it grows to five developers across two timezones, consistency needs to be enforced by the code itself.
At Brewstack Labs, the Product Lab is where engineering and interface design merge. Our design systems are built to be consumed, not admired.
The Three Layers
Every system we build has three distinct layers. Mixing them is the primary source of long-term UI debt.
Tokens - raw design decisions (color, spacing, radius, motion)
Primitives - headless, accessible building blocks
Composites - assembled UI with product-specific behaviorThey compose upward. Composites consume Primitives. Primitives consume Tokens. Nothing skips a layer.
Layer 1: Tokens in Code
Design tokens are not a Figma concept. They are variables in your codebase that encode every design decision.
// tokens.ts — single source of truth
// These values flow into Tailwind config, CSS custom properties, and React Native StyleSheet
export const tokens = {
color: {
brand: {
primary: '#ED6D40',
secondary: '#5C93A0',
},
neutral: {
900: '#111111',
800: '#1C1C1C',
700: '#252422',
200: '#DADADA',
100: '#F0F2F5',
},
semantic: {
success: '#4ADE80',
warning: '#FACC15',
error: '#F87171',
},
},
space: {
1: '4px', 2: '8px', 3: '12px', 4: '16px',
5: '20px', 6: '24px', 8: '32px', 10: '40px',
12: '48px', 16: '64px',
},
radius: {
sm: '8px', md: '12px', lg: '16px', xl: '24px', full: '9999px',
},
motion: {
fast: '150ms',
base: '250ms',
slow: '400ms',
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
},
} as const;Feeding these into Tailwind means the design language is enforced at the utility level:
// tailwind.config.ts
import { tokens } from './tokens';
export default {
theme: {
extend: {
colors: {
brand: tokens.color.brand,
neutral: tokens.color.neutral,
},
borderRadius: tokens.radius,
transitionDuration: {
fast: tokens.motion.fast,
base: tokens.motion.base,
slow: tokens.motion.slow,
},
},
},
};A developer who reaches for bg-brand-primary instead of bg-[#ED6D40] is using the system. One arbitrary hex value in a component is a crack in the foundation.
Layer 2: Headless Primitives
Primitives are components with behavior but no opinions about appearance. Accessibility, keyboard navigation, and ARIA semantics live here. Visual styling does not.
We use Radix UI as the headless foundation.
import * as Dialog from '@radix-ui/react-dialog';
// The primitive provides: focus trap, escape key, scroll lock, ARIA roles
// We provide: the visual design
function Modal({ trigger, title, children }: ModalProps) {
return (
<Dialog.Root>
<Dialog.Trigger asChild>{trigger}</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/60 backdrop-blur-sm data-[state=open]:animate-fadeIn" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg bg-neutral-800 rounded-xl p-6 shadow-2xl data-[state=open]:animate-slideUp">
<Dialog.Title className="text-lg font-bold text-neutral-100 mb-4">
{title}
</Dialog.Title>
{children}
<Dialog.Close asChild>
<button className="absolute top-4 right-4 text-neutral-400 hover:text-neutral-100 transition-colors">
<X className="w-4 h-4" />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}The critical discipline: the Primitive never knows about the product. The product-specific behavior lives in Composites.
The Compound Component Pattern
For complex UI with multiple related parts, we use the Compound Component pattern. It keeps the API clean without prop-drilling.
// Usage — reads like English, no internal state visible to the consumer
<Card>
<Card.Header>
<Card.Title>Project Alpha</Card.Title>
<Card.Badge variant="active">Live</Card.Badge>
</Card.Header>
<Card.Body>
<Card.Metric label="Uptime" value="99.97%" />
<Card.Metric label="Requests / day" value="1.4M" />
</Card.Body>
<Card.Footer>
<Button size="sm">View Dashboard</Button>
</Card.Footer>
</Card>// Implementation — shared context, no prop-drilling
const CardContext = React.createContext<{ variant: CardVariant }>({ variant: 'default' });
function Card({ children, variant = 'default' }: CardProps) {
return (
<CardContext.Provider value={{ variant }}>
<div className="rounded-xl bg-neutral-800 border border-neutral-700/50 overflow-hidden">
{children}
</div>
</CardContext.Provider>
);
}
Card.Header = function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="px-5 pt-5 pb-3 flex items-start justify-between">{children}</div>;
};
Card.Title = function CardTitle({ children }: { children: React.ReactNode }) {
return <h3 className="text-sm font-semibold text-neutral-100">{children}</h3>;
};
// etc.Layer 3: Composites With Behavior
Composites are where the product lives. They are opinionated, they know about the domain, and they are not reused across products.
// ProjectStatusCard — a composite
// Knows about Project, Status, and the business rules around them
function ProjectStatusCard({ projectId }: { projectId: string }) {
const project = useProject(projectId);
const { mutate: archive } = useArchiveProject();
if (!project) return <ProjectStatusCardSkeleton />;
return (
<Card>
<Card.Header>
<Card.Title>{project.name}</Card.Title>
<Card.Badge variant={statusVariant(project.status)}>
{project.status}
</Card.Badge>
</Card.Header>
<Card.Body>
<Card.Metric label="Last deploy" value={formatRelative(project.lastDeployAt)} />
<Card.Metric label="Uptime" value={`${project.uptimePercent}%`} />
</Card.Body>
<Card.Footer>
<Button size="sm" onClick={() => archive(projectId)}>Archive</Button>
</Card.Footer>
</Card>
);
}A Composite can call hooks. A Primitive cannot.
Variant Management With CVA
As components grow, managing variants with conditional className strings becomes unmaintainable. We use class-variance-authority to encode variants as data.
import { cva, type VariantProps } from 'class-variance-authority';
const button = cva(
// Base classes — always applied
'inline-flex items-center justify-center font-mono uppercase tracking-widest transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary disabled:opacity-40 disabled:pointer-events-none',
{
variants: {
variant: {
primary: 'bg-brand-primary text-neutral-900 hover:bg-brand-primary/90',
ghost: 'bg-transparent text-neutral-300 hover:bg-neutral-700 border border-neutral-600',
danger: 'bg-semantic-error text-white hover:bg-semantic-error/90',
},
size: {
sm: 'h-8 px-3 text-[10px]',
md: 'h-10 px-5 text-[11px]',
lg: 'h-12 px-7 text-[12px]',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof button> {}
export function Button({ variant, size, className, ...props }: ButtonProps) {
return <button className={button({ variant, size, className })} {...props} />;
}Adding a new variant is a data change, not a logic change. The type system enforces that all variant combinations are valid.
Accessibility as a Build Constraint
Accessibility is not a post-launch audit item. It is a component-level contract.
Every interactive primitive in our system satisfies:
- Keyboard navigation -- all interactions reachable and operable without a mouse
- Focus management -- focus moves logically through the UI; modals trap focus; on close, focus returns to the trigger
- ARIA semantics -- roles, states (
aria-expanded,aria-selected,aria-disabled), and labels are correct - Contrast ratios -- text meets WCAG AA (4.5:1 for body, 3:1 for large text)
// Accessible icon button — common failure point
// An icon-only button with no accessible label fails screen readers
function IconButton({ icon: Icon, label, ...props }: IconButtonProps) {
return (
<button
aria-label={label} // screen reader text
title={label} // tooltip on hover
className="p-2 rounded-lg hover:bg-neutral-700 transition-colors focus-visible:ring-2 focus-visible:ring-brand-primary"
{...props}
>
<Icon className="w-4 h-4" aria-hidden="true" /> {/* icon is decorative */}
</button>
);
}The Governance Rule
A design system without governance degrades into a component graveyard. Our rule is simple:
No new component is added to the system without a documented usage contract. The contract answers three questions:
- When should this component be used?
- When should it NOT be used?
- What are the required vs optional props, and why?
Components without contracts are product code. They live in the product. They do not graduate to the system until they have been used in at least two distinct product contexts and their API has stabilized.
This keeps the system small, coherent, and trustworthy.