Zebra Labs Frontend Default Design — Outline (v1)

by William Warne, Software Engineer | Fractional CTO | Founder

Introduction

This design document describes our default approach to frontend code design and architecture, focused on outcomes such as performance, maintainability, accessibility, and developer speed.

Every choice in this outline should tie back to our goals and the trade‑offs we’ve reasoned through.

Aim

To outline a practical, opinionated baseline for building frontends at Zebra Labs with every decision connecting to higher‑level goals we have reasoned about in advance that shall form a default base which we continually improve and from which specific deviations can be reasoned.

Our Goals

  1. Deliver a mobile compatible experience.

  2. Optimize for fast user experiences and reliable rendering.

  3. To keep features cohesive and scaling manageable.

  4. Minimize complexity.

  5. Respectable performance and accessibility — covering common web app needs and providing a default base to which specific deviations can be applied.

Technology Constraints & Baseline

Next.js 14+ (App Router), React 18+, TypeScript strict mode — chosen for maturity, ecosystem, and SSR capabilities.

TanStack Query for fetching/caching — avoids over‑engineering state.

Supabase for auth/realtime as a default reference, but architecture allows replacement.

PWA‑ready (service worker, manifest) to enable offline and installable experiences.

At Zebra Labs we dedicate ourselves to specific outcomes. The tools above are chosen for a variety of reasons and we're confident these are a great choice for the development of the majority of web apps now and in the foreseeable future. Whether they are 'the best' tools in all respects is debatable, a debate we welcome as long as the main goal is not impacted.

Code Design

Folder & Code Organization

To enable fast onboarding, domain encapsulation, modularity, testability, and scalability we organize by feature/domain. Keeping each domain self-contained and representative of a real business capability/domain.

Standard Domain Layers:

  • ui/ — UI components specific to the domain.
  • hooks/ — application layer logic.
  • services/ — infra/IO (API calls, storage, etc.).
  • services/api — API requests, client, and mapping.
  • types/ — contracts and shared types.
  • tests/ — unit, integration, and contract tests.
  • index.js —
Example: /app/activities/
  ui/
    ActivityOverview.jsx
    ActivityList.jsx
  hooks/
    useActivities.js      # list/read use-case
    useActivity.js        # detail/read use-case
    useCreateActivity.js  # write/mutation use-case
  services/
    api/
      client.js           # fetch wrapper, base URL, auth
      requests.js         # getActivities(), getActivity(id), createActivity()
      mappers.js          # API <-> ViewModel
    realtime.js           # Supabase channels -> cache updates
  types/
    activity.js           # Activity, ActivityListParams, etc.
  tests/
    activities.contract.test.js
  index.js                # <— public API surface for the domain

Notice there is no reference to roots or pages. Why the split? This aids in domains to remain agnostic of frameworks and potentially volatile UI design. Given that in NextJS, only the root app/ drives routing, this is a welcomed convention for the NextJS ecosystem.

Here we see an example app/ directory.

app/
  (home)/                  # domain agnostic route group
    page.jsx               # imports components from any domain/s
    layout.jsx
    loading.jsx
    error.jsx
  (activities)/            # route group for encapsulation (not in URL)
    activities/
      page.jsx             # imports ActivitiesList component from domain
      loading.jsx
      error.jsx
    activities/
      [id]/
        page.jsx
  layout.jsx

Guidelines:

  • No cross‑domain imports except via a public API.
  • Maintain aggregate boundaries.
  • Follow ubiquitous language to ensure clarity across teams.

Design System, Styling & Theming

Our UI layer is driven by design tokens (color, spacing, typography, radii, shadows, motion), exposed as CSS variables and consumed by components. Themes are just different token sets (e.g., light, dark, high-contrast, brand/tenant variants). Components don’t hard-code colors or sizes; they reference tokens.

Generating Themes

We produce accessible, cohesive token sets quickly, whether starting from Figma, AI-generated prototypes (Lovable/Codev), or manual creation.

The output is always the same:

  • Token JSON: one file defining color, typography, spacing, radii, shadows, motion tokens.
  • CSS Variables: a small stylesheet that maps those tokens to CSS custom properties in three scopes:
    • :root → the default (typically light) values
    • .dark → overrides for dark mode
    • .hc → overrides for a high-contrast theme Meaning: components always read var(--token); switching theme = toggling a scope class.
  • Theme QA report: a tiny checklist (can be a markdown file) reporting:
    • contrast results for text/icon on key surfaces,
    • focus/hover/active visibility,
    • reduced-motion behavior verified.

