How We Build Interfaces

Principles for building consistent, maintainable, and accessible user interfaces

How We Build Interfaces

Good interfaces feel invisible — users accomplish their goals without thinking about the UI. This guide covers how to think about building interfaces that are consistent, maintainable, and accessible.

The Core Question

When building UI, ask:

"What is the user trying to accomplish, and what's the simplest way to help them do it?"

Your interface should reduce friction, not add it.

Principles

1. Build with Layers

Create a hierarchy of components, from generic to specific.

The layers:

┌─────────────────────────────────────────────────────────────┐
│                       PAGES                                  │
│  Route-level components, data fetching, layouts             │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                    FEATURE COMPONENTS                        │
│  Domain-specific: UserProfile, OrderList, Dashboard         │
│  Composed from primitives, contain business logic           │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                    UI PRIMITIVES                             │
│  Generic: Button, Card, Input, Dialog, Table                │
│  No business logic, fully reusable                          │
└─────────────────────────────────────────────────────────────┘

The mistake: Building everything from scratch for each feature. You end up with five different button styles and inconsistent spacing.

The principle: Start with primitives. Compose them into feature components. Feature components become pages. Each layer depends only on the layer below.

The benefit: Changes to a primitive automatically propagate everywhere. Consistency is built-in.

2. Prefer Composition Over Configuration

Let components be assembled, not configured with dozens of props.

The mistake: A Card component with props for title, subtitle, avatar, actions, footer, headerActions, variant, showBorder, collapsible...

The principle: Create composable parts that can be arranged flexibly.

The pattern: Instead of one component with many props, create small components that compose together. A Card is composed of CardHeader, CardContent, and CardFooter. Each is simple; together they're flexible.

Why it matters:

  • Easier to understand (each piece does one thing)
  • Easier to customize (swap or add parts)
  • Easier to maintain (change one part, not the whole)

3. Design with Constraints

Use design tokens instead of arbitrary values.

The mistake: Hardcoding colors like #3b82f6 and spacing like 17px throughout the codebase. Inconsistent blues everywhere. Spacing that doesn't quite line up.

The principle: Define a finite set of values and use only those. Colors, spacing, typography, shadows — all from a predefined palette.

The benefit:

  • Consistency: Everything uses the same 8 spacing values
  • Theming: Change one token, update everywhere
  • Communication: Designers and developers share a vocabulary

Design tokens to define:

  • Colors (including semantic: success, warning, error)
  • Spacing scale (typically powers of 2 or a ratio)
  • Typography (font families, sizes, weights, line heights)
  • Shadows
  • Border radii
  • Breakpoints

4. Make Accessibility the Default

Accessibility isn't a feature — it's a quality standard.

The mistake: "We'll add accessibility later." You never do. Or you add ARIA attributes without understanding them, making things worse.

The principle: Build accessible from the start. Use semantic HTML. Handle keyboard navigation. Test with screen readers.

The basics that matter:

  • Use the right element (<button> for actions, not <div onClick>)
  • Ensure sufficient color contrast
  • Make interactive elements focusable and obvious when focused
  • Provide text alternatives for images
  • Ensure forms have proper labels

The test: Can you use your interface with only a keyboard? Can you understand it with your eyes closed?

5. Start Mobile, Grow Up

Design for the smallest screen first, then add complexity for larger ones.

The mistake: Building for desktop, then cramming it into mobile. Everything breaks at 768px.

The principle: Mobile-first means you start with the essential experience, then enhance. The mobile layout is your foundation, not an afterthought.

Why it works: It's easier to add than to remove. Starting small forces you to prioritize.

Decision Framework

"Should this be a new component?"

Create a new component when:

  • You're repeating the same markup more than twice
  • The piece has a clear, single responsibility
  • It's reusable across different contexts
  • It encapsulates meaningful behavior or styling

Keep it inline when:

  • It's used only once
  • Extracting it would just move code without simplifying anything
  • The "component" would be five lines with no logic

The test: Does this component have a clear name that describes what it is? If you can't name it, maybe it shouldn't be a component.

"Should this be a primitive or a feature component?"

