Skip to main content

UI Strategy & Design System

@doc-version: 16.1.0 @last-updated: 2026-01-01 This guide explains Visita’s UI strategy and design system architecture. It defines how we build interfaces that are civic-appropriate, accessible, performant, and maintainable.

Overview

Core Principle: Our UI strategy balances control with efficiency. We own our design, behavior, and accessibility while leveraging proven tools for foundation.

The UI Strategy Question

When designing a UI strategy, you’re deciding:
Who controls design, behavior, accessibility, and future change β€” you, or a library?
Every UI strategy exists on this spectrum:
Fast & Opinionated  ←────────────→  Flexible & Future-Proof

The Four UI Strategies

Examples: MUI, Ant Design Pros:
  • Very fast to start
  • Lots of pre-built components
Cons:
  • Heavy bundle size
  • Highly opinionated design
  • Difficult to customize deeply
  • Generic β€œSaaS” appearance
Why This Doesn’t Work for Visita:
  • ❌ Long-term civic platforms need custom identity
  • ❌ Heavy frameworks conflict with performance goals
  • ❌ Opinionated designs don’t reflect civic seriousness
  • ❌ Difficult to evolve with changing requirements

🟨 Strategy 2: Build Everything Yourself (Not Ideal Alone)

Pros:
  • Total control over everything
  • Lightweight bundle
Cons:
  • Accessibility is extremely difficult to get right
  • Reinventing wheels slows development
  • Requires significant UI expertise
Why This Isn’t Sufficient:
  • ❌ Civic platforms require excellent accessibility
  • ❌ Small team can’t maintain all primitives
  • ❌ Risk of inconsistent behavior across components
This is the modern, professional approach we’ve chosen. Stack:
  • Radix UI β†’ Behavior + Accessibility foundation
  • Tailwind CSS β†’ Styling control
  • shadcn/ui β†’ Starter components (copy, not dependency)
Pros:
  • βœ… Full control over design and behavior
  • βœ… Accessible by default (Radix handles ARIA, keyboard nav, focus management)
  • βœ… No vendor lock-in (we own the code)
  • βœ… Scales beautifully with the platform
  • βœ… Aligns with civic design principles
Why This Works for Visita:
  • βœ… We control markup and styling
  • βœ… We inherit battle-tested accessibility
  • βœ… We can refactor without breaking contracts
  • βœ… Design reflects civic values, not generic SaaS

🟦 Strategy 4: Design System First (Future Evolution)

This is where we’re heading as we mature:
  • Design tokens as single source of truth
  • Component contracts and APIs
  • UI governance and contribution guidelines
  • Automated testing and visual regression
Not needed on day one β€” but Strategy 3 grows into this naturally.

Our UI Stack

Next.js (App Router)    // Framework
TypeScript (Strict)     // Language safety
Tailwind CSS            // Styling control
Radix UI                // Accessibility + behavior
shadcn/ui               // Component templates

Why This Stack Works

  1. You Control Markup β€” No magic divs or unwanted DOM structure
  2. You Control Styling β€” Tailwind utilities, not pre-styled components
  3. You Inherit Accessibility β€” Radix handles the hard parts (ARIA, keyboard nav, focus)
  4. You Can Refactor β€” Own the code, change what you need
  5. Civic-Appropriate β€” No β€œstartup” aesthetic imposed by framework

Component Hierarchy

The Three-Layer Architecture

Components are structured by responsibility to prevent spaghetti code:
/components
 β”œβ”€β”€ ui/              ← Primitives (generic, reusable)
 β”‚    β”œβ”€β”€ Button.tsx
 β”‚    β”œβ”€β”€ Input.tsx
 β”‚    β”œβ”€β”€ Modal.tsx
 β”‚    └── Dropdown.tsx
 β”‚
 β”œβ”€β”€ layout/          ← Structural (persistent UI)
 β”‚    β”œβ”€β”€ Header.tsx
 β”‚    β”œβ”€β”€ Sidebar.tsx
 β”‚    └── Footer.tsx
 β”‚
 β”œβ”€β”€ domain/          ← Visita-specific (business logic)
 β”‚    β”œβ”€β”€ WardCard.tsx
 β”‚    β”œβ”€β”€ CrimeMap.tsx
 β”‚    β”œβ”€β”€ BusinessListing.tsx
 β”‚    └── PoliticalProfile.tsx

The Golden Rule

UI components must not know business logic. Domain components may use UI components. Never the reverse.
Example:
// βœ… GOOD: Domain component uses UI primitive
function WardCard({ ward }: { ward: Ward }) {
  return (
    <Card>
      <CardHeader>
        <Text variant="heading">{ward.name}</Text>
        <Badge variant="info">{ward.code}</Badge>
      </CardHeader>
      <CardContent>
        <WardStats ward={ward} />  // Domain-specific stats component
      </CardContent>
    </Card>
  );
}

