One system, three surfaces.

Ristra is a shared shopping list that keeps households in sync, aisle by aisle. This design system is the single source of truth for how it looks and feels in three places: the app (one web client that runs in the browser and inside the iPhone app), the marketing site, and Figma. Every color and dimension comes from one token source, and every component below is documented as it ships in the app today.

9 brand anchors 7 tonal ramps · 84 primitives 34 semantic tokens · light + dark Documents app 1.5.1 · every text pair AA · updated 2026-09-23
How it works

Tokens flow downhill

Colors, dimensions, type, and motion live in a single source module. A zero-dependency generator expands the nine brand anchors into tonal ramps, verifies every semantic pairing against a WCAG contrast contract, and emits the file each surface consumes. Nothing downstream is hand-edited.

The app: web and iPhone

tokens.css is copied verbatim into the app and never hand-edited. Since 2.0 (September 2026) the iPhone app is a Capacitor shell around the same web app, so one stylesheet themes every platform. The few platform differences are CSS dialects keyed off data-platform on the root element (see App shell).

App-level aliases

Every color the app draws text or glyphs with is a token here, under the contrast gate. What the app still layers over the tokens in its app.css is layout and material: the 26 px card radius, the layout margin, control sizes, row padding, glass. See Spacing & layout and Color.

Marketing site

getristra.com loads the same tokens.css before its own stylesheet; its dark-field aliases (--ink-*) are earth-ramp references. The brand SVGs and both display faces ship from this repo's assets/.

Figma

ristra.tokens.json is a W3C-DTCG token file: ramps as primitives, the semantic set as one collection with light and dark modes. The Ristra Design System library in Figma was built from these values in July 2026.

Adoption is complete. The web app, the native iOS app, and getristra.com moved onto the system on 2026-07-10, and Figma followed on 2026-07-24. On 2026-09-10 the native iOS and Android clients were retired in favour of Capacitor shells, so the generated Theme.swift and Theme.kt have no consumer; they stay in the generator, dormant (see Tokens & downloads). The phase-by-phase record is in MIGRATION.md.
Start here

What changed

The decisions this page reflects, newest first. Each one was made and shipped in the app first; this is where the design record catches up.

