Expanding the Design System: UI Components Part 2
Building on the foundational components, this second round added the form controls, feedback mechanisms, and layout primitives, so the library now covers most of the common UI patterns I reach for.
Business Impact
- Expanded the component library with form controls (Checkbox, Select, Switch) enabling rapid form development
- Maintained design consistency across all interactive elements
- Reduced time-to-implementation for new form-based features
Form Components
Checkbox
A controlled checkbox with label support. Handles both click and keyboard interactions with proper ARIA attributes for screen reader compatibility.
Select
A native select wrapper with label, error state, and validation support. Uses aria-invalid and aria-describedby to connect error messages for assistive technologies.
Switch
A toggle switch component for binary choices. Uses the switch ARIA role with proper aria-checked state management. Supports both controlled and uncontrolled usage.
Textarea
A multi-line text input with validation states, character counting, and auto-resize capabilities. Shares the same validation patterns as the Input component.
Feedback Components
Alert
A dismissible alert component with four variants: info, success, warning, and error. Uses role="alert" with aria-live="assertive" for urgent messages and role="status" with aria-live="polite" for informational content.
Badge
A simple label component for status indicators and tags. Supports multiple semantic variants: default, accent, success, warning, error, and info.
Tooltip
A hover-triggered tooltip with configurable position and delay. Uses role="tooltip" with aria-hidden state management to ensure screen readers announce content appropriately.
ProgressBar
A visual progress indicator for loading states and completion tracking. Includes proper ARIA attributes for accessibility.
Spinner
A loading spinner for async operations. Complements the Loading component from Part 1 with a more compact visual indicator.
Layout Components
Card
A composable card component with sub-components: CardHeader, CardTitle, and CardContent. Supports elevation variants and configurable padding.
Stack
A flexbox-based layout primitive for vertical or horizontal stacking with consistent spacing. Eliminates the need for manual gap utilities.
Tabs
An accessible tab interface with proper ARIA roles: tablist, tab, and tabpanel. Supports keyboard navigation and controlled/uncontrolled modes.
Section
A semantic section wrapper for page content organization. Provides consistent vertical spacing and optional headings.
Divider
A simple horizontal rule for visual separation between content sections.
AspectRatio
A container that maintains a specified aspect ratio for its content. Useful for responsive images and video embeds.
Spacer
A utility component for adding consistent whitespace. Available in preset sizes for maintaining vertical rhythm.
PageContainer
A top-level page wrapper that combines Container with page-specific padding and max-width constraints.
Component Count
The design system now includes 24 components across four categories:
| Category | Components |
|---|---|
| Form | Button, Input, Checkbox, Select, Switch, Textarea |
| Feedback | Alert, Badge, Loading, Modal, ProgressBar, Spinner, Tooltip |
| Layout | AspectRatio, Card, Container, Divider, Grid, PageContainer, Section, Spacer, Stack |
| Navigation | Tabs |
Framework Adaptation Pattern
The shared-ui library is framework-agnostic by design; it depends only on React and Tailwind CSS, with no next/link, next/image, useRouter, or any other Next.js API in sight. That separation is deliberate, and it gives me a clear two-layer architecture:
| Layer | Location | Dependencies | Examples |
|---|---|---|---|
| Framework-agnostic | libs/shared/ui/ | React, Tailwind CSS | Button, Card, Modal, Badge |
| Next.js-specific | apps/root/src/components/ | Next.js, shared-ui | Nav (uses Link, useRouter), PostCard (uses shared-ui Badge + Heading), Button wrapper (adds Link routing) |
How it works in practice
The app-level Button component (components/Button.tsx) wraps the shared-ui Button to add Next.js Link integration: when it renders as a link, it uses next/link for client-side navigation with prefetching, and the shared-ui Button underneath knows nothing about routing at all.
Kit components like PostCard work the same way, composing shared-ui primitives (Badge, Heading, Text) with the Next.js-specific concerns like image optimization and route-aware links.
React 19 migration
Somewhere between V1 and V2, React 19 deprecated forwardRef, so the shared-ui library now accepts ref as a regular prop in the component interface:
// Before (React 18 — forwardRef)
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
return <button ref={ref} {...props} />;
});
// After (React 19 — ref as prop)
function Button({ ref, ...props }: ButtonProps) {
return <button ref={ref} {...props} />;
}That cleared out every forwardRef wrapper, along with the Client* wrapper pattern I'd previously needed just to keep refs working at 'use client' boundaries.
Why this matters
Because of this architecture, the shared-ui library can be consumed by any React project, not just this Next.js app. A Vite app, a Remix app, or a React Native Web project could all pull in the same components, and the framework-specific adaptation stays in the consuming app where it belongs.
What I Learned
Composition beats configuration. The Card started life as one monolithic prop-based component, and breaking it into CardHeader, CardTitle, and CardContent made it far more flexible.
ARIA roles matter. The Tabs component needed careful attention to the tablist, tab, and tabpanel roles, and getting keyboard navigation right took me a few iterations.
Test interactions, not implementation. Storybook's play functions let me test the real user flows (clicking, typing, tabbing) instead of poking at internal state.
What's Next
Dark mode theming across all componentsDone!- Animation system with reduced motion support
- Additional form components (radio groups, date pickers)
- Documentation site with usage guidelines
A design system is never finished; it keeps evolving alongside the product it serves, and that's the point.
