Expanding the Design System: UI Components Part 2
Building on the foundational components, this second installment adds form controls, feedback mechanisms, and layout primitives. The library now covers most common UI patterns.
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. No next/link, next/image, useRouter, or any Next.js API. This separation is intentional and creates 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 rendered as a link, it uses next/link for client-side navigation with prefetching. The shared-ui Button knows nothing about routing.
Similarly, kit components like PostCard compose shared-ui primitives (Badge, Heading, Text) with Next.js-specific concerns like image optimization and route-aware links.
React 19 migration
During the V1→V2 evolution, React 19 deprecated forwardRef. 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} />;
}This eliminated all forwardRef wrappers and the Client* wrapper pattern that was previously needed for ref compatibility at 'use client' boundaries.
Why this matters
This architecture means 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 use the same components. The framework-specific adaptation stays in the consuming app.
What I Learned
Composition beats configuration. The Card component started as a monolithic prop-based design. Breaking it into CardHeader, CardTitle, and CardContent made it far more flexible.
ARIA roles matter. The Tabs component required careful attention to tablist, tab, and tabpanel roles. Getting keyboard navigation right took multiple iterations.
Test interactions, not implementation. Storybook's play functions let me test real user flows—clicking, typing, tabbing—rather than 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 evolves with the product it serves.