Tokens come out of garage_ui/tool/export_design_tokens_test.dart, the components are CSS on top of them, and the cards cover foundations, components and whole hub and Arcs & Angles screens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
Garage design system
The look of every Garage app — the hub, the payment portal, Arcs & Angles — as tokens, CSS and preview cards, for designing new Garage screens.
It's generated from the Flutter package, not drawn separately:
tokens.css and tokens.json come out of
garage_ui/tool/export_design_tokens_test.dart, and components.css only
uses those tokens. Don't edit the token files by hand — change garage_ui and
rerun:
cd garage_ui && flutter test tool/export_design_tokens_test.dart
design_system/
tokens.css, tokens.json colours, density, type, shape — generated
components.css the components, built on the tokens
foundations/ colour, type, spacing, shape, density
components/ buttons, fields, selects, lists, dialogs, …
patterns/ whole screens from the hub and Arcs & Angles
The one rule: let the theme speak
The colour scheme, the density and the component's variant decide every colour, font size, weight, icon size, padding, radius and gap. Designing a Garage screen is choosing which component and which variant. Then leave it alone.
- No custom colours, font sizes or weights on a component or its contents.
- No icon sizes or colours — an icon takes them from where it sits.
- No hand-picked padding, gaps or radii. Use the
--gap-*and--radius-*tokens, and let components that already carry spacing keep it. - No home-made badges, pills, warning boxes or popovers built from a box with a fill and a border. If a component doesnt exist, that's a gap in the system, not an invitation.
- No shadows. Everything is flat — surfaces are separated by a step in lightness and a hairline border.
If something looks wrong, the answer is a different variant or a different component.
Setting up a screen
<html data-scheme="carbon" data-density="product">
<link rel="stylesheet" href="components.css">
- Scheme:
carbon(default — true black, no accent hue; what the Garage apps run),dark,light. - Density:
product(default — web and product apps, 12px text, 33px controls),compact(dense desktop tools like Arcs & Angles, 10px text, 23px controls),normal(compact but roomier). Pick one per app. Don't mix.
Type is Geist, and Geist Mono for numbers and readouts. Body text is weight 300, control labels 400, primary buttons 600.
Layout
Every Garage screen is the same shape: a chrome ground, panels standing on
it, and panel-gap gutters between them.
<div class="shell">
<nav class="panel sunken">…rail…</nav>
<main class="panel">…workspace…</main>
</div>
Headers and footers are chrome-bars. Only one panel is highlighted
(panel active) at a time.
A hub page is a centred column inside the workspace panel: a page-heading,
then either a stack of properties-sections or sub-heading + settings-list
forms. See patterns/.
Choosing components
Buttons
| Variant | For |
|---|---|
primary |
the one action on the view, or the thing that's on / chosen |
secondary |
a neutral filled control; the selected nav item; a button welded to a field |
outline |
a normal action that isn't the main one — Cancel, Retry |
ghost |
chrome, toolbars, nav items, row actions |
ghost-destructive |
remove / revoke / delete as a row action |
destructive |
the confirm button of a destructive dialog |
link, text |
inline in copy |
Icon-only buttons are btn icon-only. A nav item is ghost with a leading icon,
left-aligned, secondary when selected — never primary. Toolbar toggles are
primary when on, outline when off.
Fields
- Free text: the default outline
field. - Values inside a properties section:
field secondary. A read-only value is a read-only secondary field — never bare text in a value slot. - Selects: secondary in a properties section,
ghostin a settings list, outline elsewhere. - Numbers in an editor: secondary, centred, unit as a muted suffix.
Lists
properties-section— a boxed card of label/value rows. Inspectors, account pages, one card per record in a list. Controls inside are secondary.settings-list— unboxed rows ruled between entries, for forms. Controls inside are ghost; text fields are outline.
Subtitles are short static descriptions of what something is. Never live data — no counts, dates, names, emails or statuses. Facts go in rows.
Dialogs are dialog with the actions in their band: outline Cancel, then
a primary or destructive confirm. Max width is fixed; don't widen it.
Loading is a spinner. No skeletons. Empty is one muted line. Error is the message, muted, and an outline Retry.