// ❌ BAD: UI component knows about wards
function Button({ wardCode }: { wardCode: string }) {
  // Never do this - Button shouldn't know what a ward is
}

Design Philosophy

Visita’s UI Must Feel:

  1. Civic β€” Serious, trustworthy, institutional
  2. Calm β€” Not flashy or attention-grabbing
  3. Structured β€” Clear hierarchy and organization
  4. Neutral β€” Data-respecting, not emotionally manipulative
  5. Accessible β€” Works for all citizens

What We Avoid:

  • ❌ Over-rounded β€œstartup bubbles” (excessive border-radius)
  • ❌ Flashy gradients and animations
  • ❌ Loud, attention-seeking visuals
  • ❌ Trendy designs that age poorly
  • ❌ Engagement-optimized patterns (infinite scroll, pull-to-refresh for its own sake)

Design Principles

  1. Clarity Over Cleverness β€” Simple, readable interfaces
  2. Function Over Form β€” Beauty emerges from purpose, not decoration
  3. Consistency Over Novelty β€” Predictable patterns build trust
  4. Accessibility Over Aesthetics β€” If it’s not accessible, it’s not done
  5. Civic Responsibility Over Engagement β€” Inform, don’t manipulate

Design Tokens

Foundation Before Components

Design tokens are the atomic design decisions everything else builds on.

🎨 Colors (Semantic, Not Aesthetic)

// Not just pretty colors - they mean something
primary      β†’ Civic blue/green (trust, stability)
secondary    β†’ Muted neutral (background, subtle)
danger       β†’ Red (alerts, crime, warnings)
warning      β†’ Amber (caution, attention needed)
success      β†’ Green (positive, confirmed)
surface      β†’ White/light gray (cards, panels)
muted        β†’ Gray text (metadata, secondary info)

πŸ”² Border Radius

radius-sm    β†’ 4px   (inputs, small elements)
radius-md    β†’ 8px   (cards, panels)
radius-lg    β†’ 12px  (modals, large containers)

πŸ“ Spacing

space-xs     β†’ 4px
space-sm     β†’ 8px
space-md     β†’ 16px
space-lg     β†’ 24px
space-xl     β†’ 32px
These become design law β€” enforced through Tailwind and code review.

Styling Strategy

How We Use Tailwind

Use Tailwind for:
  • βœ… Spacing and layout (p-4, mt-2, grid-cols-3)
  • βœ… Colors (bg-surface, text-muted, border-danger)
  • βœ… Responsive design (md:p-6, lg:flex)
  • βœ… Typography (text-sm, font-medium)
Do NOT:
  • ❌ Create massive custom CSS files
  • ❌ Inline styles everywhere (style={{ color: 'red' }})
  • ❌ Over-abstract class names too early (classNames="card-primary")
  • ❌ Create one-off utilities for every variation

Design Token Implementation

// βœ… GOOD: Using semantic tokens
function Alert({ variant }: { variant: 'info' | 'danger' }) {
  return (
    <div className={`
      p-4
      rounded-md
      border
      ${variant === 'danger' 
        ? 'bg-red-50 border-red-200 text-red-800' 
        : 'bg-blue-50 border-blue-200 text-blue-800'
      }
    `}>
      {/* Content */}
    </div>
  );
}

// ❌ BAD: Hardcoded values
function Alert({ variant }: { variant: 'info' | 'danger' }) {
  return (
    <div style={{
      padding: '16px',
      borderRadius: '8px',
      backgroundColor: variant === 'danger' ? '#fee' : '#eef'
    }}>
      {/* Content */}
    </div>
  );
}

Accessibility Strategy

Non-Negotiable for Civic Platforms

Accessibility is not optional. Civic platforms serve all citizens, including those with disabilities.

What Radix Provides

Radix UI handles the hard parts of accessibility:
  1. Keyboard Navigation β€” All components work without a mouse
  2. Focus Management β€” Proper focus traps and restoration
  3. ARIA Roles β€” Correct semantic HTML and ARIA attributes
  4. Screen Reader Support β€” Announcements and state changes
  5. Color Contrast β€” Meets WCAG AA standards

Our Accessibility Requirements

Every component must support:
  • βœ… Keyboard navigation (Tab, Enter, Escape, Arrow keys)
  • βœ… Focus rings (visible focus indicators)
  • βœ… Screen readers (ARIA labels, live regions)
  • βœ… High contrast mode (4.5:1 minimum contrast)
  • βœ… Reduced motion preferences

Accessibility Checklist

Before shipping a component:
  • Test with keyboard only (no mouse)
  • Test with screen reader (NVDA, VoiceOver, or JAWS)
  • Verify color contrast ratios
  • Check focus management in modals/dialogs
  • Ensure form labels are properly associated
  • Test with browser zoom (200%)

