Skip to main content

Expanding the Design System: UI Components Part 2

An abstract composition of colorful geometric blocks
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 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.

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 — VariantsOpen 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, 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:

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 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 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 keeps evolving alongside the product it serves, and that's the point.