Skip to content

UX Principles

Sapari's interface is built on a set of deliberate design decisions. This page explains the philosophy behind them.


Dual-Experience Design

Mobile and desktop are completely separate interfaces, not responsive adaptations of the same layout. They share backend state through React Query but render entirely different component trees.

Dashboard.tsx (line ~1305)
├── isMobile = true  → <MobileDashboard />   (20 components, touch-first)
└── isMobile = false → <Desktop layout />     (sidebar + timeline + video)

Why not responsive? Video editing has fundamentally different interaction models on touch vs. mouse. A responsive layout would compromise both experiences. Instead:

  • Desktop optimizes for precision: multi-track waveform editing, keyboard shortcuts, drag handles, pixel-level control
  • Mobile optimizes for speed: swipe gestures, wizard flows, tap-to-decide, one-handed use

The breakpoint is 768px (UI.MOBILE_BREAKPOINT). Mobile renders when the device supports touch AND the shorter viewport dimension is below 768px. Desktop browsers resized narrow stay in desktop mode.


Brutalist Design System

Sapari uses a brutalist aesthetic — intentionally raw, direct, and functional. No soft gradients, no rounded corners, no decorative elements.

Visual Tokens

Token Value Usage
Border weight border-4 border-black Primary containers
Border radius rounded-none Everything — sharp edges only
Shadow shadow-hard Hard-offset box shadow (no blur)
Typography Monospace, ALL-CAPS Labels, headers, UI text
Primary color #FF3300 (Sapari Orange) Accents, active states, CTAs
Dark background #0d0d0d Dark mode base

Button System

Four variants, all with hard shadows and press animations:

Variant Style Use
primary White bg, black text Default actions
black Black bg, orange text Emphasis
secondary Gray bg, black text Secondary actions
orange Orange bg, white text Primary CTAs

Interaction micro-animations:

  • Hover: hover:-translate-y-1 hover:-translate-x-1 (lift effect)
  • Press: active:translate-x-[2px] active:translate-y-[2px] (push into surface)
  • Transitions: transition-all duration-200

Dark Mode

Dark mode removes shadows (dark:shadow-none) and replaces black borders with semi-transparent orange (dark:border-orange-500/50). The brutalist structure remains, but the contrast shifts from black-on-white to orange-on-dark.


Shared State, Separate Rendering

Both mobile and desktop read from the same React Query cache. This means:

  • Edits made on desktop appear instantly on mobile (and vice versa)
  • No separate "mobile API" — same hooks, same query keys
  • Optimistic updates work identically across both surfaces

Shared hooks (used by both mobile and desktop):

  • useEdits() — edit list with mutations
  • useClips() — clip data
  • useCaptionLines() — caption state
  • useProject() — project metadata
  • useAssets() — asset library

Separate hooks exist where interaction models diverge (e.g., useTimelineViewport for desktop scroll/zoom vs. mobile's pinch-zoom in FocusMode).


Design Decisions

Why monospace?

Monospace text makes timestamps, durations, and technical values scan faster. Since Sapari's UI is dense with time codes and numeric values, monospace creates visual consistency and predictable column alignment.

Why no border radius?

Sharp edges reinforce the tool-like, professional feel. Rounded corners suggest friendliness; hard edges suggest precision. Sapari is a power tool, not a consumer toy.

Why hard shadows?

Hard shadows (no blur, fixed offset) create a clear visual hierarchy without ambiguity. Elements either cast a shadow or they don't — there's no gradient of "how elevated" something is.

Why separate mobile/desktop?

A timeline editor cannot be made responsive. On desktop, you need:

  • Multi-track waveform visualization
  • Pixel-precise drag handles
  • Horizontal scroll with zoom
  • Right-click context menus
  • Keyboard shortcuts (Space, E, Cmd+Z)

On mobile, you need:

  • Swipe-to-decide card stacks
  • Pinch-to-zoom with inertia
  • Bottom sheets instead of sidebars
  • Large touch targets (44px minimum, per WCAG 2.5.5)
  • One-handed reachability

These are incompatible interaction models. Trying to merge them would make both worse.


Constants Reference

Key layout values from shared/config/constants.ts:

Constant Value Context
MOBILE_BREAKPOINT 768px Mobile/desktop split
TABLET_BREAKPOINT 1024px Tablet detection
MIN_TOUCH_TARGET 44px WCAG minimum tap size
UI_DEBOUNCE_MS 100ms Resize event throttling
SEARCH_DEBOUNCE_MS 300ms Search input delay
AUTO_SAVE_DELAY_MS 1000ms Edit auto-save
SETTINGS_SAVE_DEBOUNCE_MS 500ms Settings persistence
SUCCESS_NOTIFICATION_MS 2000ms Success toast duration
ERROR_NOTIFICATION_MS 3000ms Error toast duration
CLICK_THRESHOLD_MS 200ms Quick-click vs long-press

Next: Desktop Interface →