Performance Strategy

UI-Level Performance

Ask these questions early:
  1. Is this component above the fold? β†’ Load immediately
  2. Can this load later? β†’ Lazy load
  3. Is it static or interactive? β†’ Server vs client component
  4. Does it have heavy dependencies? β†’ Dynamic import

Tactics

1. Server Components by Default

// βœ… GOOD: Server component (default)
export default async function WardPage({ params }: { params: { code: string } }) {
  const ward = await fetchWard(params.code);  // Server-side fetch

  return (
    <div>
      <WardHeader ward={ward} />  {/* Static content */}
      <WeatherWidget data={ward.weather} />
    </div>
  );
}

2. Client Components Only When Needed

'use client';
// Only use client when you need:
// - useState, useEffect
// - click handlers
// - browser APIs
export default function SignalFilter() {
  const [selectedCategory, setSelectedCategory] = useState('all');

  return (
    <Select value={selectedCategory} onValueChange={setSelectedCategory}>
      {/* Options */}
    </Select>
  );
}

3. Dynamic Imports for Heavy Components

import dynamic from 'next/dynamic';

// Lazy load heavy components
const DirectoryMap = dynamic(
  () => import('@/components/ward/DirectoryMap'),
  {
    loading: () => <MapSkeleton />,  // Show skeleton
    ssr: false  // Don't SSR map (browser-only)
  }
);

export default function WardPage() {
  return (
    <div>
      <WeatherWidget />  {/* Above-the-fold */}
      <DirectoryMap />   {/* Below-the-fold, lazy load */}
    </div>
  );
}

4. Skeleton Loaders

// Show skeleton while data loads
function WardSkeleton() {
  return (
    <div className="animate-pulse space-y-4">
      <div className="h-8 bg-gray-200 rounded w-1/3"></div>
      <div className="h-32 bg-gray-200 rounded"></div>
      <div className="space-y-2">
        <div className="h-4 bg-gray-200 rounded"></div>
        <div className="h-4 bg-gray-200 rounded w-5/6"></div>
      </div>
    </div>
  );
}

Page-Building Rule

Pages Compose, Not Design

A page should orchestrate components, not contain UI logic.
// βœ… GOOD: Page composes domain components
export default function WardDashboard({ params }: { params: { code: string } }) {
  const ward = await fetchWard(params.code);

  return (
    <DashboardLayout>
      <WardHeader ward={ward} />
      <WardStats ward={ward} />
      <WardFeed wardCode={params.code} />
    </DashboardLayout>
  );
}

// ❌ BAD: Page contains UI logic
export default function WardDashboard({ params }: { params: { code: string } }) {
  const ward = await fetchWard(params.code);

  return (
    <div className="p-6 max-w-6xl mx-auto">
      <div className="flex items-center justify-between mb-6">
        <h1 className="text-2xl font-bold">{ward.name}</h1>
        <div className="flex gap-2">
          {/* Page shouldn't design UI */}
        </div>
      </div>
      {/* More inline UI logic */}
    </div>
  );
}

UI Governance Rules

Prevent Chaos with Clear Rules

Adopt these rules and enforce them in code review:
  1. No Page-Level Styling β€” Pages compose, components style
  2. No Business Logic in ui/ β€” UI components are dumb
  3. No State in Layout Components β€” Layouts are static structure
  4. No Copying JSX Across Pages β€” Extract to shared component
  5. No Component Without Clear Purpose β€” Every component solves a problem
  6. Extend, Don’t Fork β€” Add variants to existing components
  7. Accessibility First β€” No component ships without a11y testing

Component Lifecycle

When you need a new component:
  1. Does it belong in ui/, layout/, or domain/?
  2. Can an existing component be extended?
  3. What are its states? (loading, empty, error)
  4. What props does it need?
  5. Is it accessible?
  6. Is it performant?
  7. Does it follow design tokens?

How This Fits Visita

Visita Needs:

  1. Trust β€” UI must feel reliable and institutional
  2. Clarity β€” Information must be easy to understand
  3. Seriousness β€” This is civic infrastructure, not entertainment
  4. Accessibility β€” Must serve all citizens
  5. Scalability β€” Must grow with the platform

Our UI Strategy Delivers:

  • βœ… Calm Design β€” No flashy animations or trendy effects
  • βœ… Structured Layout β€” Clear information hierarchy
  • βœ… Neutral Aesthetic β€” Data-respecting, not emotionally manipulative
  • βœ… Accessible by Default β€” Radix ensures baseline accessibility
  • βœ… Maintainable β€” Clear separation of concerns
  • βœ… Civic-Appropriate β€” Reflects seriousness of civic intelligence

Summary

Strategy

Architecture

Accessibility

Performance


Next Steps


Status: Active
Applies To: All UI development
Reviewed: January 2026