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 palette is aligned with the web app's CSS tokens
(js/web/src/styles.css), which is the source of truth. The app is
dark-first; light values mirror the web's :root block.
Surfaces (theme.colors.surface0–3, surfaceHover) #
Purple-tinted dark surfaces, never pure black. Surfaces separate with hairline borders, not shadows.
| Token | Dark | Light | Use |
|---|---|---|---|
surface0 |
#150e1c |
#fdf6fa |
App background |
surface1 |
#281b28 |
#ffffff |
Cards, panels, inputs |
surface2 |
#201324 |
#ffffff |
Popovers, menus, sheets |
surface3 |
#292129 |
#f5e8f0 |
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) #
Steps 1–2 mirror the web's --foreground / --muted-foreground; 3–4 derive
from the foreground at fixed alphas.
| Token | Dark | Light | Use |
|---|---|---|---|
text1 |
#f7eaf3 |
#3d1c44 |
Primary: titles, body |
text2 |
#b79aae |
#9b7e92 |
Secondary: metadata, descriptions |
text3 |
rgba(247,234,243,.45) |
rgba(61,28,68,.46) |
Tertiary: placeholders, timestamps |
text4 |
rgba(247,234,243,.30) |
rgba(61,28,68,.32) |
Disabled |
Legacy aliases: text→text1, textMuted→text2, textDisabled→text4.
Borders (borderSubtle / border / borderStrong) #
1px hairlines: rgba(247,234,243,0.06 / 0.10 / 0.15) 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 sans typeface: Atkinson Hyperlegible Next (+ Ioskeley 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 Ioskeley Mono. typography.mono.*(Ioskeley 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.