put.io design system
Tokens, primitives and component specs for put.io. Generic
CSS / JSON here; platform apps adapt natively.
v1.0 · 2026-05
tokens.css ↗
Pipeline
Sourcetokens/*.tokens.json · canonical DTCG
Generatorscripts/build-tokens.ts
Outputsystem/tokens.css · generated
Basetokens/foundation.css · static layer
TargetsCSS now · Figma / iOS / Android via the same graph
Consume
CSS
DTCG JSON
Build
/* generated output — import in any page */ @import "system/tokens.css"; /* use vars as roles, never literals */ .btn-primary { background: var(--yellow-solid); color: var(--primary-foreground); border-radius: var(--radius); }
// tokens/*.tokens.json · W3C DTCG — paths map 1:1 to CSS vars { "yellow": { "solid": { "$value": "hsl(44.7, 97.9%, 63.1%)", "$type": "color", "$description": "Brand · #FDCE45 · → --yellow-solid" } }, "radius": { "radius": { "$value": "6px", "$type": "dimension", "$description": "Default radius · → --radius" } } }
// regenerate after editing tokens/*.tokens.json $ node scripts/build-tokens.ts // any tool that speaks DTCG can read the same graph read("tokens/*.tokens.json") // Style Dictionary, Tokens Studio, … // platform adapters live in app repos, not here
House rules
- Utility is beautiful. Beauty comes from making file operations feel premium — not from decoration.
- Content-agnostic by default. No posters, album art, curated metadata, or “watched” state. Filename, size, date.
- Raw filenames stay raw. Never parse, rewrite or fake metadata from a filename. Render verbatim, mono.
- Clarity over cleverness. One verb per icon, one icon per verb. Download / Save / Stream never overlap.
- Type carries the weight. Three families — sans for UI, two monos for numerics and identifiers.
- One yellow at a time.
#FDCE45is the single CTA and the focus ring. Not decoration. - TV is list-first. Without thumbnails, rows beat card walls. Don’t copy Apple TV or Plex.
- Voice and restraint. Plain, short, English-first. Subtract before you add.
Part A
Tokens & primitives
Foundations (color, type, space, radii, motion) followed by brand primitives (logo, icons, focus ring). Canonical source:
tokens/*.tokens.json — tokens.css and every other artifact is generated from it.A.01
Color
12-step scales, light + dark. One brand, three semantic, one transfer-state lime.
- Purpose
- Role-based color tokens. Vars name the role (
--bg,--text-secondary), not the hex. Components consume roles, never literals. - Source of truth
tokens/*.tokens.json(canonical) →tokens.css— light:root+.darkoverlay- Usage
/* light + dark in one stylesheet — toggle .dark on <html> */ .btn { background: var(--yellow-solid); color: var(--primary-foreground); } .row { background: var(--bg-secondary); color: var(--text); } .row:hover { background: var(--list-item-bg-hover); }
- Constraints
--yellow-solidis fill-only. For yellow text, use--yellow-text-secondary(AA on light bg).- No new color literals in component code
- No semantic scale for decoration — status only
Brandstep 9 shifts between modes; everything else flips like a normal 12-step scale
BRAND
--yellow-solid
#FDCE45
Brand fill: CTAs, focus halos (35% alpha), folder icons, progress bars. Never as text on light.
+ hover
--yellow-solid-hover
#F3C435
CTA hover — slightly darker brand yellow. Yellow can't darken much without going olive; this is the most we can shift.
contrast
--primary-foreground
#292109
Label text on yellow-solid — warm dark, not pure black.
yellow-contrast.surface
--yellow-bg
step 1 · #FEFDFA / #000
Faintest tint backdrop in light; drops to pure black at the deepest step in dark.
Neutrals · dark (app default)12 steps · gray scale
--bg
hsl(0 0% 8.5%)
Page background — the dominant app surface. Sidebars, body, full-screen modals.
--bg-secondary
hsl(0 0% 11%)
Inset panel · quiet aside. Code blocks, reference panels, secondary nav.
--component-bg
hsl(0 0% 13.6%)
Card surface · default button fill · chips. Focal components.
--component-bg-hover
hsl(0 0% 15.8%)
Hover state for any
--component-bg surface.--component-bg-active
hsl(0 0% 17.9%)
Pressed / selected state · toggle on.
--line
hsl(0 0% 20.5%)
Hairline divider between rows, cells, sections.
--border
hsl(0 0% 24.3%)
Component border — inputs, buttons, cards. Resting state.
--border-hover
hsl(0 0% 31.2%)
Border on hover / focus-within.
--solid
hsl(0 0% 43.9%)
Solid neutral fill —
btn-info, mid-tone icons.--solid-hover
hsl(0 0% 49.4%)
Hover companion to
--solid.--text-secondary
hsl(0 0% 62.8%)
Meta copy — captions, hints, timestamps, secondary nav.
--text
hsl(0 0% 93%)
Primary text — body, headings, button labels.
Semantic · positive · destructive · in-motionstatus only, never decoration
--green-solid
positive
btn-success · check icons · progress complete.--green-bg-secondary
positive surface
Success callout backdrop · confirmation tint.
--red-solid
destructive
btn-danger · error icons · invalid input border.--red-bg-secondary
destructive surface
Error callout backdrop · invalid input fill.
--lime-3
in-motion accent
Transfer downloading / completing rows. put.io-specific.
specimens
Color · canonical previewspreview/color-*
A.02
Type
Three local families. GT America for UI / body / display. GT America Mono for numerics. Berkeley Mono for code & filenames.
- Purpose
- One sans + two monos do all the work. Mono families enforce raw filenames stay raw — filenames render in Berkeley Mono, numerics in GT Mono.
- Source of truth
tokens/*.tokens.json(type group) →tokens.css· brand fonts from put.io's font CDN, system fallbacks- Generated
- CSS custom properties (
--font-*,--fs-*) - Usage
/* font roles */ .row { font-family: var(--font-sans); } /* GT America */ .row .size { font-family: var(--font-ui-mono); font-variant-numeric: tabular-nums; } .row .name { font-family: var(--font-mono); } /* Berkeley · filenames */
- Constraints
- Weights used: 400 · 500 · 700 · 900 (display only)
- Mobile body never below 14px · TV body never below 24px
- No display serif, no script, no system stack fallbacks for body
--fs-display
put.io
--fs-3xl
Display heading
--fs-2xl
Display heading · small
--fs-xl
Page title
--fs-lg
Heading
--fs-md
Subheading · card title
--fs-base
Body — app default (= root, matches production fontSize 1)
--fs-sm
Caption · button label (production fontSize 0)
--fs-xs
Fine print / overline
--font-ui-mono
11.20 TB · 12.4 MB/s · 00:03:42
--font-mono
ubuntu-22.04.iso · sha1: e3b0c44…
specimens
Type · canonical previewspreview/type-*
A.03
Spacing
4 px base, aggressive doubling. Reserved for layout — components handle inner padding via their own tokens.
- Purpose
- Layout rhythm.
--space-3(16) is the row gutter;--space-4(32) is the section gutter. - Generated
spacing.json· platform px / dp scales- Usage
.list { display: flex; flex-direction: column; gap: var(--space-1); } /* 4px between rows */ .section { padding: var(--space-4) var(--space-3); } /* 32 × 16 */
- Constraints
- Use scale tokens — no arbitrary px values in components
- No half-steps (no 6px / 12px / 24px)
A.04
Radii & shadow
Five radii, four elevations. 6 px is the default for buttons, inputs, cards. Yellow halo is one of the shadows.
- Purpose
- Radii match component scale. Shadows define elevation roles — hover/focus, popover, modal — and a yellow focus halo that is the focus system.
- Source of truth
tokens/*.tokens.json→tokens.css·--radius-*·--shadow-*- Generated
- CSS custom properties
- Usage
.btn { border-radius: var(--radius); } /* 6px default */ .pill { border-radius: var(--radius-pill); } .btn:focus-visible { box-shadow: var(--shadow-focus); } /* yellow halo */
- Constraints
--radius= 6 is the canonical default- Only
--shadow-focusever uses yellow - No drop shadow on hover — use bg shift instead
--radius-sm
4px
--radius
6px · default
--radius-md
8px
--radius-lg
10px
--radius-pill
999px
--shadow-sm
hover / focus
0 1px 2px / 0.4
0 1px 2px / 0.4
--shadow-md
popover, menu
0 4px 12px / 0.5
0 4px 12px / 0.5
--shadow-lg
modal, sheet
0 16px 40px / 0.6
0 16px 40px / 0.6
--shadow-focus
soft halo
0 0 0 3px / 0.35
0 0 0 3px / 0.35
A.05
Motion
Two easings. Three durations. If a transition doesn't fit one of these, it's wrong.
- Purpose
- Animation feels consistent across platforms.
--dur-fastfor hover,--dur-basefor menus,--dur-slowfor sheets. - Source of truth
tokens/*.tokens.json→tokens.css·--ease-*·--dur-*- Generated
- CSS custom properties · native easing curves
- Usage
.btn { transition: background var(--dur-fast) var(--ease-out); } .sheet { transition: transform var(--dur-slow) var(--ease-in-out); }
- Constraints
- Respect
prefers-reduced-motionat the component level - No bounce / overshoot easings
- Respect
A · 2
Brand & primitivespreview/brand-* · focus-system · icon-set
A.06
Brand & logo
Logomark, app-icon family, empty-state catalogue. Kaomoji
ᕦ(ò_óˇ)ᕤ is part of the brand.- Purpose
- Brand identity assets. Logomark = wordmark + dot; app icon = mark on yellow square; empty-state = kaomoji + one-line copy + one yellow CTA.
- Source of truth
system/assets/logo.svg·logo-retro-{dark,light}.svg·app-icon-*.png- Generated
- iOS / Android / web favicons · app icons per platform
- Usage
<!-- always lowercase --> <span class="brand">put<span class="dot">.</span>io</span>
- Constraints
- Wordmark is always lowercase put.io — never upper-cased, title-cased, or dot-less
- No mark in body text · no shrunk-then-cropped variants
A.07
Icons
Phosphor going forward.
ph-fill ph-folder tinted #FDCE45 is the signature.- Purpose
- One icon family across all platforms. Regular weight default · fill weight for active states. Inline SVG only — no icon fonts in new code.
- Source of truth
- Phosphor icons ·
phosphor-icons.com - Generated
- Inline-SVG icon set · per-icon metadata JSON
- Usage
<!-- inline SVG, 24×24, currentColor --> <svg width="24" height="24" viewBox="0 0 256 256"> <path fill="currentColor" d="…"/> </svg>
- Constraints
- 24×24 default, 20×20 in dense tables, 32×32 on TV
- Folder icon =
ph-fill ph-foldertinted--yellow-solid - No emoji as icons · no decorative SVG
- No legacy Flaticons font in new code
A.08
Focus
Three input contexts, one ring. Keyboard / pointer / D-pad resolve to the same brand-yellow halo, scaled to the device.
- Purpose
- Unified focus visual across runtimes. Web/mobile = soft 3px brand-yellow halo (35% alpha). TV = 4px ring +
scale(1.06)+ drop shadow. - Source of truth
tokens/*.tokens.json→tokens.css·--shadow-focus- Generated
- TV variant via
tv-shell.css - Usage
/* web/mobile */ .btn:focus-visible { box-shadow: var(--shadow-focus); } /* TV — overrides for 10ft viewing */ .tv .card:focus { transform: scale(1.06); box-shadow: 0 0 0 4px var(--yellow-solid), var(--shadow-lg); }
- Constraints
- Always yellow — focus is the one place yellow is mandatory
- Use
:focus-visibleon web/mobile, never plain:focus - No outline removal without a visible replacement
Part B
Components
Reusable, composable, framework-agnostic specs. Each card answers purpose · source · artifact · usage · constraints, then shows the canonical specimen.
source · preview/components-*
preview/ ↗
B.02
Inputs
Text, search (with / hint), password, error. Yellow border + halo on
:focus-visible.- Purpose
- Text entry primitive. Search box, password field, validation states use the same shell.
- Source of truth
Input- Generated
- CSS class set · platform-native field shells
- Usage
<input class="input" type="text" placeholder="Search files"> <input class="input is-error" aria-invalid="true">
- Constraints
- Label outside the field · placeholder is hint, not label
- Error message uses
--red-solidbelow the field - No placeholders as labels
B.03
Form fields · extended
Date · stepper · slider · tag chips · autocomplete · file row · password strength. Plus the full validation vocabulary — error / warning / success / async / counted.
- Purpose
- Field types beyond plain text. Each inherits Input's shell + adds a behavior. Validation vocabulary is shared.
- Source of truth
Field/*- Generated
- CSS variants · native pickers per platform
- Usage
<FieldDate name="due" value={d} onChange={set} /> <FieldSlider min={0} max={100} step={5} />
- Constraints
- Validation state is a prop, not a class — one source per field
- No custom-built date picker — defer to platform native on mobile
B.04
Form layouts
Seven canonical compositions. Settings row (label-left) · modal stacked · two-column · inline composer · wizard · sticky save bar · click-to-edit.
- Purpose
- Composition patterns. Pick the closest match — don't invent a new one.
- Source of truth
FormLayout- Generated
- Grid templates · responsive breakpoints
- Usage
<FormLayout variant="settings-row"> ... </FormLayout> <FormLayout variant="two-column"> ... </FormLayout>
- Constraints
- Sticky save bar appears only on dirty state
- No mid-form modals — break into a wizard instead
B.05
Badges & chips
Lifecycle pills, filter chips, user tags. No codec / quality / "watched" badges — see content-agnostic.
- Purpose
- Status communication. Lifecycle (queued/in-progress/done/failed), filter chips, user tags.
- Source of truth
Badge·Chip- Generated
- CSS classes · semantic color mapping
- Usage
<Badge tone="positive">Completed</Badge> <Badge tone="in-motion">Downloading</Badge> <Chip selected>Video</Chip>
- Constraints
- Tone maps to semantic scale (positive/destructive/in-motion)
- No codec / quality / poster badges on files
- No "watched" state — we don't track that
B.06
List items
The most-touched primitive. Sidebar nav · setting row · friend row · activity row — same skeleton, four shapes.
- Purpose
- Single horizontal row primitive. Composes leading icon · primary text · secondary text · trailing meta · trailing action.
- Source of truth
ListItem- Generated
- Slot-based CSS · TS prop types
- Usage
<ListItem leading={icon} title="Files" trailing={count} /> <ListItem title="Theme" subtitle="Auto" layout="setting" />
- Constraints
- Min row height 44px (touch) · 48px (TV focus margin)
- Trailing meta uses
--font-ui-mono - No more than one trailing element
B.07
File row
The content-agnostic file primitive. Yellow folder icon, mono filename, mono size, mono date. Same skeleton in web tables, mobile lists, TV rows.
- Purpose
- Render any file/folder identically: icon · filename · size · date. No metadata enrichment, ever.
- Source of truth
FileRow- Generated
- Web table / mobile cell / TV row variants
- Usage
<FileRow kind="folder" // folder | video | audio | doc | archive | other name="The.Wire.S03E04.1080p.x264-GROUP.mkv" // raw, verbatim size="1.42 GB" date="2026-05-25" />
- Constraints
- Filename always Berkeley Mono, ellipsis-on-overflow, hover reveals full
- Folder =
ph-fill ph-folderin--yellow-solid - No parsing, no marketing-group stripping
- No thumbnail · no codec badge · no resolution chip
B.08
Storage bar
Healthy (with file-type breakdown) · warn 80% · over 95%. Compact sidebar variant + plan-upsell card.
- Purpose
- Quota readout. Three thresholds (healthy / warn / over) drive color. Breakdown is by file-type, not metadata.
- Source of truth
StorageBar- Generated
- Web sidebar + standalone card
- Usage
<StorageBar used={11_200_000_000_000} total={50_000_000_000_000} breakdown={["video", "audio", "doc"]} />
- Constraints
- Threshold colors: green ≤80, yellow 80-95, red >95
- No "junk file" or "biggest movie" labels — we don't know that
B.09
Transfer states
Queued → downloading → completed / seeding / failed. Health indicator replaces torrent jargon (seed ratio, peer count).
- Purpose
- The "is it working?" answer. One health dot (green/yellow/red) plus a one-line status. Power-user detail (ratio, peers) is in expand.
- Source of truth
TransferRow- Generated
- Web + mobile + TV transfer-row variants
- Usage
<TransferRow state="downloading" // queued | downloading | seeding | completed | failed health="good" // good | warn | bad progress={0.62} name="ubuntu-22.04.iso" />
- Constraints
- Default view is plain-English: "Downloading · 62%"
- Lime accent (
--lime-3) for in-motion surface only - No "Seed ratio 2.00/10 days" in default view
B.10
Empty & error states
Kaomoji + one-line copy + one yellow CTA. Loading skeleton above 200ms wait. Red error sheet for real failures.
- Purpose
- Communicate "nothing here yet" or "something broke" with personality and a single clear next action.
- Source of truth
EmptyState·ErrorSheet- Generated
- Per-platform empty illustrations as text/kaomoji
- Usage
<EmptyState kao="ᕦ(ò_óˇ)ᕤ" title="No transfers yet" cta={{ label: "Add a transfer", onClick: ... }} />
- Constraints
- One CTA · one kaomoji · one line of copy
- Loading skeleton kicks in at 200ms, not earlier
- No illustration art · no decorative SVG
B.11
Overlays
Dialog · sheet · bottom-sheet · confirm. One scrim, one elevation. Bottom-sheet is mobile-first; the others render anywhere.
- Purpose
- Modal surfaces. Dialog (centered) · sheet (right edge) · bottom-sheet (mobile) · confirm (small destructive).
- Source of truth
Overlay/*- Generated
- CSS + ARIA primitives · native sheets on iOS/Android
- Usage
<Dialog open={o} onClose={c}> ... </Dialog> <BottomSheet open={o} onClose={c} actions={[ ... ]} />
- Constraints
- Scrim opacity from
--overlay-fullonly - Bottom-sheet primary action at top; destructive in red
- No nested overlays
- Scrim opacity from
B.12
Other primitives
Avatars · breadcrumbs · dropzone · notification · tabs · tooltip. Same token rules, same focus model.
- Purpose
- Smaller, less-touched primitives. Lifted verbatim from the same token roles as everything else.
- Source of truth
Avatar / Breadcrumbs / Dropzone / Notification / Tabs / Tooltip- Generated
- Per-platform variants
- Usage
<Avatar name="kai" /> <Breadcrumbs path={["Files", "Movies"]} /> <Tabs items={t} /> <Tooltip content="…">…</Tooltip>
- Constraints
- Tooltips: keyboard-accessible, dismissible on Esc
- Notifications: max 3 stacked · auto-dismiss 4s for info, manual for destructive
Part C
Platforms
Where tokens and components compose into the surfaces users actually touch. This repo publishes generic CSS / JSON; platform repos adapt locally. Visual stays put.io.
C.00
Platform contract
What lives where. This repo = framework-agnostic source of truth. Each platform repo consumes the generated artifact and implements natively.
- Purpose
- Clarify the seam between generic spec (here) and platform implementation (in each app repo). Spec changes propagate via artifact regeneration, not copy-paste.
- Source of truth
system/tokens.css+preview/*.htmlspecimens- Generated
- CSS · DTCG JSON · flat JSON · TypeScript metadata · Figma export
- Usage · per-platform consumption
// any web / JS runtime — install the CSS package import "the generated CSS bundle"; // any tool that speaks DTCG JSON (Style Dictionary, Tokens Studio …) read("tokens/*.tokens.json") // → consume in your own build // native app repos build their own adapter from the same JSON
- Constraints
- Platform repos may add — never override — token values
- Visual matches across platforms; engine (focus, blur, player) is local
- No platform-specific color in this repo
- No "TV-only" tokens here — TV adaptation lives in the iOS app / android
// distribution model this design system // tokens, component specs, flow specs // ships: CSS · DTCG JSON · TS · Figma export // consumes the generated CSS bundle the web apps // desktop + smart-TV · same CSS the native apps // iOS / Android / Roku — build their OWN // adapters from the DTCG JSON; this system // doesn't ship Swift / Kotlin / BrightScript
B · 1
Web · desktop browserthe web app
C.01
App shell
Collapsible sidebar · breadcrumb top bar with /-search · selection toolbar · column-headed file table.
- Purpose
- Top-level web layout. Sidebar nav + main content + storage footer. Keyboard-first.
- Source of truth
- the web app · main layout
- Generated
- React component · CSS grid template
- Usage
<AppShell sidebar={<Sidebar />} topbar={<TopBar />}> <FilesView /> </AppShell>
- Constraints
- Sidebar collapsible; persisted preference
- One yellow CTA on the topbar (New transfer)
- No second yellow button anywhere in the shell
C.02
Web references
Convergent ground-truth screens to lift from. Live in
the web app; cross-project canvases mirror them here.v12 – v15 · linear-neutral
Pending source
Files list · Light + Dark
the web app · files feature
Pending source
File detail / player
Right-rail actions, breadcrumbs, Chromecast button. the web app · file detail
Pending source
Billing & plans
Persona-shaped pricing for trial / casual / plus / power tiers. Composes from Buttons + List items.
B · 2
Mobile · iOS & AndroidiOS · Android
C.03
iOS & Android shells
iOS Files screen · Android player chrome · shared pairing/activation flow used across iOS, Android, tvOS, Roku.
- Purpose
- Touch-first shells. iOS uses native nav stack; Android uses Material You scaffold tinted with brand yellow.
- Source of truth
- Platform repos (out-of-tree). Shells composed from
the component libraryprimitives in each platform's idiom. - Generated
- Spec only — platform repos implement natively
- Usage · iOS
NavigationStack { FilesView() .toolbar { ToolbarItem(placement: .primaryAction) { AddTransferButton() } } }
- Constraints
- Long-press → bottom-sheet for file actions
- Destructive items use
--red-solidtext - No swipe-to-delete without confirm
C.04
Bottom-sheet actions
Mobile uses the bottom-sheet from Overlays for file actions. Long-press → bottom-sheet → tap; destructive in red.
C.05
Mobile references
Pending canonical sources — the new
the iOS app & the Android app apps are being built spec-first.
Pending source
iOS · Player + PiP
AVPlayer-backed. Lock Screen / Dynamic Island controls.
Pending source
Android · Files list
Material You scaffold, brand-yellow accent override.
Pending source
Settings · iOS / Android
Compose from List items — Account · Plan · Privacy · Notifications · Apps & integrations · Storage groups.
C · 3
TV · Apple TV · Android TV · Roku · Smart-TVplatform repos adapt locally
C.06
Foundations
Row anatomy · type scale at 10ft · surface materials · content rules. List-first — see house rules.
tv-shell.css
- Purpose
- 10-foot UI primitives. Filename-driven; no thumbnails, no posters. Rows of 56-72px focus targets.
- Source of truth
system/preview/tv-shell.css+ the TV specimen pages below- Generated
- CSS scope (
.tv) · DTCG TV-context tokens - Usage
/* TV body never below 24px */ .tv .row { font-size: 28px; line-height: 1.25; padding: 18px 32px; } .tv .row .name { font-family: var(--font-mono); } /* raw filename */
- Constraints
- List rows, not card grids · D-pad navigation only
- Min focus target 88px
- No card walls · no hero rails · no "Continue Watching" if we can't track it
C.07
Focus
scale(1.06) + 4px yellow ring + drop shadow. Same visual on every TV platform, different engines drawing it.C.10
Player chrome
Scrubber, audio/subtitle pickers, info overlay. Player engine is platform-specific (AVPlayer / libVLC / Roku / HTML5).
C.11
Platform matrix
Seven platforms × five differences (focus engine, surface material, player ownership, perf floor, token mapping).
Pending preview
Strategy & token mapping table
Web · iOS · Android · Apple TV · Android TV · Roku · Smart-TV web. Each consuming platform repo owns its own engine, surface, player-ownership and token-mapping breakdown.
C.12
Specimen file map
Every TV preview embedded above is a flat HTML file in
system/preview/. Native app repos consume the DTCG JSON for tokens and lift these specimens verbatim into their own idiom.system/preview/tv-*
The specimen is the contract. When implementing a TV component natively, open the matching
preview/tv-*.html + preview/tv-shell.css, read the exact values (font-size, weight, tracking, line-height, padding, radius, color tokens, shadow), then re-express them in your platform's native language. No native bundles ship from this repo.
TV
Foundations
spec
TV
Focus
spec
TV
Top navigation
spec
TV
Action menus
spec
TV
Player chrome
spec
TV
TV stylesheet
css
Native consumers · out-of-treenot shipped from this repo
iOS
Apple TV · tvOS app
native
AT
Android TV / Google TV
native
RK
Roku channel
native
SM
Smart-TV web · Tizen / webOS
native