Make it a primitive when:

  • It has no business logic (doesn't know about users, orders, etc.)
  • It's generic enough to use anywhere
  • It accepts standard HTML attributes
  • It's about how things look, not what they mean

Make it a feature component when:

  • It knows about your domain (UserCard, OrderList)
  • It contains business logic (format prices, calculate totals)
  • It's specific to a part of your application

"How should I handle this state?"

Local state (useState) when:

  • Only this component needs it
  • It's UI-specific (open/closed, hover, focus)
  • Resetting it when the component unmounts is fine

Lifted state (parent manages it) when:

  • Sibling components need to coordinate
  • The parent needs to know about changes
  • Multiple children need the same data

Global state (Context/store) when:

  • Many unrelated components need access
  • It persists across navigation
  • It's truly application-wide (user session, theme)

The trap: Putting everything in global state "just in case." Start local, lift when needed.

"Which styling approach should we use?"

Utility classes (Tailwind):

  • Fast iteration, visual changes without switching files
  • Works well for one-off layouts and component variants
  • Can get verbose for complex, repeated patterns

CSS Modules:

  • Scoped styles, no naming collisions
  • Full CSS power when you need it
  • Better for complex, cascading styles

CSS-in-JS:

  • Dynamic styles based on props
  • Colocation of styles with components
  • Runtime cost to consider

The principle: Pick one primary approach and be consistent. Mixing approaches leads to confusion.

"How do I handle responsive design?"

Define breakpoints based on content, not devices:

  • Don't think "iPhone, iPad, Desktop"
  • Think "When does this layout break?"

Progressive enhancement pattern:

  1. Base (mobile): Single column, stacked elements, essential features
  2. Small screens: Two columns where sensible
  3. Medium screens: Sidebars appear, more items per row
  4. Large screens: Multi-column layouts, enhanced features

The trap: Making every component responsive. Not everything needs to change at every breakpoint. Let content flow naturally; only intervene when it breaks.

Common Mistakes

The Everything Component

Cramming all functionality into one component.

Signs: Props like showHeader, showFooter, variant, mode, type, style — a component trying to be everything to everyone.

The fix: Break it into composed parts. Let the consumer assemble what they need.

Inconsistent Patterns

Different ways of doing the same thing.

Signs: Some buttons are <Button>, some are <button className="btn">, some are <div onClick>. Loading states handled differently everywhere.

The fix: Establish patterns and use them consistently. Document them if needed.

Accessibility Afterthoughts

Adding ARIA attributes to fix inaccessible markup.

Signs: <div role="button" tabIndex="0" onClick> instead of just <button>. aria-label on everything because labels weren't designed in.

The fix: Start with semantic HTML. ARIA is for complex widgets, not fixing bad markup.

Style Soup

Mixing inline styles, CSS classes, utility classes, and CSS-in-JS in the same component.

Signs: One element has className, style, and a styled() wrapper. Nobody knows where styles come from.

The fix: Pick an approach. Use it consistently. Have good reasons for exceptions.

Missing Loading and Error States

Only designing for the happy path.

Signs: Components that show nothing or break while loading. Errors that crash the whole page.

The fix: Every data-fetching component needs three states: loading, error, and success. Design all three.

Mobile as an Afterthought

Building desktop-first, then forcing it onto mobile.

Signs: Everything requires horizontal scrolling on phones. Touch targets are too small. Critical actions are hidden in hover menus.

The fix: Start with mobile. If it works on a small screen with touch, it will work everywhere.

How to Evaluate Your UI

Your interface is working if:

  • Users can accomplish tasks without asking for help
  • Components are consistent across the application
  • You can add new features using existing primitives
  • The interface works with keyboard only
  • Mobile experience is intentional, not accidental

Your interface needs work if:

  • Similar elements look or behave differently
  • Every new feature requires new styles
  • Designers redline things that don't match the system
  • Accessibility audits find basic issues
  • Users complain about mobile experience

Building a Component Library

Start Small

Don't build a design system before you need one.

Phase 1: Just use whatever framework or library you've chosen. Learn what you need.

Phase 2: Extract repeated patterns into shared components. A Button here, a Card there.

Phase 3: As patterns emerge, document them. Create a consistent API.

Phase 4: If your library grows, consider dedicated tooling (Storybook, design tokens, documentation).

The trap: Building a complete design system for a two-page app. Start simple, extract when you see repetition.

Component API Principles

Props should be obvious: variant="primary" not v="p"

Extend native elements: A Button should accept anything a <button> accepts

Sensible defaults: The most common use case should require the fewest props

Consistent naming: If one component uses size="sm", they all do

Forward refs: Interactive components should be focusable and ref-able

Theming

How Themes Work

┌───────────────────────┐
│    Theme Definition   │
│  (tokens, variables)  │
└───────────┬───────────┘
┌───────────────────────┐
│   Theme Application   │
│   (CSS vars, class)   │
└───────────┬───────────┘
┌───────────────────────┐
│     Components        │
│  (read theme values)  │
└───────────────────────┘

The pattern:

  1. Define themes as collections of token values
  2. Apply themes by setting CSS custom properties
  3. Components use those properties, never hardcoded values

Theme switching: Change the CSS custom properties, components update automatically.

Dark Mode Considerations

  • Not just inverting colors — some colors need different mappings
  • Images and shadows may need adjustment
  • Test with actual dark mode, not just inverted colors
  • Respect system preferences as the default