Monogram Design System
The visual language behind Monogram AI — read out of the shipped iOS binary rather than off the screen. Twenty-eight semantic tokens with no accent among them, system SF Pro, flat cards that separate on tone alone, and a fixed catalog of ~38 components the model composes from. Every value here carries the class of evidence it came from, because the colors were extracted and the measurements were not.
Color
Twenty-eight semantic tokens, each carrying a light and a dark value, read directly out of the app’s compiled asset catalog. Components reference content/primary and surface/secondary, never raw hex — which is the mechanism by which a screen the model composed five seconds ago is automatically correct in both appearances. There is no accent token. Nothing saturated, across all 28. Precisely 1 carries any chroma at all — surface/footer (#F5F4F0), a warm tint for the what's-next and cheat-sheet bands — the only token that isn't neutral, and nowhere near saturated. So “no accent” is the true claim and “fully achromatic” would be the overclaim. Either way, color in Monogram comes from content.
Surface — light / dark · the canvas and everything on it
Content — light / dark · every mark that carries meaning
Two of those fail WCAG AA as text on the light canvas and the table says so: content/tertiary measures 1.69:1 and content/secondary-muted 2.32:1. They are real tokens and this is real system data, so they are documented rather than quietly corrected — but anything that has to be read should step up to content/secondary at 5.33:1. This page renders its own tertiary UI text at #757575 (4.61:1) for exactly that reason. Documented value is not always rendered ink.
Border — light / dark · eight edge tokens, which is how a flat system builds depth
Color that isn’t in the system · and why that is consistent, not contradictory
Saturated color does reach the screen — orange map pins, green and red judgment pills in the comparison flow. None of it is a token, which is precisely why it can be vivid without breaking the rule. The claim is about the chrome, and stating the exception is what makes it true.
Corrections the binary forced · kept visible rather than quietly folded in
/* Usage — resolves per appearance, no hand-branching */ color: var(--content-primary); /* #1F1F1F · light | #EDEDED · dark */ background: var(--surface-secondary); /* #F5F5F5 · light | #141414 · dark */ border: 0.5px solid var(--border-subtle); /* The affordance for an editable value. In-system by rule. */ border-bottom: 1.5px solid var(--border-spotlight); /* NOT an invented accent — the system has no accent to invent from. */
Typography
The app is system SF Pro, and the proof is an absence: there is no UIAppFonts key in the plist, and the only bundled face is a Latin Modern math font for rendering equations. So the Inter on monogram.ai is web-only. This also resolves something easy to misread — the in-app tracking looks like SF metrics because it is SF.
Type scale · measured in the 402pt frame, not extracted
Weights run 400, 500, 600, 650, 700 — the 650 step is real and recurs, which is what a variable face buys over a four-cut family. The color column maps the measurement notes’ legacy ink ladder onto the binary’s token names (ink-2 → content/secondary, ink-3 → content/secondary-muted). Easy to get wrong: a first pass promoted both ink-3 roles two steps up the ladder.
Families
/* The whole type system, in two lines. */ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui; letter-spacing: -0.019em; /* ≈ iOS's -0.32px at 17pt; the app's values are absolute */ /* Do not self-host SF Pro. system-ui resolves more accurately, and the licence does not permit it. */
Spacing & Layout
Everything here is a measurement, not a token. Monogram’s numeric spacing, radius, and type-scale constants are compiled Swift values and are stripped from the release binary — a DesignTokens.Radius type exists, but its numbers do not survive as data. So these came off screen recordings at 804×1748, and the page labels them accordingly. It is the one place where this documentation is weaker than it looks.
Layout constants
The steps that recur in Monogram’s own chrome are 6 / 16 / 32 / 40pt. Note what is not in that list: the 8pt and 9pt steps quoted elsewhere in this documentation are the parameter dock’s inter-card gap and interior spacing. Those are mine, not measurements of the app, and folding them into a list of the app’s rhythm would be the quiet kind of overclaim.
/* The rhythm that recurs everywhere in an answer */ padding-inline: 32pt; /* content margin */ --section-rhythm: 40pt; /* block end → next header */ --header-to-sub: 6pt; --sub-to-content: 16pt;
Radius
A five-step ladder: buttons at 14, cards at 18, things that hover at 22, a modal sheet at 28, and anything fully interactive as a pill. It is tempting to read altitude into that — rounder the higher it floats — but the system’s own data refuses it. The POI overlay carries the heaviest shadow in the product at 22 while a flat sheet sits at 28, and the shadowless entity card at 18 is rounder than the equally flat action button at 14. The ladder tracks component family, not height. The parameter dock sits at 14, the button step — which is the first structural hint that it is chrome rather than content.
border-radius: 18pt; /* entity card — the most-used surface in the product */ border-radius: 22pt; /* step card, POI overlay */ border-radius: 28pt; /* sheet, top corners only */ border-radius: 999pt; /* chips, judgment pills, the top-actions capsule */
Elevation & Material
The entity card — the surface an answer uses more than any other — has no shadow and no border. It separates from the canvas on tone alone. Shadow is spent only on things that genuinely float: the top-actions capsule, a step card lifted off the warm band, an overlay above a map — 4 in total, and the heaviest of them belongs to the mic ring rather than to anything inside an answer. And there is a real material in the system, not just a gradient: iOS 26 glass on the bottom bar, so content ghosts through it as it scrolls underneath.
One caveat on the hairline this page draws every surface with. Its width is not in any source: border/subtle is a color token with no width, and the only stroke width recovered from the prototype is the dock’s 0.8pt sheen. 0.5px is a reasonable read of a hairline on a 2× display and it is what renders here, but it is mine rather than theirs. Derived
Material & hardware
/* Depth = tone + a hairline. Not a shadow. */ background: var(--surface-secondary); box-shadow: none; border: none; /* All four shadows in the system, and what each is for */ box-shadow: 0 2px 10px rgba(30,25,15,0.08); /* capsule, dock card */ box-shadow: 0 4px 18px rgba(0,0,0,0.06); /* step card on the warm band */ box-shadow: 0 6px 36px rgba(10,10,10,0.32); /* the mic RING, not the pill */ box-shadow: 0 12px 40px rgba(0,0,0,0.18); /* overlay above the map */
Motion
The signature isn’t a curve, it’s a cadence. An answer arrives as a typewriter of components, one block at a time, each fading up and rising about 13pt — roughly 530ms apart. There are no skeletons and no spinner; the screen simply becomes an answer. Hover the table below to replay the real stagger, or a track to compare the curves.
Generation cadence · frame-counted at 30fps · interval ≈530ms
Each element’s own entrance is fade 0→1 + rise ~13pt over ~190ms, ease-out. The arrival centroid decays 369.2 → 362.3 → 358.8 → 357.3 → 356.6, which is an expo-out.
Easing · recovered from the compiled web CSS
Durations · and which one the prototype actually shipped
All eight are documented from the same place: one sentence in the measurement notes, sitting under an easing block sourced to the web CSS. Splitting them by provenance would invent a distinction the source doesn’t make. The built column is the honest one, and it is brutal — only the entrance exists in the prototype. A first pass at this table marked five of the eight as shipped, which is exactly the error the column was added to catch.
What the build actually animates with · springs, not curves
Worth being precise about, because it is a genuine divergence: the curves above are real and they come from Monogram’s web build. The app’s feel is spring-driven, and the prototype followed the app. Any document that lists dock expand 220ms as shipped app behavior is describing the website.
Rules that outrank every curve
- No skeletons.Content simply arrives. Nothing stands in for it first.
- Nothing re-arranges after arrival.A component that has landed does not move again. This is the property Live Parameters had to preserve to be editable at all.
- The answer page never shows a spinner.Streaming replaces waiting. A spinner would admit the screen is not yet an answer.
/* The cadence, which is the thing worth copying */
@keyframes arrive { from { opacity: 0; transform: translateY(13pt) } }
animation: arrive 190ms cubic-bezier(0.22, 1, 0.36, 1);
/* one block per ~530ms — no skeleton, no spinner, nothing re-arranges */Architecture
The reason a design system belongs in a case study about an AI product: Monogram’s model doesn’t draw a UI, it emits a typed document. A tree of components chosen from a fixed native vocabulary, serialized over protobuf, rendered as SwiftUI. The model owns which components and what content; the app owns how they look. Bounded generative composition, not generative rendering. The model owns which components and what content; the app owns how they look.
The response is a serialized component tree, not text. Actions are first-class.
~38 native component types the model arranges. It selects and fills; it never draws.
28 tokens, light and dark. Components reference names, never hex — so a screen composed five seconds ago is correct in both appearances.
Each node maps to a SwiftUI view. Text streams; 3D assets load by id from CDN so the payload stays small; some inference runs on-device.
The trade
A fixed vocabulary plus tokens cannot produce a broken or off-brand layout, and it runs at native speed no matter how strange the prompt. The cost is that anything outside the catalog cannot be shown — so the roadmap is the catalog. Every new capability is a new component, shipped to a client that must keep rendering every screen the model already knows how to compose. That is the constraint this study designed against, and it decided everything downstream: no new primitives.
model → protobuf component tree → fixed catalog → token layer → SwiftUI
/* Actions are first-class in the contract */
UpdateAction { target, value, valueType } /* a mutation primitive, shipped */Component Catalog
~38 component types — the model’s entire instruction set, extracted from the binary. Read the emphasized rows first. The eight interactive input controls and the three actions were already in the catalog; in every flow observed they appear at intake, gathering parameters before an answer generates, and then the answer renders and they are gone. That is the whole Live Parameters thesis in one picture: the primitives exist, pointed at the wrong moment.
The lists below name 43 things, which is not a contradiction: 36 component types, 4 league-pack variants of SportsScorecard, and 3 action types. ~38 is the count of things the model composes with.
Layout & containers
Content blocks
Card lists
A size ramp — and it IS the emphasis system. If the model cannot draw, it says “this matters more” by picking a bigger card. Hierarchy without layout authority.
Rich domain cards
Hand-built for high-value verticals, down to individual league packs. The roadmap, made visible.
Media
Interactive input controlsBinary
Already in the catalog. Emitted at intake, then retired the moment the answer renders — the whole Live Parameters thesis.
ActionsBinary
UpdateAction carries a target, a value, and a valueType — a mutation primitive, already shipped.
The card ramp is the emphasis system
Worth isolating, because it answers a question the architecture raises. If the model can’t draw, how does it say “this matters more”? It picks a bigger card. SmallCardList through HeroCardList is a visual-hierarchy control expressed as component choice — hierarchy without layout authority. It is the most elegant idea in the catalog.
/* Same vocabulary every time. Only selection, order, and content change. */
"chewy cookie recipe" → MarkdownText → EntityView grid → step timeline → FeatureList
"compare the Lucid Air" → HeroCardList → DetailsTable + judgment pills
"plan a kid's party" → RecapForm (OptionsPicker, NumberStepper) → FeatureList
^^^^^^^^^ the controls, used once, at intakeIcons
Hugeicons for content. Bespoke for chrome. Content glyphs are open-ended — the model might name any noun — so they need a library with breadth. Chrome is a closed set that users touch constantly, so it earns bespoke geometry tuned to its exact optical size. The Hugeicons, 6,756 glyphs, bundled whole is bundled whole for the content half; the chrome is the app’s own set. This is the correct call, and the study adopted it rather than trying to improve on it.
The glyphs below are drawn from the app’s own path data, reused verbatim. Strokes read oddly until you know the conversion: the app strokes at weight × size / 24 × 1.4, so in a 24-box the effective stroke is weight × 1.4 — which is why these are 3.00 and 2.24 and 1.5 rather than the raw weights, and why the source’s own comments check out.
Three details worth the space
- chrome/menu draws three bars of unequal length.12, 16, and 8 units. The prototype’s first pass used SF Symbols’ , which draws three equal lines, and it was wrong. The taper is the whole character of the mark — and this is the one chrome glyph that is not bespoke: it matches Hugeicons
menu-02exactly, kept because it already fit. Redrawing it would have been craft for its own sake. - The remove glyph is an add glyph, rotated.The chip set’s ✕ is the app’s own turned 45°. No new asset was introduced for it. In a closed chrome set, that restraint is the system working.
- Every glyph has round caps and round joins.Including
chevron-left, which the teardown calls a “straight 2pt chevron”. That reads like a mitered join and isn’t — “straight” describes the path being straight-sided, versus the curved chevron used for . A first pass here rendered it mitered with butt caps, contradicting the source.
The asset catalog embeds the original SVGs, LZFSE-compressed in `bvx` blocks — 44 documents. Recoverable as vectors with liblzfse. Used to verify geometry, not redistributed. Binary
/* stroke in a 24-box = weight × 1.4 */ chrome/plus weight 2.14 → 3.00 "3pt rounded bars" chrome/menu weight 1.60 → 2.24 chevron-left weight 1.43 → 2.00 "2pt chevron" stepCounter/± weight 1.80 → 2.52 "2.5 stroke" chrome/keyboard weight 1.07 → 1.50 "1.5 stroke" prototype/convoMic weight 0.86 → 1.20
The Dock
The one genuinely new arrangement in the proposal, so it owes the clearest lineage of anything here. And the lineage is short: it is the intake card, kept alive.
The birthday flow already renders the model’s parameters as cards — Whose party? Which day? How many kids? — inside a RecapForm, built from OptionsPicker and NumberStepper. That is Monogram already conceding the job needs your parameters, in a grammar it already ships, at a moment it has already designed. The only thing Live Parameters changes is when it dies. Today the intake card goes inert the instant the plan generates; here it descends below the answer and stays. The intake becomes the dashboard.
Scope, stated before any of the numbers: the dock exists only in the SwiftUI prototype. The case-study page shows it as recorded video of that app. It was never re-implemented as DOM, and nothing here should be read as claiming otherwise.
It runs across all five of the interfaces in that section, not just the recipe: the recordings under Live Parameters are a cookie recipe, an EV comparison, a party plan, a movie pick and a restaurant search. Fourteen parameters across the five interfaces, carried by four card types — and the docks stay at two or three cards each. Every specimen below is one of those cards, at the values the footage shows, so the two pages can be checked against each other.
Parameter type → control · every type, as a specimen
Rebuilt here from the measured spec, so the geometry can be inspected rather than described. The card is the unit, not the control: a 40×34 thumb, a 10.5pt label over a 13pt semibold value, and a control that displaces that stack when the card opens. Tap a card to open or close it. The stepper glyphs are the app’s own; the segmented thumb runs at the measured 180ms; the press is 120ms to 0.97.
Three treatments, keyed to what is actually built. Solid cards are live and are in the footage. Dashed cards are drawings of the spec and do nothing, because a specimen you can operate is a claim that the thing exists. A partial row draws both halves — the built control live, the missing piece dashed beside it.
The recipe's own card. Steps by 6 from a floor of 6; at the floor the − dims but stays tappable.
The movie card's runtime cap at its default 140, 85–180 by 5 — the recording drags it to 90, where the film changes. Drag it or use the arrow keys; the app springs the knob rather than easing it.
Texture — three options. Four is the widest enum in the build.
Add and remove, live, at the real 2–3 domain. The dashed chip is swap, which was specified and never built.
Left, the card the build ships. Right, the toggle the spec asked for — drawn, because a switch can only name one of the two states.
A drawing. Genre padded past its real four options to show a case that never occurs.
A drawing. Shown open — a collapsed date chip looks like every other collapsed card.
The receipt, and what replaced it
The receipt chip above is specified, not shipped — and the substitution the build made instead is the more interesting answer. Rather than flashing a box behind a changed value, it transitions the value itself, rolling its digits. The result is the receipt. Listing it as built would have been the easy lie.
The count it would have reported is nine: the recipe carries nine ingredients, and a servings change rescales all nine. Bake time is bound to texture rather than to batch size, so it genuinely doesn’t move — which is the harder half of the claim, and the reason the chip would have had to name what didn’t change too.
Bake at 350°F for 12 minutes, until the edges set and the centers still look underdone.
Shell anatomy · measured, save one authored value — none of it extracted
Three states, not two · the third one isn’t in the spec
Rules the dock holds itself to
- One open card at a time.The open card is a single optional id, so opening one closes the last.
- An out-of-range stepper button dims but stays tappable.A dead button would refuse silently. Dimmed-and-live can explain itself.
- Sections never reorder.The layout-stability contract, enforced in the source by comment: a value change is a legal edit; re-arranging surviving sections is not.
- The glass band never eats a tap.The 168pt bottom material is explicitly non-hit-testing.
- The dock arrives after the answer.It is chrome over the answer, so its entrance is timed off the answer's own block count rather than a hand-authored delay — 11 blocks lands it at 2.40s.
/* The dock is chrome over the answer, so it arrives after the answer does.
Its entrance is computed from the answer's own block count, not authored: */
dockDelay(blocks) = 0.7 + 0.11 × (blocks − 1) + 0.45 + 0.15
→ 11 blocks = 2.40s
/* Open cards overrun a 402pt frame (415, 425, 431pt against a 384pt fade),
so the row measures its own overflow and shifts. Measured, not constant —
eyeballing it is how the clipping got missed the first time. */
shift = min(0, (384 − 8) − openMaxX)Asset Derivation
Every element in the prototype is one of three things, and this page says which. The point of the exercise is the third column: a proposal that needs a long New list hasn’t understood the system it is proposing to. Three entries here are new, and each owes a reason no shipped primitive covered it.
Specified and never built · the gap between the document and the artifact
A design system page that documents only what worked is marketing. Six things were specified and are not in the prototype — including, awkwardly, the rule the whole proposal leans on hardest.
- The inline whisper affordanceA barely-there underline on bound numerals. Accent tokens were declared in the prototype and used nowhere — all manipulation ended up in the dock.
- Two recompute tiersLocal under 100ms versus model under 2s. Every setter in the build is synchronous, so there is no tier distinction to observe.
- The receipt chipSpecified as the proof-of-work surface. Never built. The build instead transitions the changed value itself, rolling its digits — a deliberate reversal, and arguably the better answer: the result is the receipt.
- The undo stackSpecified alongside the receipt. Not built.
- The conflict reason lineThe build ships the refusal shake and discards the reason string, with a comment reserving it for the voice layer. So a refusal is currently felt but not explained.
- The four-parameter salience capA written rule with no enforcement. Built docks carry two or three cards; nothing in the code prevents a fourth or a ninth. The most important rule in the system is the one least defended by it.
Claims I can’t stand behind · listed so you don’t have to find them
- The dock's card shell descends from Monogram's own entity dock.The prototype credits an App Store screenshot for the 14pt radius, the 40×34 thumb, and the 8pt gaps. That screenshot is not in the research folder, so those figures can't be re-derived from anything kept. Either the source gets added or these downgrade to “authored in their idiom.”
- chrome/plus has 16pt arms.The teardown records 16; the path actually in use spans 13 units in a 24-box. The stroke reconciles exactly at 3.00, the arm length does not. One of the two is wrong and it can't be settled without decompressing the shipped vector.
- The controls are never bound to rendered answers.The binary proves the controls exist and that UpdateAction carries a target and a value. It does not prove the negative. This is an observation from the flows I watched — and if Monogram binds them somewhere I didn't see, the proposal gets smaller and more obvious.
- The mic pill measures 88 × 44pt.An earlier measurement pass recorded 84 × 52. The build and the device frame agree on 88 × 44, so that is what ships here, but the two readings were never reconciled.
- Each component entrance runs ~190ms.A later audit at 120fps put it nearer 150ms. The prototype uses 190ms. The discrepancy is real and unresolved; 190 is the value the motion shipped at, not necessarily the app's.
Principles
The throughline behind the tokens — why the system looks the way it does, and the tie-breakers for cases the tables don’t cover.
Color comes from content
Tone, not shadow
Composed, not drawn
Nothing re-arranges after arrival
Controls are earned, not sprayed
Open sets get a library, closed sets get bespoke
Anti-patterns
The guardrails. If a change introduces any of these, it has left the system.
- Introducing an accent color for an affordance — the system has 28 tokens and none of them are saturated. Use border/spotlight.
- Raw hex in a component — reference the semantic token so the screen is correct in both appearances for free.
- A shadow on an entity card — it is flat by rule. Depth is the tonal step to surface/secondary.
- Re-arranging surviving components after an edit — content may change and sections may come and go, but the spatial map has to hold.
- A spinner or a skeleton on an answer — content arrives instead. A placeholder admits the screen is not yet an answer.
- Self-hosting SF Pro — the app uses the system face. system-ui resolves more accurately and the licence does not permit the alternative.
- Body text on content/tertiary or content/secondary-muted — 1.69:1 and 2.32:1. Step up to content/secondary at 5.33:1.
- Adding a dock control because a parameter exists — four is the cap. Every quarter ships a component, every component has parameters, and an uncapped dock becomes a cockpit.
- Quoting a duration from the web CSS as app behavior — the site ships curves, the app is spring-driven. They are not the same system.
Source
Colors, the component catalog, the linked frameworks, and the icon bundles were read out of the shipped binary — Monogram AI v1.4.2 · ai.monogram.app, pulled July 2026 via ipatool → assetutil / nm / strings, plus frame-counted screen recordings. Type and motion were frame-counted at 30fps off six screen recordings at 804×1748. Where the binary disagreed with an earlier measurement, the binary wins, and the corrections stay visible in the Color section rather than being folded in quietly.
Numeric spacing, radius, and type-scale tokens could not be recovered. They are compiled Swift constants and are stripped from the release binary. A `DesignTokens.Radius` type exists; its values do not survive as data. Every size on this page is a measurement off video, labeled `measured`. The color tokens are the only values here read directly from the app.
The app’s own mark is also absent here. The bundle holds a launch-screen logo whose geometry was never extracted, so this page’s wordmark is typographic rather than a drawing — inventing a logo and setting it beside real extracted tokens would poison the evidence around it.
Unofficial concept study. Not affiliated with Monogram. Nothing redistributed; no assets scraped.
MonogramDesign.MD/ ├── DESIGN.md # written spec (agent-followable) ├── tokens.json # machine-readable tokens, both appearances └── README.md # methodology + this page's route // the page renders from: src/components/monogram-design-log/tokens.ts