DateDecisionSee
2026-09-23getristra.com moves to Bold & Strung, a Graft kit built on these ramps exactly as this repo emits them: a chili opener with a golden marker, the espresso and sand fields, the system font at Black weight, hand-placed cards. The site's own pages no longer load tokens.css (its /proto pages still do).Marketing hero
2026-09-23Contrast: every text pair in the app passes WCAG AA, measured in the running app in both themes and gated twice (the token contract here, an axe-core check in the app's test suite). The swipe colors, the Finish teal and a new --danger moved into the tokens; Skip It deepened to ochre-600, in-cart text to turquoise-700, secondary text by 0.02 L.Color, Swipe actions
2026-09-23The Font Awesome kit renders here and on getristra.com: a sprite generated from the app's own icon registries replaces the stand-in drawings.Iconography
2026-09-23This styleguide caught up with the shipped app (1.5.0). The launch lockups became canonical here.Launch & loading
2026-09-21What's New: after an update, one centred card of two to four icon rows, shown once per device per version.Sheets & dialogs
2026-09-20Skipped rows leave the aisles for a Skipped card below them. Under a store lens, a long press drags a row within its aisle, and the order is saved per store.Aisle cards
2026-09-16Dark Home paints --bg flat: the paper grain is a light-theme layer. On the dark field it lifted #0f0a06 to #1e1916, too close to the cards.Home
2026-09-14UI pass 1. The native-iOS layout runs on every platform: large-title navigation bar, glass controls, borderless inset-grouped cards, floating buttons. Also: Font Awesome Sharp, the Finish capsule, Staples as its own screen, the seven-step Tour, the launch lockup, and rubber-band scrolling back on in the iOS shell.Navigation bar, App shell
2026-09-10Capacitor: the iPhone app becomes a shell around the web app (store version 2.0.0). The native iOS and Android clients are retired, and SF Symbols with them; the Swift and Kotlin theme files lose their consumers.Overview
2026-08-241.2.0: a store is an ordering lens, not a filter. Store tabs and the store chip row are gone; a lens row names the order the aisles follow.Aisle cards
2026-08-031.1.0: the list runs on swipes. Got It, Skip It and Delete replace the check circle.Swipe actions
2026-07-24The Figma library is built from these tokens: variables, text styles, icons, components.Overview
2026-07-10Design system v1: tokens, generator, contrast gate. Brand v4 (vivid anchors, split icon canonicality), the 975 near-black step, chip tokens and the grain texture.Color
Brand

Logo & lockups

The mark is a ristra — four chilies strung together, one per household member pulling the same list. It hangs on one of two brand fields: sand in light contexts, earth in dark ones. Variant names describe the ink, not the background: use the light-ink files on earth, the dark-ink files on sand.

Full-color lockups

Full-color lockup, dark ink, on sand
logo-color-dark.svg — on sand / light surfaces
Full-color lockup, light ink, on earth
logo-color-light.svg — on earth / dark surfaces

Launch lockup: mark and wordmark, no tagline

Approved 2026-09-14 for the launch screen and the web loading screen (see Launch & loading). Derived from the full-color lockups: tagline paths removed, the wordmark moved down to centre on the mark, the viewBox cropped with equal padding. Same ink naming. These two files are the canonical source; the app keeps verbatim copies.

Launch lockup, dark ink, on the light page color
ristra-lockup-color-dark.svg — on --bg, light
Launch lockup, light ink, on the dark page color
ristra-lockup-color-light.svg — on --bg, dark

Monochrome lockups

For single-color contexts: embossing, watermarks, partner co-branding, or anywhere the full palette would fight the surrounding design.

Mono lockup, earth ink
logo-dark.svg — earth ink on light
Mono lockup, sand ink
logo-light.svg — sand ink on dark
Mono lockup, sweet-orange ink
logo-bright.svg — sweet ink, high-energy moments

Icon tiles & bare mark

Icon tile, light
icon-light.svg — light-mode tile
Icon tile, dark
icon-dark.svg — dark-mode tile
Bare mark
mark-full.svg — transparent, either field
Icon canonicality splits by surface (Brand v4). The Apple Icon Composer bundle (ristra.icon) — glass materials over the vivid brand anchors — is canonical for the iOS app icon only. Every web and marketing icon surface (favicons, PWA set, touch icons, site marks) rasterizes from the flat tiles icon-light.svg / icon-dark.svg in this repo. In the app, the stacked mark itself is the logo, and it also watermarks Home at 5% opacity; the old v2 line chili survives only as decorative illustration.
Brand

Wordmark & tagline

Two display faces, two jobs, and they never trade. Solitreo sets the wordmark — and only the wordmark. Jua sets the tagline and short display moments on marketing surfaces. Neither ever sets body copy; body and UI text are always the native system stack.

Ristra wordmark
wordmark.svg — Solitreo, outlined, turquoise-500
Shared lists, aisle by aisle.
tagline.svg — Jua, outlined, sand

Live specimens

Ristra
Shared lists, aisle by aisle.
ABCDEFGHIJKLM abcdefghijklm 0123456789

--font-display: 'Solitreo' · --font-tagline: 'Jua' · webfonts are Latin-subset woff2 (~16–22 KB)

FaceTokenUse forNever for
Solitreo--font-displayThe wordmark "Ristra"Anything else — headings, UI, body
Jua--font-taglineTagline; short marketing display linesBody copy, UI labels, in-app text
System stack--fontAll body and UI text on every platform—
Mono stack--font-monoCode, token names, quantity and count pillsProse
Brand

Clearspace & minimum sizes

Give the ristra room to hang. Keep a clearspace of one chili-height (¼ of the mark's height) on all sides of any lockup, and never let other elements cross it.

AssetSizeNotes
Bare mark24 px tall minimumBelow that, use the icon tile instead
Full lockup (mark + wordmark + tagline)160 px wide minimumDrop the tagline below 220 px
Launch lockup (mark + wordmark)240 pt wide, fixedLaunch and loading screens; at phone widths a tagline would set about 15 pt tall, so it stays off
Wordmark alone72 px wide minimum—
Icon tiles16 px minimumFavicon floor

Don'ts

  • Don't recolor the chilies outside the four brand body colors.
  • Don't set the wordmark in any face but Solitreo, or the tagline in any face but Jua.
  • Don't place the dark-ink lockups on earth, or light-ink lockups on sand.
  • Don't rotate, outline, shadow, or add effects to any lockup.
  • Don't use the app-icon glass palette in UI or marketing surfaces.
Brand

Launch & loading

The first thing anyone sees. The launch screen shows the launch lockup 240 pt wide, centred on --bg in both appearances. The web loading screen draws the same lockup at the same size and position, so the hand-off from the native launch to the app doesn't move.

Launch screen, light appearance
Light · earth-50
Launch screen, dark appearance
Dark · earth-975

To scale: a 402 pt screen with the 240 pt lockup · iOS: storyboard + named color + appearance-variant image set · web: Splash.svelte

  • It follows the phone's appearance, not the in-app theme. A launch screen can't read the app's setting, and the only override would force the keyboard and system sheets into one mode too.
  • The loading screen paints in its first frame: it inlines the SVG rather than loading an image. If loading runs past three seconds, "Loading your list…" fades in 24 px below the lockup in --text-muted.
  • iOS gets PNGs at @2x and @3x, generated from these SVGs and tokens.css; an SVG in a launch image view has black-screen reports. Never set a splash background color in the shell's config: it paints one color over dark mode.
Foundations

Color

Nine brand anchors — the four chili bodies, the stem, the two fields, and the two wordmark turquoises — expand into seven tonal ramps. Anchors are brand and illustration colors; interface color always comes from the semantic tokens, which are locked to contrast-safe ramp steps by a generator-enforced WCAG contract.

Brand anchors

Values are verbatim from the Figma brand file and the SVG assets. Anchors flagged brand only fail AA as text on their field — use their ramp's deeper steps in UI instead. Click any swatch to copy its hex.

Tonal ramps

Twelve steps per ramp, 50→975, built on an OKLCH lightness ladder. The dot marks the step where the brand anchor survives verbatim. The earth ramp doubles as the warm-neutral ladder: sand sits at 100, earth at 950, and every surface, border, and text tone between them is brand-derived. The 975 near-black exists for dark-mode backgrounds — in dark themes, cards sit at earth-950 (the brand field) on an earth-975 page.

Semantic tokens

The interface vocabulary. Names match the app's custom properties exactly, so a value change here is a value change there. Status colors are text colors (item names), so they carry the 4.5:1 text contract; the swipe colors are the same in both themes. Live values follow the page theme — flip the toggle up top and watch them re-resolve.

TokenLightDarkRole in the app

Materials (app-level)

The app layers these over the tokens in its app.css. None of them carries text: every color text or glyphs are drawn in is a semantic token above.

AliasResolves toUsed for
--separator--border mixed 60% into --surfaceInset row hairlines
--glass-bg--surface at 72%, blur 16, saturate 160%Glass circles and capsules, glass floating buttons
--bar-material--bg at 80%, blur 20, saturate 180%The navigation bar, only once content scrolls under it
--glass-shadow0 4 24 earth at 10% · dark: black at 50%Glass and floating controls

Contrast, proven live

Every pair in the contrast contract, read from the generated token file: each swatch draws its actual token pair and computes the WCAG ratio in your browser. The generator enforces the same minimums at build time — a failing pair can't ship. Pairs held to 3:1 are large text or glyphs, so their sample is set large. The app's test suite then checks, with axe-core, that every screen uses these pairs as intended.

Light theme

Dark theme

Foundations

Typography

Body and UI text is always the platform's native stack: SF on Apple platforms, Roboto on Android. Zero font payload in product surfaces; the two brand faces appear only where the brand rules allow.

Token scale

The general-purpose ladder: settings screens, forms, this page, and the marketing site.

--text-3xl
36px · 1.15 · bold — page titles
Saturday's big shop
--text-2xl
28px · 1.2 · bold — section titles
Produce & pantry
--text-xl
22px · 1.3 · semibold — card titles
Weekly staples
--text-lg
18px · 1.45 — leads, emphasized rows
Two avocados, just ripe enough.
--text-base
16px · 1.5 — body, inputs
Tortillas, the good corn ones from the middle aisle.
--text-sm
14px · 1.45 — secondary UI, meta
Added by Sam · Produce
--text-xs
12px · 1.45 — captions, timestamps
Updated 2 minutes ago

Interface text styles

The native-iOS layout (2026-09-14) sets its text at the iOS text-style sizes: Large Title 34, Body 17, Footnote 13. They apply on every platform, as literals in the components named.

StyleSize / lineWeightColorWhere
Large title34 / 41700--titleScreen titles; they scroll away (NavBar)
Bar title17 / 22600--title-smallFades into the bar once the large title passes under it
Row title17 / 22500 list · 600 Home--textItem rows (ItemRow), list rows (Home)
Dialog title21–24800--titleTour, What's New
Section header13 / 18700--text-mutedAisle headers, sheet titles; the lens row at 600
Subtitle and note13 / 18400--text-mutedFreshness line, row notes (one line, ellipsis), Home meta
Band13 / 18700--trip-textThe trip band
×212 / 16 mono400--text-muted on --pill-bgQuantity pills, aisle counts
1228 / 34 rounded700--textHome shortcut counts (tabular figures)
Inputs stay at 16px. Anything smaller triggers iOS Safari's focus zoom. This is why --text-base is the floor for form controls, including the Staples screen's field (raised from 15 in September 2026).
Foundations

Spacing & layout

A twelve-step scale for general spacing, plus the layout metrics of the native-iOS layout the app adopted on 2026-09-14. Those were measured against the retired native app and are pinned at 402 pt (iPhone 17 Pro) by the app's metrics suite, within 0.5 px.

Layout metrics

AliasValueRule
--edge16 · 20 at ≥ 414 wideThe iOS layout margin. Cards, the nav bar, and the floating buttons inset by it.
--row-pad-x= --edgeRow content insets by the layout margin, so row text, separators, and Home icons line up with it.
--row-pad-y1515 + 22 + 15 = a 52 px single-line row. Home rows add 2 each side: 17 + 42 + 17 = 76.
--nav-bar-h44Sits under the safe area: --nav-top = max(safe top, 8), so 62 + 44 = 106 on an iPhone 17 Pro.
--control-size44Glass circles and the buttons inside capsules. A capsule of two is 6 + 44 + 3 + 44 + 6 = 103.
--float-size48Floating buttons.
--float-bottommax(safe bottom − 6, 16)28 above the bottom edge on an iPhone 17 Pro.
--float-clearance--float-bottom + 48 + 24The room a scrolling screen leaves under its last row so it clears the floating buttons.
Section rhythm26 above a header, 10 belowCard to card through an aisle header measures 56.

Widths: the floating bar caps at 640 px and centres, so on a desktop window the cart and the + stay near the list; sign-in and join forms cap at 440 px. The app respects device safe areas through the --safe-* insets.

Foundations

Radius & elevation

Soft, and flat by default. Inset-grouped cards take the iOS 26 corner (26 px) with no border and no shadow: the --surface fill alone separates them from the --bg page. Shadow is for things that float or lift.

--group-radius
26px · fill only
--radius
14px
--radius-sm
9px
--radius-pill
999px
--shadow
lifted
--glass-shadow
floating
RadiusWhere
26 · --group-radiusInset-grouped cards: aisles, Skipped, Complete, Home's list group. Also a row or aisle lifted mid-drag.
22Centred dialogs (Tour, What's New)
20, top cornersBottom sheets
14 · --radiusHome shortcut tiles, Home banners, dialog buttons, settings-screen cards
12Icon tiles (Home rows, What's New), menus
9 · --radius-smInputs, form buttons, theme chips
999 · --radius-pillCapsules, the Finish buttons, pills and badges, the composer field
LayerElevation
Cards on the pageNone. Fill only.
Glass controls, floating buttons--glass-shadow
A row or aisle lifted mid-drag; menus; the hover quick-acts; settings-screen cards--shadow
Bottom sheets0 −8 40 rgba(40,22,12,.28) over a 45% scrim
Centred dialogs0 20 60 rgba(40,22,12,.4) over a 55% scrim
Foundations

Motion

Quick and settled — rows glide, sheets rise, nothing wobbles. Three durations, two curves. Click a chip to play its timing.

--dur-fast · 120ms · --ease-out — state flips, hovers, quick-acts
--dur-base · 200ms · --ease-out — swipes settling, sheets rising
--dur-slow · 320ms · --ease-in-out — slow reveals, like the loading line
MovementTiming
A swipe settling open, closed, or out on commit200ms --ease-out
Arming: the action circle stretches into a pill150ms --ease-out
Neighbours sliding aside during an aisle or row drag150ms --ease-out
The bar title fading in150ms --ease-out
Sheet rise · scrim fade · dialog pop200ms · 150ms · 180ms ease
Trip dot pulse2s ease-in-out, looping
  • Reduced motion: every movement above has an instant or still fallback. The Tour's swipe step holds one still frame with both swipes open.
  • Reduced transparency: glass and the bar material flatten to solid --surface / --bg, and Home's texture layers hide.
  • The rubber band is scroll physics, not animation. The iOS shell keeps the native bounce, which shows --bg past the edge; it locks while any sheet or dialog is open.
Foundations

Iconography

One family, two weights, every surface. Ristra draws Font Awesome Sharp from its licensed kit: Sharp Regular for interface chrome, Sharp Solid for aisle and list-type glyphs. Weight is the only thing separating the two roles. The app bundles its glyphs so they render offline; this page and getristra.com load a generated sprite of exactly the same glyphs, so all three always agree.

Interface chrome · Sharp Regular (fasr)

GlyphKey (UI.*)Font AwesomeRole

Sizes: 16–20 in nav controls, 18 on floating buttons, 13 in aisle headers and the lens row, 14 in quick-acts, 22 in What's New tiles. Chrome glyphs take --accent inside glass controls; the back chevron reads as text.

List-type icons · Sharp Solid (fass)

A list's type picks its default icon; people can choose any of these from the picker. The household document stores only the key.

Licensing and source. The kit (@awesome.me/kit-f4cb7e0690) is licensed for Ristra's own surfaces: the app, this page, and getristra.com. The two sites load assets/icons/ristra-icons.svg, which the app repo generates (scripts/gen-design-icons.mjs) from its own icon registries: only the glyphs the app uses, never the library, and never a glyph the app doesn't use for that meaning. The repos that commit the sprite stay private.
Foundations

Departments

Twenty-six aisles with stable keys: fourteen in the default grocery walk, eleven specialty aisles that appear only through a store's department set (hardware, garden, and the like), and Other. Each gets a Sharp Solid glyph, rendered muted in its aisle header; departments never get their own accent colors.

GlyphKeyLabelFont AwesomeSet
Primitives

Buttons

Shape follows placement. Form actions on a page or sheet are rectangles; the button that closes a dialog spans it; anything in a bar or floating over the list is a circle or a capsule. Accent is the default action color. Teal is reserved for finishing a trip.

Form and dialog buttons

Form: app.css .btn (--accent, 9px, 16/600) and .btn-ghost (--accent-strong label, --border hairline) · Dialog: 14px, 16/700, full width; the paired ghost sits on --surface-sunken (Tour.svelte, WhatsNew.svelte)

Bar and floating controls

Finish Join

Glass circle 44 · capsule 103 (two 44 buttons) · floating 48: glass cart (--accent-strong), glass camera (--status-incart), prominent + (--accent) · Finish capsule (--finish-*) · Join (Home) · app.css .glass / .fab, BottomBar.svelte

Text actions

Change End Skip

Change (lens row): --accent-strong 13/700 · End (trip band): --text-muted 12/700, underlined, a 45 x 34 hit area · Skip (Tour): --text-muted 15/600, 44 px hit area

  • Teal means finish. --finish-bg is used by the Finish capsule, the in-list Finish shopping button, and the Complete count, and nowhere else. It passes AA in both themes (4.59:1 light, 8.1:1 dark).
  • The Finish capsule's name is exactly its label. It has no aria-label, so assistive tech hears "Finish", the word on screen.
  • Focus rings are a 2 px --title outline, offset 2 px (drawn inward inside clipped cards).
  • Destructive text and buttons (delete a list, delete an account) use --danger: chili-700 in light, chili-400 in dark, with --on-danger for a label on the fill. It replaced a hard-coded red that measured 2.34:1 in dark.
Primitives

Inputs & forms

Quiet fields on surface with hairline borders; focus is a turquoise ring and the border steps aside. 16px minimum font size, always. Quick add is the one pill-shaped field: it's a composer, not a form.

Quantity and notes are optional.

Field: --surface, 1px --border, 9px, 12/14 padding · focus: 2px --title outline, transparent border · composer: pill on --surface-sunken + 46px --accent go (QuickAdd.svelte) · stepper 40 x 40, radius 10 · checkbox accent-color --accent

Primitives

Pills & badges

Counts and quantities ride the sand pill in mono. Status pills sit in the nav bar. The one teal badge counts what's in the cart.

×2 ×2 3 4 offline saving… Eggs Added — reading the label…

Quantity + aisle count: mono 12/16, --text-muted on --pill-bg · bought quantity: --finish-* · Complete count: --finish-* · offline: 11/700 caps --status-unavailable · saving: --text-faint on --surface-sunken · staple chip: dashed --accent (QuickAdd) · toast: BottomBar flash

Store chips are retired. The store selector row went away with store tabs in 1.2.0 (2026-08-24); the lens row replaced it. The chip tokens stay for compatibility: --chip-active-bg / --chip-active-text are now aliases of --finish-bg / --finish-text, --chip-bg fills the trip-complete seal, and --chip-border / --chip-text are unused.
Primitives

Toggles

Choices of three are equal chips with the pick filled in accent — the theme picker in app settings is the canonical instance. On/off settings are full-width rows with a dot that fills. Try both.

Chips: 1px --border, 9px, 15/600 --text-muted · selected: --accent fill, --on-accent label · toggle row: --accent border and dot when on · AppSettings.svelte .theme-chips / .toggle

Components

List item row

The flagship component — the row a household touches a hundred times a week. No checkbox: the name leads, a tap opens the editor, and swipes do the rest. A single-line row is 52 px, with an inset hairline between rows.

In an aisle

Corn tortillas×2
The good ones from the middle aisle
Limes
Cold brew
Reading the label…

In the Complete card: bought, then bought while a trip is live

Oat milk×2
Avocados×3

In the Skipped card

Hatch chiles
StateLooks likeWritten as
Needed--text name on --surfaceneeded
BoughtStruck through in --status-bought (= --text-muted) on --trip-wash; the quantity pill turns teal (--finish-*)bought
In cartA bought row while a trip is live: --status-incart (turquoise-700) name, no strikeRender-only; still bought
Skipped--status-unavailable name, in the Skipped card. Outside that card it adds a "Skipped this trip" tag.skipped (legacy: unavailable)
  • Anatomy: name 17/22 medium; quantity pill when above 1; a coupon glyph in --accent-strong; one line of note (or the photo read) that ellipsizes rather than growing the row; a 48 px photo thumbnail at the trailing edge, radius 8.
  • Press: the row goes --surface-sunken while a finger is down. A tap opens the edit sheet.
  • Long press under a store lens lifts the row to drag within its aisle (see Aisle cards).

ItemRow.svelte inside SwipeRow.svelte · padding --row-pad-y / --row-pad-x · separator: 1px --separator, inset --row-pad-x both sides, hidden on the last row and whenever the row is displaced

Components

Swipe actions

Completion is a leading full swipe; skipping is a trailing full swipe at the screen edge; Delete sits inboard and is tap-only. A destructive action never rides a flick.

Leading edge open · Got It

Got It
Limes×4

Trailing edge open · Delete (tap only) and Skip It

Delete
Paper towels, the big pack

A taller row past the commit point: labels show, the circle arms

Got It
Corn tortillas
The good ones from the middle aisle

A bought row: Put Back leads, Delete trails

Put Back
Oat milk

Mouse and keyboard: quick-acts on hover or focus

Eggs, dozen
EdgeNeeded rowsBought and skipped rows
Leading (swipe right)Got It, full swipe commitsPut Back, full swipe commits
Trailing (swipe left)Delete (inboard, tap only) + Skip It (at the edge, full swipe commits)Delete (tap only)
  • Feel: the row captures after 10 px of travel; a full swipe commits past 55% of the row's width or on a fast fling; open states snap to 84 px per action. The left 24 px are a dead zone, left to the system's back gesture. One row is open at a time, and a tap anywhere else closes it.
  • Compact rows: under 64 px tall there's no room for circle and label, so the action goes icon-only on a 30 px circle, the way Mail does.
  • Never gesture-only: on a mouse or keyboard, hovering or focusing a row reveals the same actions as 28 px buttons.
  • The write lands after the exit animation, so the row finishes leaving before the list re-sorts.

SwipeRow.svelte (zones, thresholds) · ItemRow.svelte (actions per status, quick-acts) · zone colors: --swipe-got-bg turquoise-600, --swipe-skip-bg ochre-600, --swipe-delete-bg terra-600, all the same in both themes; white labels clear 4.5:1 on each

Components

Aisle cards

Items group into borderless inset-grouped cards, one per aisle, in the walking order of the chosen store. Each header sits on the page above its card and scrolls with it. Headers are wayfinding, not content: they stay quiet.

Aisles ordered for SproutsChange

Produce

2
Roma tomatoes
Limes×4

Dairy & Eggs

1
Eggs, dozen

Skipped

1
Hatch chiles

Complete

2
Oat milk
Corn tortillas×2
Finish shopping

Saves the trip & clears 2 checked-off items · skipped items and staples come back

  • The lens row names the order: "Aisles ordered for {store}" or "Default aisle order", with Change opening the store sheet. The choice is per person and per device. During your own trip it locks to a faint label, so the order can't shift mid-shop.
  • Header: Sharp Solid glyph at 13 px, the label in sentence case (13/18 bold), a count pill, and, only under a store lens, a grip. Drag the grip to reorder aisles for that store.
  • Rows reorder too: under a store lens, long-press a row and drag it within its aisle. The order is saved per store by item name, so it survives items being cleared and re-added every trip.
  • Order down the page: aisles, then Skipped (after the last aisle, so aisle counts show only what's left to get), then Complete, then Finish shopping.
  • Skipped wears the skipped color on its glyph and count. Complete collapses, fills with --trip-wash, and tints its hairlines teal.

DepartmentSection.svelte (variant aisle · skipped · complete) · ListView.svelte (lens row, drags, Finish shopping) · card: --surface, --group-radius, no border, no shadow · header padding 26 / 10

Components

Home

Your lists in one inset-grouped card, a shortcuts dashboard under it, and two floating buttons: + for a new list, Join for a shared one. Home is the one screen with texture.

Flip the page theme: the grain shows only in light

  • List rows are 76 px: a 42 px icon tile (radius 12, the list's Sharp Solid glyph in --accent on --surface-sunken), the name at 17/22 semibold, members at 13/18 muted, a faint chevron. The hairline starts at the text, not the tile.
  • Shortcuts are two-column tiles in the Reminders shape: a round glyph top left, the count large top right, the title under them. Tapping Staples asks which list first when you have more than one.
  • Texture is decoration only. Paper grain at 35% in light only (on the dark field it lifted the page to #1e1916, too close to the cards, so dark Home paints --bg flat), and the brand mark oversized at 5% in both themes. Both are click-through, and both hide under reduced transparency.

ListsScreen.svelte · InstallBanner.svelte and UpdateNudge.svelte appear above the lists when relevant (see Trip & status)

Components

Sheets & dialogs

Choices rise from the bottom as sheets. Moments that need a pause — the Tour, What's New — are centred cards. Both dim the page, trap focus while open, and close on Escape.

Bottom sheet · the store sheet in its "start" mode

Centred dialog · What's New, with the 1.5.0 notes

AspectBottom sheetCentred dialog
Used forStore choice, the edit sheet, share, quick add, list and department pickersTour, What's New
Shape--surface, 20 px top corners (quick add: 18), 40 x 5 grab handle in --border--surface, 22 px, up to 420 wide, 16 from each edge
Scrim · entrance45% · rises over 200ms55% · pops over 180ms
Title13/700 --text-muted (pickers) or 18/700 --title (Share)21–24/800 --title, with a 13/600 muted eyebrow on What's New
Options16 px rows, 13 x 12 padding, radius 10 on press; a tick in --accent marks the current oneWhat's New: 44 px glyph tiles on --pill-bg, glyph in --accent, 16/700 title, 14.5 body in --text-muted
  • Every overlay marks itself (data-modal) so the iOS page bounce locks while it's open.
  • Dialogs own the Tab key while open: focus moves to the title on open, stays inside, and returns to whatever opened it.
  • The Tour is the same centred card as a seven-step carousel: add, photo, staples, store lens and aisle drag, swipes, Finish, share. Each step has art on a 376 x 150 slot (--surface-sunken, radius 16) drawn in tokens with the real glyphs, dots in --border with the current one in --accent, Skip top right, and "Let's go" on the last step. It shows once per device.
  • What's New shows once per device per version, after an update, never on a first run (the Tour shows then). Settings > Help reopens it.
  • Confirming a finish uses the system alert, not a custom dialog.

Menus

Past trips Staples Settings

HeaderMenu.svelte (More, in the list's capsule) · --surface, 1px --border, radius 12, --shadow · items 15/600 with --accent glyphs · StoreChoiceSheet.svelte · WhatsNew.svelte · Tour.svelte

Components

Trip & status

A live trip is the list's loudest state and it's still quiet: a sage band pinned under the nav bar with a pulsing dot. Connection state is a small pill beside the nav controls, never a banner.

Your trip

Shopping in progress · SproutsEnd

Someone else's trip

Sam is shopping · Sprouts — add anything now

Connection, beside the nav capsule

offline saving… Updated 2m ago · by Sam

Home cards

Add to your Home ScreenFor push notifications, offline access & the full app.Install
A newer Ristra is in the App Store. Update to keep getting improvements.Later
  • The band is 34 px: 13/18 bold --trip-text on --trip-wash, a --trip-border hairline drawn as an inset shadow so it adds no height, and a 7 px --trip-dot pulsing every 2 s.
  • End vs Finish. End (your trip only) cancels and leaves every item as it is. Finish, on the capsule or under Complete, saves the trip and clears what you got.
  • Offline reads the device's connection, not the sync layer's cache flag, which reads "not confirmed" far too often to mean offline. Saving… appears while local edits wait to sync.
  • The freshness line under the large title says who changed the list last and when: the trust signal of a shared list.
  • Home cards (install on the web, update in the app) are --surface tiles at radius 14 with no border.

ListView.svelte .trip-band · FreshnessBadge.svelte · InstallBanner.svelte · UpdateNudge.svelte

Patterns

App shell

One layout on every platform since 2026-09-14: the retired native iOS app's. The page is --bg; a large-title nav bar sits over it; aisles are inset-grouped cards at the layout margin; three floating buttons hover at the bottom with no dock behind them. The document itself is the scroller.

Before a trip: large title, the lens row with Change, the cart starts shopping

Your trip: title in the bar, the band pinned, the lens locked, the cart becomes Finish

PartRule
PageNatural document flow on --bg: no fixed shell and no inner scroller, so safe-area insets report correctly on iOS.
Nav barSticky; transparent at rest; the trip band pins inside it.
CardsInset by --edge; the next header's 26 px top padding is the gap between cards.
Floating buttonsFixed, capped at 640 wide and centred. Left: the cart starts a trip, becomes Finish during yours, and hides during someone else's. Right: add by photo, then + (accent). The list keeps --float-clearance under its last row.
BackGoes to the screen underneath, whichever way you came: a screen with two ways in (Staples) can't hard-code its back target. Opening a screen starts at the top; going back restores where you were.

Platform dialects

The root element carries data-platform (web, ios, and later android), set before first paint. Only iOS has a dialect today.

DialectiOS shellWeb
OverscrollRubber band on, showing --bg; locked while an overlay is openOff (no pull-to-refresh mid-list, no trackpad bounce)
Glass rimA 1 px top highlight (white at 45%) on glass controls, as iOS draws its ownNone; it would read as a stray border
Two layouts coexist. Home, the list, and Staples run the layout above. Settings, Past trips, My Stores, App settings, Feedback, and Admin still use the earlier one: a --surface top bar with a 28 px title, and 14 px cards with --shadow.

Members

Everyone here can add, check off, and finish.

Patterns

Marketing hero

Marketing surfaces lead with chili. getristra.com is built on Bold & Strung, a Graft kit (~/Sites/Graft/kits/bold-and-strung) that takes these ramps exactly as this repo emits them and casts them into three color fields, with heavy system type, cards set down by hand and one marker per headline. The hero at the top of this page is an older, darker cousin.

Recipe (getristra.com, since 2026-09-23): the chili field --terra-700 (sand type at 6.6:1) with two --terra-800 orbs · the header lies over it: the app icon and the Solitreo wordmark in sand, a sand button · an overline in --terra-200 · the headline in the system font at 900, tight tracking, its second line stepped in, and one golden marker (--ochre-300 behind --earth-950 type) · a sand primary button and a sand outline · the dark-mode app capture in an espresso bezel, turned 1.5° · below it the page alternates the espresso field (--earth-950: the screenshots, the footer) and the sand reading field (--earth-50, turning --earth-975 in the dark theme, with a 2px seam line where it meets espresso) · Solitreo sets only the wordmark, and Jua is not used · every pair in the kit's contract (276, both themes) passes AA, and axe checks the rendered pages.
Resources

Tokens & downloads

Everything on this page's color sections is generated from one source module. Regenerate with node scripts/gen-tokens.mjs — the contrast contract runs on every build and refuses to emit failing tokens.

tokens.css

CSS custom properties, light + dark. Copied verbatim into the app (src/tokens.css) and getristra.com; never hand-edited downstream.

ristra.tokens.json

W3C DTCG format. In Figma: Tokens Studio → import → sync ramps and both semantic modes as variables.

tokens.resolved.json

Machine-readable resolved hex map — what this page's color sections render from.

ristra-icons.svg · .json

The app's Font Awesome Sharp glyphs as a sprite (<use href="…#ui-check">), and the manifest this page's icon tables render from. Generated in the app repo from its own icon registries; licensed for Ristra's sites.

Brand SVGs

Lockups, the launch lockups, icon tiles, the mark, wordmark, and tagline in assets/brand/. The app copies the ones it needs verbatim into its brand/.

Theme.swift · Theme.ktDormant

The native iOS and Android theme files. Both clients were retired for Capacitor shells on 2026-09-10, so nothing consumes them; the generator still emits them in case a native target returns.

ResourceWhere
Migration record (web → iOS → marketing → Figma → Android → Capacitor)MIGRATION.md
Figma brand file (the nine anchors originate here)Ristra-branding
Token source of truthtokens/tokens.source.mjs
App-level aliases (mirrored on this page)the app's src/app.css, top block
Brand SVGs & subset webfontsassets/brand/ · assets/fonts/

Ristra · a Longbrook Studio project · getristra.com · app.getristra.com