Streamplace Design System #
Dark-first. Linear-grade restraint, YouTube-familiar layout grammar. Every
visual decision is a token in js/components/src/lib/theme/tokens.ts,
consumed through useTheme() (js/components/src/lib/theme/theme.tsx).
The one rule: component code never contains a raw hex value, rgba(),
numeric font size, spacing, radius, or duration. The ratchet
(js/scripts/check-tokens.mjs) enforces this. Intentional exceptions
(e.g. transparent OBS overlay roots) carry a // token-ok comment.
Color #
The neutral ramp (surfaces, text, borders) is a clean, untinted
near-black/white system. The accent colors (primary, secondary) are
aligned with the web app's CSS tokens (js/web/src/styles.css).
Surfaces (theme.colors.surface0–3, surfaceHover) #
Clean near-black, never pure black and never tinted. Surfaces separate with hairline borders, not shadows.
| Token | Dark | Light | Use |
|---|---|---|---|
surface0 |
#0a0a0b |
#ffffff |
App background |
surface1 |
#111113 |
#fafafa |
Cards, panels, inputs |
surface2 |
#18181b |
#f4f4f5 |
Popovers, menus, sheets |
surface3 |
#1f1f23 |
#ececef |
Hovered overlay rows, tooltips |
surfaceHover |
= surface3 | = surface3 | Hover fill on interactive rows |
Legacy aliases (kept working): background→surface0, card→surface1,
popover→surface2.
Text (theme.colors.text1–4) #
White (dark) / ink (light) at fixed alphas.
| Token | Dark | Light | Use |
|---|---|---|---|
text1 |
rgba(255,255,255,.92) |
rgba(9,9,11,.92) |
Primary: titles, body |
text2 |
rgba(255,255,255,.65) |
rgba(9,9,11,.66) |
Secondary: metadata, descriptions |
text3 |
rgba(255,255,255,.45) |
rgba(9,9,11,.46) |
Tertiary: placeholders, timestamps |
text4 |
rgba(255,255,255,.30) |
rgba(9,9,11,.32) |
Disabled |
Legacy aliases: text→text1, textMuted→text2, textDisabled→text4.
Borders (borderSubtle / border / borderStrong) #
1px hairlines: rgba(255,255,255,0.06 / 0.08 / 0.10) in dark. Subtle for
surface separation, default for controls at rest, strong for hover.
Accent (primary, ring, focus) & secondary #
One accent: pink/magenta #e955c2 (colors.primary ramp, the web's
--primary). Used sparingly — primary buttons, focus rings, active
states, links, the Go Live moment. Never for large fills or decoration.
Broadcaster branding may override primary/ring (and focus follows
ring automatically).
Secondary/accent: teal #1abbc0 (colors.secondary ramp, the web's
--secondary/--accent).
Status #
| Token | Dark | Rule |
|---|---|---|
live / liveDim / liveForeground |
#f23041 |
Reserved for the LIVE state only — badges, live avatar rings, on-air dots. Never used for errors. |
success |
#3dd68c |
Healthy ingest, confirmations |
warning |
#ffb224 |
Degraded states |
danger / destructive |
#ff3b5c |
Errors, destructive actions |
overlay |
rgba(0,0,0,0.6) |
Modal scrims |
Typography #
One typeface: Geist (+ Geist Mono), weights 400/500/600 only,
static (no variable fonts). Canonical scale
typeScale — sizes 12/13/14/16/20/24/32, line heights in the token file,
tight letter-spacing from 20px up:
| Key | Size/Line | Weight | Use |
|---|---|---|---|
xs |
12/16 | 400 | Badges, timestamps, overlines |
sm |
13/18 | 400 | Chat messages, dense metadata |
base |
14/20 | 400 | Default UI text |
md |
16/24 | 400 | Stream titles (rows), emphasized body |
lg |
20/26 −0.2 | 500 | Section headings |
xl |
24/30 −0.3 | 600 | Page titles |
xxl |
32/38 −0.5 | 600 | Hero moments only |
- Counts, timers, durations always use
tabularNums(fontVariant: ["tabular-nums"]) so digits don't jitter; long-form timers use Geist Mono. typography.mono.*(Geist Mono) for stream keys, ingest URLs, diagnostics.typography.ios/typography.android/ fontFamily keys outside regular/medium/semiBold are deprecated remaps — do not use in new code.
Spacing & layout #
4px grid. Canonical steps and their token keys:
| Key | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16 |
|---|---|---|---|---|---|---|---|---|
| px | 4 | 8 | 12 | 16 | 24 | 32 | 48 | 64 |
All padding/margin/gap from this set (via spacing[n], atoms p/m/gap).
Off-grid keys (5, 7, 9, 10, 11, 14, 20…) are deprecated.
Radii (borderRadius): sm 4 (small controls), md 8 (cards, inputs,
buttons), lg 12 (thumbnails, modals), full (avatars, pills). xl/2xl/3xl
are deprecated aliases of lg.
Hit targets: minimum 44px on touch platforms (touchTargets.minimum).
Motion (motion) #
| Token | Value | Use |
|---|---|---|
motion.fast |
120ms | Micro: hover, press feedback |
motion.base |
200ms | Standard: reveals, toggles, fades |
motion.slow |
300ms | Structural: sheets, panels |
motion.bezier |
(0.25, 0.1, 0.25, 1) | Easing.bezier(...motion.bezier) in reanimated |
motion.easingCss |
same, as CSS string | Web transitions |
motion.sheetSpring |
damping 30 / stiffness 300 | The only allowed spring, sheets only |
Everything that appears fades + translates 4–8px. Nothing pops. No bounce.
Elevation #
Prefer a raised surface + hairline border over shadow. Shadows (shadows.sm
only, in practice) are reserved for floating layers: menus, toasts, popovers.
Focus (web) #
Every interactive element: 2px focus-colored ring with 2px offset
(outline on web, border fallback native). Keyboard navigation must be
flawless; focus states are never removed, only styled.
Migration status #
Deprecated-but-working during the redesign (removal tracked in
MIGRATION.md at the end of Phase 3): platform typography scales, fontFamily
weight aliases, off-grid spacing keys, radius xl/2xl/3xl, animations
(use motion), the 17 unused Tailwind ramps.