Skip to main content

Expanding the Design System: UI Components Part 2

Jan 29, 20264 min readReact, TypeScript, Storybook, Forms, Component Library, Tailwind CSS

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.

Checkbox — DefaultOpen in Storybook

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.

Select — With LabelOpen in Storybook

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.

Switch — DefaultOpen in Storybook

Textarea

A multi-line text input with validation states, character counting, and auto-resize capabilities. Shares the same validation patterns as the Input component.

Textarea — DefaultOpen in Storybook

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.

Alert — DefaultOpen in Storybook

Badge

A simple label component for status indicators and tags. Supports multiple semantic variants: default, accent, success, warning, error, and info.

Badge — DefaultOpen in Storybook

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.

Tooltip — DefaultOpen in Storybook

ProgressBar

A visual progress indicator for loading states and completion tracking. Includes proper ARIA attributes for accessibility.

ProgressBar — DefaultOpen in Storybook

Spinner

A loading spinner for async operations. Complements the Loading component from Part 1 with a more compact visual indicator.

Spinner — DefaultOpen in Storybook

Layout Components

Card

A composable card component with sub-components: CardHeader, CardTitle, and CardContent. Supports elevation variants and configurable padding.

Card — Full CardOpen in Storybook

Stack

A flexbox-based layout primitive for vertical or horizontal stacking with consistent spacing. Eliminates the need for manual gap utilities.

Stack — VerticalOpen in Storybook

Tabs

An accessible tab interface with proper ARIA roles: tablist, tab, and tabpanel. Supports keyboard navigation and controlled/uncontrolled modes.

Tabs — DefaultOpen in Storybook

Section

A semantic section wrapper for page content organization. Provides consistent vertical spacing and optional headings.

Section — DefaultOpen in Storybook

Divider

A simple horizontal rule for visual separation between content sections.

Divider — DefaultOpen in Storybook

AspectRatio

A container that maintains a specified aspect ratio for its content. Useful for responsive images and video embeds.

AspectRatio — DefaultOpen in Storybook

Spacer

A utility component for adding consistent whitespace. Available in preset sizes for maintaining vertical rhythm.

Spacer — DefaultOpen in Storybook

PageContainer

A top-level page wrapper that combines Container with page-specific padding and max-width constraints.

PageContainer — DefaultOpen in Storybook

Component Count

The design system now includes 24 components across four categories:

CategoryComponents
FormButton, Input, Checkbox, Select, Switch, Textarea
FeedbackAlert, Badge, Loading, Modal, ProgressBar, Spinner, Tooltip
LayoutAspectRatio, Card, Container, Divider, Grid, PageContainer, Section, Spacer, Stack
NavigationTabs

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:

LayerLocationDependenciesExamples
Framework-agnosticlibs/shared/ui/React, Tailwind CSSButton, Card, Modal, Badge
Next.js-specificapps/root/src/components/Next.js, shared-uiNav (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 components Done!
  • 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.