Why JSON over Tailwind config: JSON is tool-agnostic — works across Figma, web, native. Tailwind config is framework-specific. If functions or comments are needed, you can keep a JS source and export JSON from it.

Token categories we standardize:

  • Color: brand, surfaces (background layers 1/2/3), text (default/muted/inverse), borders, semantic (success/warning/danger/info).
  • Typography: families, scale steps, line heights, weights.
  • Spacing: a small modular scale.
  • Radii & Shadows: 3–5 steps each.
  • Motion: 3–4 durations + 1–2 easing curves, with reduced-motion alternatives.

Workflow A - From Figma

  1. Audit Figma designs for usability:
  • Colors/typography defined as Figma Styles or Variables (ensures reusability).
  • Interactive states are present for all components (hover/focus/active/disabled).
  • Dark mode variant present if needed.
  • Contrast check: use Stark or Axe plugin to verify WCAG AA — text ≥ 4.5:1, icons ≥ 3:1, focus outlines ≥ 3:1.
  1. Export tokens via Tokens Studio or Figma Variables → JSON.
  2. Normalize taxonomy:
  • Map arbitrary style names to our canonical names (Brand/500color.brand.500, Text/Mutedcolor.text.muted).
  • Merge or delete visually duplicate tokens.
  1. A11y pass: (If I was to go through this process I wouldn't know what I'm trying to achieve with this step or how I'd need to go about it?)
  • Handled by automated testing and nothing required at this stage.
  • [TODO: Probably best if this is handled with automated tests. Determine how to do this to meet the requirements outlined.]
    • Text contrast: AA ≥ 4.5:1 (normal text) / 3:1 (≥18px or 14px bold).
    • Icon/graphic essential contrast: AA ≥ 3:1.
    • Focus indicator: visible, ≥3:1 contrast against adjacent colors, not just color-only (add outline/shape).
    • Links must differ from body text by more than color (e.g., underline).
  1. Generate CSS variable files: [Is this just manually creating a file/files - how will we typically go about this?]
  • Create tokens.css (or theme.css).
  • Place defaults in :root, dark overrides in .dark, high-contrast overrides in .hc.
  • Keep names stable between themes.
  1. QA in the component gallery: [REJECTED in favour of automated tests].
  • Render all UI components in all states in Storybook or a gallery page.
  • Purpose: catch issues that automated contrast checks miss (e.g., hover ring invisible on some surfaces).

Workflow B - From AI tools (Lovable/Codev) // TODO: Improve this section.

  1. Ingest their Tailwind theme (colors/radius/shadows).
  2. Convert/clean: prefer HSL for colors, collapse near-dupes.
  3. A11y audit (same checks as above).
  4. Map into semantic palette.
  5. Publish tokens JSON, CSS vars, and QA report.

Workflow C — Manual creation (last resort) // TODO: Improve this section.

  1. Start from base theme; pick brand hue, create a 9-step scale.
  2. Define surface layers with text/border colors passing WCAG AA.
  3. Set typography; test in multiple scripts.
  4. Accessibility pass + gallery QA; publish.

TailwindCSS Configuration

Multi-tenant & environment notes:

  • Runtime theme loading: the server selects which token file to inject based on tenant/environment; the client switches dark/high-contrast by toggling a class on <html> or <body>.
  • On-prem:
    • No remote font hosting: some private deployments block Google Fonts/Adobe Fonts; use system fonts or self-host.
    • Self-host assets: store images/fonts locally, not on external CDNs, to comply with security or offline requirements.
    • No-CDN palette: ensure colors don’t rely on external image assets (gradients, patterns).
  • Honor user preferences:
    • prefers-color-scheme → start in light/dark to match the OS. Ensure (@media (prefers-color-scheme: dark)) or JS (window.matchMedia) set the initial theme class accordingly.
    • prefers-reduced-motion → reduce or disable non-essential motion. Ensure wrap transitions/animations in @media (prefers-reduced-motion: no-preference); in JS, skip non-essential animations if matchMedia returns reduce.

Other than this general best practices advised by Tailwinds official documentation - https://tailwindcss.com/

Additional tools

  • Tailwind Plus
  • Shadcn

Rendering Strategy

Any rendering strategy needs to balance SEO, personalization, design, latency, hydrations costs, and infrastructural requirements.

Our rendering strategy is set up to achieve:

  • A fast user experience
    • Responsive interactivity – components respond immediately once hydrated.
    • Predictable interactivity – avoid long gaps between render and usable UI.
    • Fast first paint — reduce perceived load time and improve user engagement.
  • Good SEO - to ensure discoverability and ranking.

We default to SSR for most components to deliver perceived speed and SEO benefits, marking highly interactive components for CSR where appropriate.

Moving from SSR -> CSR

Typical reasons:

  • Reduced SEO importance.
  • Heavy personalization best handled client-side.
  • Desire to reduce server load.

Required changes:

  1. Data fetching moves client-side -> Replace server loaders with client hooks (e.g., TanStack Query). Pass initialData only if SEO still matters.

  2. Loading states become mandatory -> Implement full-page and component-level fallbacks where SSR previously delivered complete HTML.

  3. Error handling becomes user-facing -> Inline retries and contextual messages replace opaque 500 pages.

  4. Auth & permissions shift to client edge -> SSR delivers secure tokens; client fetch layer must gracefully handle 401/403.

Risks to manage:

  • Slower first paint if bundles are large or data is slow → mitigate with code-splitting and skeletons.
  • Duplicate fetching (server + client) → avoid by removing server fetches or disabling auto-refetch when initialData is fresh.
  • SEO regression → consider pre-rendering shell/above-the-fold HTML or dynamic rendering for crawlers.

Moving from CSR to subscriptions

Typical reasons:

  • Strong SEO needs.
  • Faster time-to-content.
  • Pre-rendered personalization.

Required changes:

  1. Data fetching moves server-side -> Promote queries into Server Components or route handlers; pass data to Client Components via props or hydrated caches.

  2. Loading states reduced -> Most content arrives ready; keep micro-skeletons only for client-only sub-queries.

  3. Hydration mismatch prevention -> Ensure deterministic server/client output (no Date.now(), random IDs, or browser APIs during initial render).

  4. Increased server responsibility -> Cache safely (ISR/edge), and stream slow regions to avoid blocking TTFB.

Risks to manage:

  • Blocking TTFB if critical data fetches are slow → split critical vs non-critical regions, use streaming.
  • Leakage of browser-only logic into server → confine to Client Components and guard access.

Streaming Partial HTML

Streaming sends HTML in chunks as it becomes ready, rather than waiting for the entire page to complete.

In Next.js: Enabled through React’s Suspense boundaries, allowing some UI to render while other parts wait for data.

Our design is established to achieve:

  • Improved first paint for SEO and user perception.
  • Targeted UX improvements - where slow components would otherwise block the whole page.

By default we design and implement full-page loading states for improved first paint load times and user perception. From here, key pages/components needing component-level loading states due to high hydration cost or design needs are identified and worked on.

This helps us avoid over-fragmentation — too many boundaries make UIs feel “bitty” and add maintenance cost.

Data‑Fetching, Caching, and Error Handling

Data fetching must balance freshness, performance, and robust error recovery, while avoiding redundant requests or stale UI.

Key factors:

  • Data volatility – how quickly it changes and the risk of showing stale data.
  • Personalization needs – whether data must be scoped per user/session.
  • Transport & storage constraints – some systems may require strict client/server boundaries or security restrictions.

Our default design allows each component to encapsulate and manage its own data needs while adhering to a predictable, composable caching strategy.

Caching Conventions

Query key standards:

  • Pattern: [domain, entity, {params}] -> Example: ['activities', 'list', { status, page }]
  • Use only stable, JSON-serializable params with fixed key order.
  • Never use raw objects as root keys.

Stale time defaults (adjust per app):

  • Static/rarely changing – 30m to 24h (e.g., feature flags, legal terms).
  • User lists/tables – 60s to 5m with background refetch on focus.
  • Realtime entities – Infinity with Supabase events for manual invalidation.

Mutation invalidation:

  • Invalidate the narrowest possible key (['activities', 'detail', id]), not broad wildcards.
  • Use optimistic updates for latency-sensitive flows; rollback on error.

Pagination:

  • Prefer cursor-based pagination; prefetch the next page on near-scroll.
  • Cancel in-flight prefetches on unmount to save bandwidth.
  • Maintain separate keys for each filter/sort to prevent cache poisoning.

Background Updates

Handled via TanStack Query:

  • refetchOnWindowFocus: true for most lists/details.
  • refetchInterval for polling scenarios.
  • Trigger invalidation on mutation onSuccess.

SSR Trade-offs & Best Practices

When to SSR:

  • Critical page data (session, primary content) – SSR fetch in Server Component.
  • Optional/low-priority data – client-side fetch with loading states.
  • Use streaming to deliver partial content where latency is high.

Hydration:

  • When SSR provides initialData, React Query hydrates the cache to avoid an immediate refetch.
  • Avoid duplicate fetching by disabling auto-refetch when data is fresh.

Realtime (Supabase)

We handle live updates through:

  • Supabase Realtime channels → targeted cache invalidation.
  • Background polling where websockets aren’t viable.
  • Mutations auto-invalidating related queries.

Error Handling Patterns

Error categories & responses:

  • Auth (401/403) – refresh session or redirect to login; show “session expired” inline.
  • Validation (422/400) – show field-level messages; avoid toasts for form errors.
  • Rate limiting (429) – backoff + friendly retry notice; optionally queue retry.
  • Server (5xx) / Network errors – inline retry UI, plus monitoring logs.

Display rules:

  • Inline errors where the data is shown (preferred).
  • Toasts for global, non-field actions; rate-limit to avoid spam.
  • Error boundaries for fatal render failures → safe fallback + retry.

Retry policy:

  • Default: 2–3 retries with exponential backoff.
  • No retries on 4xx except 408/429.
  • Critical data may use longer backoff or circuit breaker to protect the API.

UI Design Considerations

Coming soon — UI Design Considerations

This section is being polished.

Component Design

Our goal is to make components composable, type-safe, accessible, and easy to evolve without ripple effects.

  • Composition over inheritance – build small, focused components and compose them into more complex UI.
  • Type-safe contracts – use TypeScript for props and events; prefer discriminated unions for complex prop patterns.
  • Accessibility-first – semantic HTML, ARIA roles, keyboard navigation, and focus management.
  • State locality – keep state as close as possible to where it’s used.
  • Derived state – compute values with useMemo or selectors rather than duplicating source data.

Key concepts: Semantic HTML: HTML that conveys meaning through the correct elements (<header>, <nav>, <main>, <section>, <button>, <form>, etc.). This gives browsers, assistive technologies, and search engines the right context without extra hints. ARIA roles: Attributes (role="button", role="dialog") that provide additional accessibility semantics when native HTML elements alone aren’t sufficient (e.g., custom-styled components built from <div>s). Presentational components (UI-only): receive all data and callbacks via props. No side-effects, no business logic. Pure UI. Controller components (logic-aware): handle fetching, state, and orchestration. They pass ready-to-render props down to presentational components. Error boundaries: at meaningful component tree levels to catch runtime errors without taking down the whole page. Skeleton states: for perceived performance; prefer lightweight CSS-based placeholders over heavy JS animations. Fallbacks: for unavailable data or failed loads, ensuring the UI always renders something predictable.

Component Design Guide

  1. Define the component’s purpose and its consumers (who/what will use it).
  2. Identify the data & events it needs (props, callbacks).
  3. Choose whether it’s a presentational (UI only) or controller (logic aware) component.
  4. Implement with semantic HTML and a11y built in.
  • Use the correct native element for the job.
  • Add ARIA attributes when semantics can’t be expressed natively.
  • Ensure focus order and keyboard operability from the start.
  • Don’t rely solely on color to convey meaning
  1. Write type-safe props with clear defaults.
  2. Add states (loading, error, empty) and fallbacks.
  3. Test for a11y:
  • Automated checks with axe or jest-axe (covers contrast, roles, labels, aria attributes).
  • Interactive pattern checks = manually verify that interactive flows (dialogs, menus, forms) are fully operable with keyboard and read correctly in a screen reader.

Component Libraries

Selection Criteria: To avoid risky or unmaintained dependencies, check:

  • Maintenance & stability – actively maintained with a healthy release cadence.
  • TypeScript support – first-class typings and generics.
  • SSR compatibility – works without browser APIs at render time.
  • Accessibility – follows WAI-ARIA guidelines.
  • Customisability – supports theming and design tokens. A universal “package checker” doesn’t really exist for all of the above — you’ll still need a quick manual review. However, you can partially automate checks with:
  • npm trends for usage stats,
  • snyk.io for security vulnerabilities,
  • bundlephobia for package size,
  • axe-core for quick a11y review of example components.

Golden-path Examples:

  • UI primitives: Radix UI / Headless UI for accessible building blocks.
  • Charts: Recharts or similar, SSR-safe with dynamic import.
  • Date handling: date-fns or dayjs (avoiding Moment.js because it’s legacy, large, mutable, and in maintenance mode only)

Forms & Validation

Forms are high-touch interactive surfaces. They must be type-safe, accessible, and consistent across the app.

Stack:

  • React Hook Form – lightweight, performant form state management.
  • Zod – schema validation with TypeScript inference for runtime + compile-time safety.

Patterns:

  • Shared form components (e.g., TextField, Select, DatePicker) live in the design system for consistent styling and a11y.
  • Client/server validation parity – always validate on both sides; reuse Zod schemas server-side.
  • Error messages – localised, concise, and tied to form fields.
  • Submission feedback – disable submit during processing, show inline success/error states.

Performance:

  • Lazy-load heavy form controls (e.g., WYSIWYG rich text editors) to reduce initial bundle size.
  • Use controlled/uncontrolled hybrids where possible in React Hook Form for optimal performance.

Accessibility Baseline

Accessibility by default. Every interactive component should be operable with a keyboard and understandable with a screen reader.

Minimum requirements:

  • Semantic HTML: For structure and meaning. Automated testable with axe/jest-axe (role/element checks).
  • Keyboard navigable & focus order: All actions reachable with Tab/Shift+Tab; Enter/Space activate buttons/links. Requires manual testing.
  • Sufficient color contrast for text and UI elements (WCAG AA minimum). Automated testing via axe/jest-axe or pa11y.
  • Visible focus indicators (≥3:1 contrast with surroundings). Automated detection possible for presence, but quality (contrast, shape) requires manual review.

Internationalisation & Localisation

Coming soon — Internationalisation & Localisation

This section is being polished.

Analytics, Monitoring & Observability

Coming soon — Analytics, Monitoring & Observability

This section is being polished.

Security & Privacy

Coming soon — Security & Privacy

This section is being polished.

Feature Flags & Configuration

Coming soon — Feature Flags & Configuration

This section is being polished.

State Management Escalation Guidelines

Coming soon — State Management Escalation Guidelines

This section is being polished.

Error Handling & Empty States

Coming soon — Error Handling & Empty States

This section is being polished.

API Contracts & Types

Coming soon — API Contracts & Types

This section is being polished.

Performance & Asset Strategy

Coming soon — Performance & Asset Strategy

This section is being polished.

Browser Support Matrix

Coming soon — Browser Support Matrix

This section is being polished.

PWA & Offline

Coming soon — PWA & Offline

This section is being polished.

Testing Strategy

Coming soon — Testing Strategy

This section is being polished.

CI/CD & Release

Coming soon — CI/CD & Release

This section is being polished.

Documentation & Code Quality

Coming soon — Documentation & Code Quality

This section is being polished.

Migration & Deprecation Policy

Coming soon — Migration & Deprecation Policy

This section is being polished.


Final Thoughts

This isn't just about clean code. It's about fast teams, less rework, and products that scale with confidence.

More Blog Posts

Zebra Labs Backend Default Design — Outline (v1)

A practical, opinionated baseline for building FastAPI backends at Zebra Labs. Domain-driven, hexagonal, code-first data, with clear observability and a smooth Fly.io → AWS path.

Read more

MVPs That Matter - Reframing the Launch Timeline

A strategic deep dive into how Zebra Labs reframes MVP planning to focus on user feedback, revenue generation, and sustainable product development. This is essential reading for non-technical founders navigating their first product build

Read more

Tell us about your project

We're all ears.