v1.39.0

Consumer Design System · v1.39.0 · 2026

Built for products that billions of people touch.

A business-to-consumer design system: 190 component categories across seventeen domains — commerce, social, media, onboarding, growth, trust, AI & agents, text animation, and spatial & direct manipulation. Every entry carries a decision register, an accessibility contract, and a platform anchor. Humans copy the code below each preview; agents read the machine contracts. Same bytes, two doors.

190Categories
17Domains
2Themes
AAWCAG 2.2 baseline
0Dark patterns
Institutional sibling

The contracts are plain JSON — whatever you already run can read this system.

Original, independent work. This system cites public platform guidelines — Apple Human Interface Guidelines, Material Design 3, WCAG 2.2, App Store Review Guidelines, Google Play policy, GDPR, COPPA, FTC rules — as design constraints, the way a building cites a building code. It is the consumer-market counterpart to the Institutional Finance Design System: same discipline, different physics — where the institutional system optimises for density and auditability, this one optimises for clarity, delight, and trust at scale.

Humane & Empathetic Design

Design with Kindness: Anti-Dark Patterns & Psychological Safety

Consumer software touches real human emotions—stress, financial vulnerability, excitement, fatigue. CDS rejects manipulative psychological traps. Every growth and engagement primitive is engineered with radical empathy and respect for user dignity.

Streaks with Grace Days

Guilt-free retention. Automatic pause buffers prevent users from feeling punished or anxious if life gets busy for a day.

FTC 1-Click Honest Cancellation

Respect user autonomy. Canceling a subscription requires exactly as few steps as signing up—zero guilt-tripping loops or hidden buttons.

Transparent Truth in Pricing

All-in pricing from step one. No drip pricing, no deceptive auto-checked add-ons, and no fake countdown timers.

Sensorial Calm & Reduced Motion

Respect cognitive energy. Calming spring physics, breathable whitespace, and 100% adherence to prefers-reduced-motion.

The domain map

Counts are computed at runtime from the live navigation — they can never drift from the sidebar. Click a domain to filter.

What every entry carries

The entry contract

4-cell register · live preview · copyable code · machine contract
Live preview

Rendered from the same tokens the code uses — light and dark, no screenshots.

When to use

The job the component exists for, stated so a reviewer can check it.

When not to

A system that can't say no isn't a system. Every entry names its misuse.

A11y contract

Focus, labels, target sizes, reduced motion — the behaviour that must survive a redesign.

Platform anchor

A public guideline the entry answers to — HIG, Material 3, WCAG 2.2, store policy, GDPR, COPPA, FTC.

Design principles

01

Clarity before cleverness

Consumer attention is borrowed, not owned. Every screen answers "what is this, what can I do" in the first glance — decoration never competes with the answer.

02

Deference to content

The interface is the frame, not the painting. Glass, hairlines, and restraint keep the user's photos, products, and words as the loudest thing on screen.

03

Motion is physics, not garnish

Motion explains where things came from and where they went. Through v1.29 that meant one spring curve everywhere; from v1.30 it means one curve per role — Navigation, Ambient, Reveal, Commit, Settle — because a duration scale answers how big an element is, not what the movement is doing to the user. Both are still one decision, taken once, in tokens. Every animation respects prefers-reduced-motion as a contract, not a suggestion.

04

Touch first, then scale up

44px minimum targets, thumb-reach layouts, bottom sheets over centered dialogs on small screens. Desktop is the enhancement, not the default.

05

Trust is a feature

Consent is real, cancellation takes as few steps as signup, and growth components persuade without trapping. The FTC's word for the alternative is a rule violation; ours is a design failure.

06

One contract, two readers

Humans read the previews; agents read tokens.json and components.json — generated from this page, so the two can never disagree.

Start · Machine Layer

The system agents can read.

Two JSON contracts and a drop-in agent brief ship alongside this page. They are not documentation about the system — they are the system, in a shape a tool call can consume. The component index is generated from this page's own navigation, so it can never drift from what you're looking at.

Readable by whatever you already run

The contracts are plain JSON over HTTPS — anything that can fetch a URL or speak MCP can consume them, cloud assistants and on-prem open-weight models alike.

The read path — how an agent consumes this system

This is the reading logic, step by step. It isn't prose an agent might follow — steps 1–4 ship inside components.json itself as agentProtocol, bundles, and hardRules, so any agent that fetches the index receives its own instructions with the data.

00

Discover

An agent landing anywhere on this page finds the machine layer without parsing the HTML: two link rel="alternate" hints in the head, Dataset structured data, and the site-level llms.txt all point at the same two files.

<link rel="alternate">Dataset JSON-LDllms.txt
01

Fetch the index

One GET returns all 190 entries carrying their whole decision register — when to use, when not to, the behaviour & accessibility contract, the platform anchor, and the canonical URL — plus the protocol itself. An agent never has to parse the HTML to build compliantly. Generated from this page's own navigation: what the agent reads and what you're reading cannot disagree.

GET cds/components.json
02

Select, never invent

Entries are chosen by matching domain and summary against the brief. A component that isn't in the index doesn't exist — the agent's creativity budget is spent inside contracts, not around them.

filter: domain + summary
03

Read the register

Each entry's URL deep-links to its four-cell register. The when-not-to cell is binding — a brief that asks for a component inside its own named misuse gets a refusal with a citation, and the anchor is a constraint, not a footnote.

entry.url → #registerwhen-not-to: binding
04

Resolve bundles & hard rules

Some components require others — paywall ships with cancellation, UGC ships with report & block, checkout shows the all-in total. Bundles auto-include; hard rules (COPPA, truthful LIVE, symmetric consent) can block the build outright.

bundles[]hardRules[] → BLOCK
05

Build from tokens

The agent pulls the token slice it needs — never raw hex — inheriting both themes, the single spring curve, and the reduced-motion contract in one fetch.

GET cds/tokens.jsonno raw hex
06

Gate to a human

Anything irreversible — money, sends, permissions — stops at a confirmation gate that authorises an exact payload, not a plan. The trace logs effects and blocked steps with the rule that blocked them. The model produces; the human signs.

agent-confirm gatetrace: effects + blocks

Connect it

Direct fetch works today

No server, no install. Point any agent, script, or CI job at the two files — the component index carries every entry's id, domain, summary, platform anchor, and canonical URL.

curl https://edwson.com/cds/tokens.json curl https://edwson.com/cds/components.json # or, in an agent prompt: # "Read https://edwson.com/cds/components.json and build # the consent screen to its rules — reject and accept # visually symmetric, per the entry's GDPR anchor."
MCP server proven pattern

The institutional sibling already ships this way — and since eds-mcp v1.17.0 it serves these files too: 29 tools, 65 institutional contracts and this 190-entry consumer catalogue from one engine. These contracts share the same shape, so a cds-mcp server would be a thin adapter over the same two files — which is exactly why one has not been written. components.json now carries a mcp.toolsToFileMapping block naming which slice of which file each tool reads, so the server is reproducible from the contract rather than trusted.

{ "mcpServers": { "eds": { "command": "npx", "args": ["github:Edwson/eds-mcp"] } } } # github.com/Edwson/eds-mcp — the production precedent

What a tool call sees

Request

        
Response

        

The two JSON files are real and downloadable today, and eds-mcp v1.17.1 (29 tools) serves them over MCP — the same engine that already serves the institutional system's 65 contracts. Payload shapes in the console above are simplified for reading.

Local models and air-gapped networks

Adding the full decision register to every entry made this contract better for a hosted agent and three times worse for a local one: the index is now 296.4 KB, roughly 75.9K tokens, which is the entire window of most models people actually run on their own hardware. So the same generated data is also written at four smaller sizes. One node cds/generate.mjs run produces all of them, which is the only reason they can be trusted to agree.

Load this Size ≈ tokens What it holds
cds/RULES.txt 4.2 KB 1.1K protocol, hard rules, bundles, the non-negotiables — the whole contract in prose
cds/tokens.json 19.3 KB 4.9K both themes, all contrast pairs, the motion contract
cds/domains/<domain>.json 6.1–44.8 KB 1.6–11.5K the full register for one domain only — 17 shards, 6–41 entries each
cds/index.slim.json 54.1 KB 13.9K id, name, domain, summary for all — enough to choose, not enough to build
cds/components.json 296.4 KB 75.9K everything, full register per entry — for hosted agents and CI
The complete offline recipe measured, per domain

Rules card + tokens + one domain shard is 7.6K tokens at the smallest domain and 17.5K at the largest (core, 41 entries). Three domains — growth, media, navigation — fit inside an 8K window whole; every domain fits a 32K one with room to write in. And it is complete: the model has the refusal conditions, both themes, and the full register for everything it is allowed to use. Nothing is fetched at build time.

cat cds/RULES.txt cds/tokens.json cds/domains/commerce.json \ | ollama run qwen2.5-coder:14b "Build the checkout screen to this contract." # or, once, then never again: curl -O https://edwson.com/cds/RULES.txt curl -O https://edwson.com/cds/tokens.json curl --create-dirs -o cds/domains/commerce.json \ https://edwson.com/cds/domains/commerce.json
Air-gapped no network at build time

None of this is an API. There is no server to reach, no key to rotate, no schema to compile, and no embedding index to rebuild. Fetch the files once, commit them beside your code, and the contract keeps working on a machine with the network off — including node cds/verify.mjs, which is zero-dependency and runs against the checkout rather than the site.

# the whole contract, offline, verifiable node cds/generate.mjs --check # are the tiers in sync? node cds/verify.mjs # 4,058 assertions, no network

Why the shards can be trusted. They are not a second copy maintained by hand. Every tier is written by the same generator run from the same components array, and cds/verify.mjs asserts it: the 17 shards must hold 190 components between them, the slim index must list the same ids in the same order, and the rules card must contain every hard rule by id — a card that silently drops a blocking rule is worse than no card at all.

Make it yours — and prove you did not break it

Most systems let you re-brand into an accessibility violation in silence: you change a hex, the page still renders, and nobody finds out until an audit. Here the tokens are the input to a measurement, so a re-brand that fails cannot be written to disk.

1 · Change only the eight that matter brand layer

Eighty tokens per theme, but a re-brand is a handful of them. Everything else — radii, spacing, the twelve motion primitives, the five motion roles, the attenuation floor — is structure, and changing it changes behaviour rather than identity. Override in a stylesheet that loads after the system, or edit the :root and [data-theme="dark"] blocks directly if you have forked the page.

/* the live values, as shipped — copy and repoint */ :root { --accent: #0a65ee; /* the one that carries the brand */ --bg: #fbfbfd; /* page ground */ --surface: #ffffff; /* raised card */ --surface2: #f5f5f7; /* recessed panel */ --text1: #1d1d1f; /* primary copy */ --text2: #66666b; /* secondary copy */ --text3: #6b6b70; /* the one that fails first */ --border: #e8e8ed; /* hairline */ } [data-theme="dark"] { /* both themes move together, never one */ --accent: #2f8fff; --bg: #000000; --surface: #101014; --surface2: #1a1a20; … }
2 · Let the generator refuse you the safety net

The token file is not hand-maintained — it is read back out of the page and the contrast matrix is recomputed from your hex values. If any of the 44 pairs per theme drops under 4.5:1, the generator exits non-zero and does not write the file. You cannot ship a re-brand that quietly failed, because the artefact never gets produced.

node cds/generate.mjs # ✗ dark --text3 on --surface2: 3.9:1 — below the 4.5:1 the system claims # exit 1, tokens.json unchanged node cds/verify.mjs # 4,058 assertions, incl. this table node cds/verify.mjs https://edwson.com # or against the deployed site

What a re-brand does not license. Three tokens are load-bearing for meaning, not for taste: --red carries destructive and error semantics, --green carries confirmed state, and --csp-floor is the opacity at which receding context stops being readable. Re-pointing the first two at brand colours — or lowering the third for atmosphere — breaks the when-not-to cell of every component that relies on them — and the register, not the palette, is what an agent is holding you to. If you need a brand red, add a token beside --red rather than over it.

Measured, not claimed

This page cites WCAG 2.2 dozens of times, so it owes you its own numbers. Every ratio below is computed from the raw hex values in tokens.json — and the contract is self-testing: node cds/verify.mjs re-runs 4,058 assertions (page ↔ index parity, anchors, bundles, skills, and this exact table) against your checkout or against the live site (node cds/verify.mjs https://edwson.com). Exit code 0 or it isn't true.

Text pair Light Dark
Primary text on pagetext1 / bg 16.28 AAA 19.29 AAA
Primary text on cardtext1 / surface 16.83 AAA 17.43 AAA
Primary text on inset paneltext1 / surface2 15.46 AAA 15.91 AAA
Primary text on raised chiptext1 / surface3 14.28 AAA 13.78 AAA
Secondary text on pagetext2 / bg 5.52 AA 8.16 AAA
Secondary text on cardtext2 / surface 5.71 AA 7.38 AAA
Secondary text on inset paneltext2 / surface2 5.24 AA 6.73 AA
Secondary text on raised chiptext2 / surface3 4.85 AA 5.84 AA
Tertiary / hint text on pagetext3 / bg 5.13 AA 6.35 AA
Tertiary / hint text on cardtext3 / surface 5.30 AA 5.74 AA
Tertiary / hint text on inset paneltext3 / surface2 4.87 AA 5.24 AA
Tertiary / hint text on raised chiptext3 / surface3 4.50 AA 4.54 AA
Accent text & links on pageaccent / bg 4.95 AA 6.49 AA
Accent text & links on cardaccent / surface 5.11 AA 5.87 AA
Accent text & links on inset panelaccent / surface2 4.69 AA 5.36 AA
Accent text & links on accent panelaccent / accent-soft 4.54 AA 4.76 AA
Success text on pagegreen / bg 4.89 AA 9.19 AAA
Success text on cardgreen / surface 5.05 AA 8.31 AAA
Success text on inset panelgreen / surface2 4.64 AA 7.58 AAA
Success text on success panelgreen / green-soft 4.53 AA 6.66 AA
Error text on pagered / bg 4.99 AA 7.45 AAA
Error text on cardred / surface 5.15 AA 6.73 AA
Error text on inset panelred / surface2 4.73 AA 6.14 AA
Error text on error panelred / red-soft 4.51 AA 6.06 AA
Warning text on pageamber / bg 4.84 AA 10.08 AAA
Warning text on cardamber / surface 5.01 AA 9.11 AAA
Warning text on inset panelamber / surface2 4.60 AA 8.31 AAA
Warning text on warning panelamber / amber-soft 4.56 AA 7.60 AAA
Accent-2 text on pagepurple / bg 5.04 AA 7.69 AAA
Accent-2 text on cardpurple / surface 5.20 AA 6.95 AA
Accent-2 text on inset panelpurple / surface2 4.78 AA 6.34 AA
Accent-2 text on accent-2 panelpurple / purple-soft 4.54 AA 6.02 AA
Tertiary / hint text on accent paneltext3 / accent-soft 4.71 AA 4.66 AA
Secondary text on accent paneltext2 / accent-soft 5.07 AA 5.99 AA
Tertiary / hint text on success paneltext3 / green-soft 4.76 AA 4.60 AA
Secondary text on success paneltext2 / green-soft 5.13 AA 5.92 AA
Tertiary / hint text on error paneltext3 / red-soft 4.64 AA 5.17 AA
Secondary text on error paneltext2 / red-soft 5.00 AA 6.65 AA
Tertiary / hint text on warning paneltext3 / amber-soft 4.82 AA 4.79 AA
Secondary text on warning paneltext2 / amber-soft 5.19 AA 6.16 AA
Tertiary / hint text on accent-2 paneltext3 / purple-soft 4.62 AA 4.98 AA
Secondary text on accent-2 paneltext2 / purple-soft 4.98 AA 6.40 AA
Button label on accent fill on accent fillaccent-fg / accent 5.11 AA 6.27 AA
Button label on destructive fill on destructive fillred-fg / red 5.15 AA 6.83 AA

Honesty note: the first run of this table failed — light-theme hint text measured 2.49:1 and the status colours missed 4.5:1 as body text. v1.18.0 changed the tokens, not the claim.

Start · AI-native

Build a screen with an agent.

Pick a brief. Watch an agent read the machine contracts, assemble the screen from this catalogue, and — when the rules demand it — get told no. This is the point of shipping contracts, not documentation: the register every entry carries is enforceable, so the system can refuse a bad build before a human reviewer ever sees it.

Agent trace — tool calls
Assembled from the catalogue
Rule checks

A canned walkthrough against the real component index. Every component chip jumps to the real entry; every rule cited is written in that entry's register. An MCP-connected agent runs the same loop against the same files.

Start · AI-native

Design Agent Mode.

An adversarial operating mode for product strategy: six personas, four strict steps, binding constraints. Load it into any capable agent and a raw idea comes back as a stress-tested, quantified, gap-anchored plan — because a single agreeable voice produces agreeable plans, and the friction between lenses is where a plan gets real.

design-agent-mode.md

The team — six lenses

01

Market Gap Hunter 市場ギャップハンター

Hunts unmet gaps, user pain, competitive whitespace, and the timing window. Every claimed gap needs an observable signal, not a vibe.

02

Product Architect プロダクトアーキテクト

Owns architecture, experience logic and innovation shape — benchmarked against the best-crafted products of the era, designed for extensibility and felt quality.

03

Business Logic Commander ビジネス戦略司令官

CPO lens: business model, monetization, growth flywheel, one North Star Metric. No plan leaves without an economic engine.

04

Risk & Feasibility Slayer リスク狙撃手

Investor + COO lens: attacks feasibility, cost structure, technical debt and execution barriers. Its job is to be right, not liked.

05

Empathy & Validation Engineer 共感・検証エンジニア

Behavioural evidence lens: every feature traces to a real behaviour or validated pain — assumptions get flagged as assumptions.

06

Delivery & Momentum Accelerator 実行アクセラレーター

Converts the surviving plan into a staged roadmap, priorities, MVP definition and a frame a real team can run Monday morning.

The SOP — strict order

01

Gap Diagnosis 市場空白スキャン

All six personas blind-read the input in parallel. Output: the three largest market/user/business gaps, and the three most fatal assumptions or blind spots.

3 gaps3 fatal assumptions
02

Stress & Build Debate 対抗型構築

Architect proposes → Commander layers the business logic → Slayer and Empathy Engineer attack in rounds. The plan must answer: why would a user pay money or time, and why is now the window?

propose → layer → attack
03

Empathy Refinement 共感リファインと検証

Real user stories, behavioural paths and emotional touchpoints get injected — moving the plan from "correct" to "wanted".

correct → wanted
04

Delivery & Metrics Lock 実行と指標ロック

North Star Metric + key sub-metrics, staged roadmap (MVP → v1.0), resource needs and risk mitigation — then the human makes the call. The mode sharpens the decision; it never makes it.

NSM + roadmaphuman go/no-go

Binding constraints

Zero AI-voice AI口調ゼロ

No transition filler, no template sentences — the language of a strong product lead in a working meeting.

Everything quantified すべて定量化

Every recommendation links to a concrete outcome — ARR, retention delta, cycle time, CAC — and every figure is labelled estimate or measured. An unlabelled number is a violation.

Zero tolerance for mediocrity 凡庸ゼロ容認

No differentiation, no real gap → the personas say so plainly. "This direction dies at the starting line" is a legitimate finding, not rudeness.

Gap first 市場ギャップ最優先

Every output answers one question before any other: which whitespace does this build a moat in?

Honest scope. The personas are lenses, not credentials — "top-tier researcher calibre" describes the standard the output is held to, not who is talking. Numbers are estimates until measured, and the mode labels which is which. It critiques directions, not people; regulated domains route through counsel; and the go/no-go stays human. Download the mode, load it into any capable agent, and pair it with the when the plan reaches the screen.

Start · AI-native

A real session: Claude × this system.

This is what working in Claude (Cowork) with these contracts connected actually looks like — the brief, the tool calls, the moment a hard rule pushes back, and the human signing the result. Step through it: every payload shown is the shape the real files return.

Hook it up — three doors, same contracts

Claude Desktop / Cowork config once

Add the MCP server to your config (the eds-mcp production pattern — a cds server is the same shape), or simply tell Claude to read the contracts by URL: no install, works in any chat.

// claude_desktop_config.json { "mcpServers": { "eds": { "command": "npx", "args": ["github:Edwson/eds-mcp"] } } } // or zero-install, in any Claude chat: "Read https://edwson.com/cds/AGENTS.md and follow it for everything you build in this session."
Claude Code one command

One line registers the server for the whole project; CLAUDE.md or AGENTS.md makes the contracts binding for every session that follows.

claude mcp add eds -- npx github:Edwson/eds-mcp # and in your repo's CLAUDE.md / AGENTS.md: # "UI work follows https://edwson.com/cds/components.json # — agentProtocol, bundles and hardRules are binding."

The session, step by step

Claude — meditation-app upgrade screen cds connected
Step 0 of 12

A real working session, replayed in-page. The same loop runs in any MCP-capable agent.

Start · Production evidence

The design file and the code agree.

This catalogue argues that a component is only real once it is written down in a form both a person and a machine can check. That argument is easy to make on a page you control. So here it is on something else: one Button out of the production component library I work in, read twice — once from the Figma inspector, once from the machine-readable specification the shipped implementation was serialised into. Same numbers, same enums. Nothing below asks to be believed; it asks to be compared.

One component, read twice

Figma selection Buttonvariant: outlinedsize: xlargecolor: infostate: default
PropertyFigma inspectorSpecification
HeightH 60"height": 60
Padding · horizontal24"padding": "12px 24px"
Padding · vertical12"padding": "12px 24px"
Gap8"gap": 8
Corner radiusfull pill"borderRadius": 30 → height / 2
Width is excluded on purpose. The Figma frame is set to Hug, so its 118 is whatever the label happened to measure; the specification states no width, so there is nothing to check. A comparison that quietly includes an unfalsifiable row is not a comparison.

Four dropdowns, four enums

The variant panel on the left of that screenshot is not decoration. Each dropdown is a prop, and its options are that prop’s declared value list — same members, same order, same default.

variantcontainedoutlinedtextdefault contained
sizexsmallsmallmediumlargexlargedefault medium
colorprimaryerrorsuccesswarninginfogreydefault primary

The fourth one deliberately does not match. Figma shows state as an enum because a designer needs default, hover, disabled and loading laid out side by side. In code they arrive as independent props — disabled and isLoading — because a button can be both. That is a translation, and it is written down as one. Drift is when a difference is undocumented, not when it exists.

The same shape, one layer down

Button is the worked example because both halves of it can be shown. The discipline is not local to it. One shared size enum — xsmall / small / medium — is the sizing contract for 12 form entry points, and each rung fixes a total height rather than leaving it to whoever draws the frame.

SizeTotal heightPadding · radius
xsmall24"4px 8px" · radius 4
small32"8px 8px" · radius 4
medium40"8px 16px" · radius 8
Specification side only   The Figma panels for these are not reproduced here, so read this as the contract — not as a second parity check. Every figure on it, and every figure in the Button table above, is a multiple of 8 or its half-step 4. That is the whole system: spacing(n) = n × 8px, and a Button’s radius is derived from its height rather than chosen.

How it stays true — the loop

01

Build the component in Figma

As a variant set, not a stack of frames: every prop that exists in code exists as a Figma property, so the two can be compared at all.

variant set = prop set
02

Write the rules down before the components

Which size for which context, what each semantic colour means, the spacing rule, the radius rule — in prose a person and an agent read the same way, so neither has to infer intent from pixels.

rules → then components
03

Serialise the whole thing

Design tokens, prop enums, pixel specifications and the guidance around them become one machine-readable file. The point is that "what this component is" stops living in someone’s memory and starts living somewhere a diff can be taken against it.

one file, both audiences
04

Update under authorization

When something needs to change, the Figma MCP connection and the agent assemble the change against that file. It lands only after a person approves it. The loop is allowed to propose; it is not allowed to merge.

agent proposeshuman authorizes

Described as workflow, not as a result. No cycle-time figure, no percentage, no before-and-after appears above, because none has been measured. What is shown is the shape of the process and one component that came out of it matching.

What this is and is not evidence of. It is evidence that a component’s numbers can be checked against a written contract instead of eyeballed, and that on this one they hold. It is not a claim of authorship over the library: the specification is derived from the shipped implementation, so it documents code that already existed — the work being shown is making that code legible enough that design and implementation can be held to each other. Coverage is partial and stated as such: of its 53 component entries, 15 carry hard pixel specifications; the rest fix props and behaviour and leave layout to the consumer. And the source file, the package it belongs to and its version are not published here — the figures under comparison are the point, and the file is not mine to hand out.

Start · Provenance

Who builds a 190-category system solo?

The same person who ships the institutional one. This page is the range proof; the production record is the depth proof. Every claim below links to its evidence — the discipline of this catalogue is that nothing asks to be taken on faith, including its author.

251Institutional categories
8 → 0Reg rewrites → rebuilds
100K+Traders in production
29MCP tools shipped
10Signed recommendations

Self-audited, not self-described: node cds/verify.mjs last ran against this exact file and returned 4,058 assertions passed, 0 failed — page↔contract parity, every anchor present, bundles and hard rules valid, all 119 skills enumerated, and the token-contrast table recomputed from raw hex. Anyone can re-run it: node cds/verify.mjs https://edwson.com.

And read, not just parsed: node cds/smoke.mjs executes this page’s own scripts against a hand-written DOM, activates all 201 sidebar entries, and dispatches a click on every one of the 975 interactive controls — twice each, because a state machine that only works once is the failure a single click never finds. It then checks keyboard parity: every widget role on a div is clicked, snapshotted, reset, and given Enter and Space, and any control that moves under a mouse and not under a keyboard is named. Last run: 0 runtime errors, 0 keyboard-parity failures. There is no browser and no network on the machine that runs it, which is the point — the DOM is 419 lines and the harness has no dependencies.

The evidence trail

every row links to the artifact itself
Depth

The Institutional Finance Design System — 251 component categories for trading, compliance, and AML, each with a regulatory anchor. This consumer system is its B2C sibling: same four-cell register, different rulebook.

Production

Nearly five years at an ASIC-regulated broker — a 150-component system across five platforms that absorbed 8 regulatory rewrites without a rebuild cycle; KYC completion 27→55% (GA4, Q2–Q3 2022).

MCP, proven

eds-mcp — the institutional system shipped as a real MCP server: 29 tools, 65 contracts across 14 domains (plus this consumer catalogue since v1.17.0), zero-dependency HTTP API with an OpenAPI 3.1 spec, tests green. The pattern this page's machine layer is built to.

AI governance

Ed Agent — a human-gated agent harness (v0.6.2): nine lifecycle stages, red-team + claim grounding, iron-law hard-halts, a deterministic skill router over a 139-method library, 15 MCP tools. The operating belief behind this page's agent demo: agents produce, the harness governs.

OSS range

ReactOmega (51 AI-native React components, shadcn-compatible, with its own MCP server) and GalaxyJS (60 canvas animations, zero dependencies) — the playful end of the same systems rigor.

People vouch

10 signed LinkedIn recommendations — Managing Director to teammates, publicly verifiable, quoted in full. “Ed’s implementation of a centralized design system was a game-changer for our team’s output” — Jim Kao, Frontend Engineer (SDE II), same team.

Why a second system, not a footnote on the first — the institutional one exists because a regulator can ask why a screen looks the way it does, and I need the answer on file. I built this one to find out whether that same discipline holds up somewhere no regulator is watching, whether the rigor was ever really about finance or just about writing the reason down. It held.

This catalogue is new — first published July 2026 — but its patterns already run in the founder's own live consumer products (Duo-Shou, PawsRoam, WizAgents). The numbers above are the institutional and open-source track record whose discipline it inherits.

Glassmorphic

Liquid Glass Surface

Three surface treatments built to Apple’s Liquid Glass material logic — and rebuilt from the blend model up, because the CSS published for that look does not render it. Drag the panel across the wallpaper: the readout is the white label’s contrast against the backdrop under it, and it travels from 16.4 : 1 to 1.1 : 1 without one property of the component changing.

When to use

Chrome that floats over the user’s own content or wallpaper — a media player, a camera control, a home-screen widget — where you want the surface to admit what is behind it rather than hide it. Works only when the backdrop is something you control or can measure.

When not to

Never as a substrate for text you have not scrimmed. Two measurements, both taken here: the backdrop under this panel ranges 1.11 : 1 to 16.41 : 1 across one wallpaper, and the composited glass itself, rendered in Chromium and read back at five positions, ranges 1.25 : 1 to 7.51 : 1 — so the surface narrows the range and never rescues it. Never inside an ancestor that isolates or flattens — isolation, transform, filter, will-change, opacity below 1, or a z-index on anything positioned, each collapse it to a near-black slab, so fading one in destroys it for the length of the fade. z-index is on that list because it collapsed four of the five components below during this build — stacking one glass element above another is the obvious thing to reach for, and it is the thing that breaks it. Never more than a couple per view: blur(20px) is a full-surface GPU pass.

Behaviour & accessibility contract

The panel is a real focusable control: arrow keys move it 12 px (48 with Shift), and the readout is an aria-live="polite" region so a screen-reader user gets the same contrast verdict a sighted one reads. Where backdrop-filter is unsupported the whole layer stack is dropped for an opaque panel — degrading to legible, not to decorative.

Platform anchor

WCAG 2.2 · 1.4.3 contrast minimum — the clause this technique is most often used to break. Apple HIG materials & vibrancy; CSS Backdrops & Filter Effects (backdrop-filter); CSS Compositing 2 (mix-blend-mode, plus-lighter). The design language is Apple’s and public; this implementation of it, and the contrast governance around it, are not derived from any shipped code.

Live — drag the panel, or focus it and use the arrow keys
Style
Wallpaper
White label on the backdrop under the panel Backdrop sampled from a 40×24 grid measured off the wallpaper at build time.
Drag me

Every combination, measured. White text on the composited surface, rendered in Chromium and read back from the pixels — the three recipes against the three zones of this wallpaper. Only three of the nine clear 4.5 : 1, and the two that clear it comfortably are the ones where the wallpaper was already dark. That is the argument for the five components below: the recipe is not what makes the text legible.

backdropStyle 1Style 2Style 3
pale1.251.421.92
mid2.622.924.62
dark3.815.7317.61

Ratios against white. pass clears 4.5 : 1 for body text, large only clears 3 : 1 but not body, fail clears neither. Spelled out as well as coloured, because a table that encoded its verdict in hue alone would fail the clause it is reporting on.

What the copied CSS does not do. Figma’s “copy as CSS” flattens this into one element with background-blend-mode. Rendered over this same wallpaper that is a near-black slab: background-blend-mode blends an element’s own background layers against each other and nothing else, and the effect depends on blending with the page behind it. The layers have to be real children with mix-blend-mode, and the backdrop-filter has to sit on the topmost one — on the container the stack paints opaque over its own blur.

baseline
glass
isolation:
isolate
transform:
translateZ(0)
opacity:
0.999

Four identical panels. The three on the right sit inside an ancestor that forms its own stacking or blending context, which is all it takes.

<!-- the layers are elements, not background layers --> <div class="glass" data-style="1"> <span class="l1"></span><span class="l2"></span> <span class="l3"></span><span class="l4"></span> <span class="label">Drag me</span> </div> .glass { position:relative; border-radius:32px; box-shadow:16px 16px 9px -18px #fff inset, -12px -12px 6px -14px #b3b3b3 inset, 2px 2px 1px -2px #b3b3b3 inset, 0 0 22px 0 rgba(242,242,242,.5) inset } .glass > span { position:absolute; inset:0; border-radius:inherit; pointer-events:none } .l1 { background:#1d1d1d; mix-blend-mode:plus-lighter } /* Style 1 */ .l2 { background:rgba(29,29,29,.20); mix-blend-mode:color-burn } .l3 { background:linear-gradient(180deg,rgba(102,102,102,0) 50%,rgba(102,102,102,.4) 100%); mix-blend-mode:plus-lighter } .l4 { background:linear-gradient(180deg,rgba(102,102,102,.2) 0,rgba(102,102,102,0) 32.69%); mix-blend-mode:plus-lighter; backdrop-filter:blur(20px) } .label { position:relative; z-index:5 } /* Style 2: .l1 mix-blend-mode:screen Style 3: .l1 rgba(29,29,29,.10) overlay + rgba(29,29,29,.30) color-burn + rgba(0,0,0,.10) */ /* no ancestor may isolate or flatten, or the whole stack goes near-black: isolation | transform | filter | will-change | opacity < 1 */

Glassmorphic

Backdrop Contrast Guard

A translucent surface cannot promise a contrast ratio, because it does not own its backdrop. This is the mechanism that lets you ship one anyway: measure what is behind the panel, and when it will not hold the text, put a substrate under the text instead of hoping.

When to use

Any glass surface carrying text over content you do not control — user wallpaper, a photo, a video frame, a map. Also the honest answer to “can we use the glass style here?”: yes, if it is guarded.

When not to

Not a substitute for choosing a legible design — a guard that fires on most of the screen is telling you the surface is wrong for the content, not that it needs a bigger scrim. Do not sample on every animation frame; sample on settle. And do not let the guard flip repeatedly while dragging: a label that strobes between glass and scrim is worse than either.

Behaviour & accessibility contract

The decision and the measured ratio are both exposed as text in a polite live region, so the guard is auditable rather than magic. The scrim is a real opaque layer behind the text, not a lowered text opacity. Threshold is 4.5 : 1 for body text and it is a floor, not a target; hysteresis stops the flip from oscillating on the boundary.

Platform anchor

WCAG 2.2 · 1.4.3 (4.5 : 1 normal, 3 : 1 large) and 1.4.11 non-text contrast; WCAG 2.2 · 4.1.3 status messages for the live verdict. Apple HIG legibility guidance for materials. The ratio is computed with the WCAG relative-luminance formula, not eyeballed.

Live — drag into the pale region with the guard off, then on
Guard
Backdrop under the panel
Guarded

With the guard off, the label over the pale cyan measures about 1.2 : 1 — white on near-white. The guard does not tint the glass or fade the text; it puts an opaque pill behind the words, because that is the only thing that actually changes the ratio.

// sample the backdrop under the panel, decide, expose the decision const L = backdropLuminance(panelRect); // WCAG relative luminance const r = (Math.max(L,1) + 0.05) / (Math.min(L,1) + 0.05); // vs #fff text const need = 4.5; // body text floor const on = r < need + (wasOn ? 0.35 : 0); // hysteresis, so it cannot strobe panel.dataset.scrim = on ? 'on' : 'off'; out.textContent = r.toFixed(2) + ':1 — ' + (on ? 'scrim applied' : 'glass alone is enough'); // the scrim is an opaque layer BEHIND the text. Lowering text opacity // lowers the ratio; it does not raise it.

Glassmorphic

Floating Glass Nav

A capsule tab bar that content scrolls under. The whole point is that you can see the list continue behind it — which is also the whole problem, because what scrolls under it is not yours.

When to use

Primary navigation in a content app where the scroll should read as continuous — feeds, libraries, maps. Best when the content behind it is your own layout and you can keep a predictable tone under the bar.

When not to

Never let it float over content it can hide: the scroller needs bottom padding equal to the bar plus the safe-area inset, or the last row is permanently unreachable. Never over user photos or video without the guard. Not on a form screen — a floating bar over a keyboard-adjacent field is a tap the user did not mean.

Behaviour & accessibility contract

Real <button>s, one carrying aria-current="page"; the active item is marked by an opaque fill and a filled icon, never by tint alone. Targets are 60×44 px. The bar sits above env(safe-area-inset-bottom), and the scroll container reserves room for it rather than letting it overlap the last item.

Platform anchor

Apple HIG tab bars & safe areas; Material 3 navigation bar; WCAG 2.2 · 2.5.8 target size (minimum), 1.4.1 use of colour, 1.4.3 contrast; env(safe-area-inset-*) per CSS Environment Variables.

Live — scroll the list; the bar reserves its own space
Reserved space, not overlap. This list ends above the bar because the container carries padding-bottom: 96px, not because the bar happens to sit lower.
Kishi Bashi · Sonderlust
Nils Frahm · Spaces
Hania Rani · Home
Floating Points · Promises
Jon Hopkins · Singularity
Last row. Scroll to here — it clears the bar. Remove the padding and this row is unreachable forever, on every device with a home indicator.

Recipe 3, for a measured reason. This bar rendered grey on a coloured wallpaper. Sampled against the wallpaper beside it, recipes 1 and 2 keep 14% of the backdrop’s chroma here and recipe 3 keeps 64% — the three are not interchangeable, and which one to use is a measurement, not a preference.

.tabbar { position:absolute; bottom:calc(16px + env(safe-area-inset-bottom)); left:50%; translate:-50% 0; border-radius:999px; overflow:hidden } /* the translate PROPERTY, not a transform: transform forms a stacking context and the glass stops blending. margin-left:-110px worked too but hard-coded half the width, so it sat 10px off-centre */ .scroller{ padding-bottom:96px } /* bar height + gap + safe area */ .tabbar { --veil:rgba(14,14,17,.56) } /* white clears 4.5:1 on this */ .tabbar::after { position:absolute; inset:0; background:var(--veil) } /* floor */ .tabbar button { min-width:60px; min-height:44px; color:#fff } /* 2.5.8 */ .tabbar button[aria-current="page"] { background:rgba(255,255,255,.95); color:#17171a } /* active is an opaque WHITE chip AND a filled icon — never tint alone (1.4.1). A translucent chip is invisible over pale ground, so it is opaque, not merely lighter. */

Glassmorphic

Glass Sheet

A bottom sheet that shows the context it interrupts. The glass is the reason it feels attached to the screen underneath; the scrim is the reason it is still readable and still a modal.

When to use

A short, focused decision that benefits from keeping its context visible — share, sort, a quick confirm. The translucency earns its place when the thing behind is what the decision is about.

When not to

Never without the scrim: on glass alone the sheet stops being modal, and the text sits on whatever happened to be underneath. Not for long forms — if it scrolls, it is a screen. Not for destructive confirms where the wording must be unmissable; that gets an opaque dialog.

Behaviour & accessibility contract

role="dialog" with aria-modal="true"; focus moves to the sheet on open and returns to the trigger on close; Tab is contained while it is open and Escape closes it. The scrim is clickable and does the same thing as Cancel. Body copy sits on its own opaque panel, so the readable part never depends on the backdrop. Reduced motion drops the slide.

Platform anchor

WAI-ARIA APG dialog (modal) pattern; WCAG 2.2 · 2.1.2 no keyboard trap (a modal is the sanctioned exception — Escape must work), 2.4.3 focus order, 1.4.3 contrast, 2.3.3 animation from interactions. Apple HIG sheets; Material 3 bottom sheets.

Live — open it, then press Escape or click the scrim
The screen underneath. The sheet lets you keep seeing it — that is the argument for glass here.
<div class="scrim" data-close></div> <div class="sheet glass" role="dialog" aria-modal="true" aria-labelledby="t" hidden> <div class="grip" aria-hidden="true"></div> <div class="scrimtext"><h4 id="t">…</h4><p>…</p></div> </div> open() { sheet.hidden = false; last = document.activeElement; sheet.querySelector('button').focus() } close() { sheet.hidden = true; last?.focus() } // focus goes home // Escape closes · Tab is contained · the scrim is a Cancel you can click // prefers-reduced-motion: reduce → no slide, just appear

Glassmorphic

Glass Segmented Control

The capsule picker this whole domain is steered with. On glass, the selected segment has to be an opaque fill — a translucent “selected” state is a state you cannot see against a backdrop you do not own.

When to use

Two to four mutually exclusive views of the same content, where switching is cheap and instant — a filter, a density, a chart range. On a glass surface it keeps chrome to one row.

When not to

Never for actions — a segment is a choice, a button does a thing. Not past four segments or with labels that wrap; that is a select. And never signal the selection with opacity or tint on glass: over a pale backdrop a translucent selected pill and an unselected one look identical.

Behaviour & accessibility contract

role="radiogroup" with role="radio" and aria-checked — a radiogroup, not a tablist, because it filters content rather than swapping panels. Arrow keys move the selection, Home and End jump to the ends, and only the selected segment is in the tab order. Selection is carried by an opaque fill and a mark, so it survives greyscale.

Platform anchor

WAI-ARIA APG radio group pattern; WCAG 2.2 · 1.4.1 use of colour, 2.5.8 target size, 2.1.1 keyboard, 4.1.2 name role value. Apple HIG segmented controls; Material 3 segmented buttons.

Live — arrow keys, Home and End
Showing: All The selected segment is an opaque fill, not a lighter glass.

A dark veil, and a metric that was measuring the wrong thing. White labels here measured 1.30 : 1, so the track needs a floor. A white frost was used first, on the grounds that the dark veil keeps only 0.10–0.16 of the backdrop’s luminance span where ink on frost keeps 0.47–0.70. Both numbers are right and the conclusion was wrong: luminance span is not what makes a surface read as glass — hue is. Rendered side by side, the dark veil keeps 140% of the wallpaper’s chroma and 0.40 of its structure where the frost kept 64% and 0.17, and the frost went milky. So the veil is dark, the labels are white, and the selected segment is an opaque white chip — unambiguous against a dark track anywhere on the wallpaper, which is exactly what the cell above asks for.

<div role="radiogroup" aria-label="Filter"> <button role="radio" aria-checked="true" tabindex="0">All</button> <button role="radio" aria-checked="false" tabindex="-1">Albums</button> </div> /* The track carries the floor: white clears 4.5:1 on this over any wallpaper */ .track { --veil:rgba(14,14,17,.56) } .track::after { position:absolute; inset:0; background:var(--veil) } [role="radio"] { color:#fff } /* selected = opaque chip + a mark. Not a lighter glass, not a tint — a translucent pill and an unselected one look identical over pale ground. */ [role="radio"][aria-checked="true"] { background:rgba(255,255,255,.95); color:#17171a } [role="radio"][aria-checked="true"]::after { content:" ✓" } /* 1.4.1 */ // ← → move · Home/End jump · roving tabindex keeps one stop in the tab order

Glassmorphic

Glass Notification

The worst case for this material and the most common use of it: a banner over a wallpaper nobody designed. It cannot know what is behind it, so it carries its own substrate and stops asking.

When to use

Transient, non-blocking feedback over a lock screen, home screen, or full-bleed media — where an opaque bar would feel like a system error and the glass reads as part of the surface.

When not to

Never for anything the user must act on, and never for errors that need a decision — a toast that disappears is not a place to put consequences. Never rely on the glass for legibility: the backdrop is a photograph the user chose. Never auto-dismiss faster than the text can be read, and never at all while it has focus or the pointer is on it.

Behaviour & accessibility contract

A polite live region — announced without stealing focus. Content sits on its own opaque inner panel, so the ratio is a property of the component and not of the wallpaper. Dismiss is a real 44 px button, not a swipe-only gesture; hover, focus, and reduced motion all pause or remove the timer.

Platform anchor

WCAG 2.2 · 4.1.3 status messages, 2.2.1 timing adjustable, 1.4.3 contrast, 2.5.8 target size, 2.1.1 keyboard (a gesture always needs a button beside it). Apple HIG notifications; Material 3 snackbars.

Live — toggle the inner substrate off to see what the wallpaper does
Inner substrate
Backup finished 1,204 photos · 2.1 GB

The stage is parked on the palest part of the wallpaper on purpose. With the inner substrate off, the white text measures 1.42 : 1 — read back from the composited pixels, not estimated — and that is the default state of most glass notifications in the wild.

<div class="toast glass" role="status" aria-live="polite"> <span class="inner">…</span> <!-- opaque; owns the contrast --> </div> .toast .inner { background:rgba(10,10,12,.58); border-radius:12px; padding:10px 12px } /* the ratio is a property of the component, not of the user's wallpaper */ // polite: announced, never focus-stealing (4.1.3) // timer pauses on hover and focus, and is removed under prefers-reduced-motion // dismiss is a 44px button — a swipe is never the only way out (2.1.1, 2.5.8)

Start · Changelog

Changelog

Versioned like software, because it is software. Historical entries are never edited — only appended.

v1.39.0Kindness-First Growth Manifesto, Omnipresent Keyboard Navigation & 3D Deliberation
  • Kindness-First & Anti-Dark Patterns Growth Manifesto. Added the flagship .cds-kindness-banner directly into Overview, formalising four humane design pillars: Streaks with Grace Days (psychological safety & cognitive relief), FTC 1-Click Honest Cancellation (zero retention mazes or dark nudge gymnastics), Transparent Truth in Pricing (full upfront pricing without drip tricks), and Sensorial Calm (reduced-motion compliance and breathing whitespace).
  • Omnipresent Keyboard Shortcuts & Parity. Added instant search focus with ⌘K / Ctrl+K and uncluttered / trigger across desktop and tablet surfaces, paired with instant clear and dismissal via Esc.
  • Verification Contract Regenerated. Synchronised all machine-readable contract layers: 190 component categories, 17 domains, 44 dual-theme WCAG 2.2 AA contrast pairs, and 0 smoke test errors across 146 rendered pages.
v1.38.0The page is now executed, not only parsed
  • Every check in this repository read the page as text. None of them ran it. A handler bound to a selector nothing matches, a demo that reads a property off null, a toggle that works once and breaks on the second press — none of those are visible to a parser, and all of them are visible to a visitor within about four seconds.
  • cds/smoke.mjs executes the page. It parses the document into cds/domshim.mjs — 335 lines of DOM: selector matching, event delegation with bubbling, classList, innerHTML re-parse, and the globals the demos touch — evaluates all ten inline scripts, activates all 201 sidebar entries, and dispatches a click on every one of the 975 interactive controls. Twice each, because a state machine that only works once is the failure a single click never finds.
  • Keyboard parity is asserted, not assumed. This catalogue cites WCAG 2.1.1 in dozens of entries. The test for it is not “is there a keydown listener somewhere” — it is whether what a mouse can do, a keyboard can also do. So every widget role sitting on a div is clicked, snapshotted, reset, and given Enter and Space; anything that moves under a mouse and not under a keyboard is reported by name, and a widget role with no tabindex is reported as unreachable.
  • Result on this build: 0 runtime errors, 0 keyboard-parity failures across 975 controls and 13 non-native widgets. That is the first time this page has been able to say that about its own behaviour rather than about its own markup.
  • Both guards were negative-tested by breaking them. One character changed in a handler’s root selector — caught, named to the inline script. One tabindex removed from a spatial card — caught, named to the section and the element. There is no browser and no network on the machine that runs this, which is the constraint the harness was written to, not a limitation it works around.
v1.37.0The page audited itself and lost on five counts
  • The contrast table was stale, and the sentence above it said it could not be. The preamble claims every ratio is computed from tokens.json and re-asserted by cds/verify.mjs. It was not: the JSON was recomputed and checked, the rendered table was not, and eight of its eighteen printed cells were pre-v1.18.0 values. Every stale figure was lower than the truth, so the page under-sold its own tokens — a smaller error than over-claiming and exactly the same failure of the claim. The table is now emitted from tokens.json by cds/emit-contrast-table.mjs, shows all 44 pairs per theme rather than nine, and every cell is bound to the contract in both directions.
  • Twelve motion tokens were documented and did not exist. The Motion section published twelve “canonical” primitives with MCP payloads labelled as served from cds/tokens.json. None of the twelve was in that file. None was in the page’s own CSS either — the demos hardcoded the same curve values as literals, in the one section arguing that a value belongs to a token. They are now declared, consumed through var(), captured by the generator, and asserted: a token the page advertises must be shipped, and a token that is shipped must be used by something.
  • The generator was reading one :root block out of eight. The page declares tokens in more than one place, and the token capture only ever saw the first. Eleven live tokens — the five motion roles with their eases, and --csp-floor — never reached the contract, while the page named --csp-floor as one of three tokens a re-brand may not repoint. An agent could not enforce a rule about a token it never received. The count moved from 42 to 65 per theme; none of the new ones is new work, all of them were already on the page.
  • Twenty citations were wrong. Found by checking every anchor against its primary source rather than re-reading them. The most consequential: this page cited the FTC “click-to-cancel” rule as binding in two places, and the Eighth Circuit vacated it in full on 8 July 2025, days before its compliance date — what actually governs is ROSCA. Also: 465.4 is buying reviews, suppression is 465.7; GDPR Art 6 is lawfulness, purpose limitation is Art 5(1)(b); the 2.2.2 exception is five seconds, not a cycle count; aria-grabbed was removed from ARIA 1.2 and is implemented by nothing; data tables, banners and steppers are Material 2 components that Material 3 never documented.
  • The page failed three criteria it cites in dozens of entries. An aria-controls pointing at an id nothing carried — inside the tree-table demo whose own cell reads “expand state is announced, not just drawn”; four controls whose only name was a placeholder; an h2 → h4 skip. All fixed, and cds/verify.mjs now checks every IDREF resolves, every control has a name that is not a placeholder, and no heading level skips.
  • An anchor is now required to cite something. Sixty-nine anchors named a body with no rule behind it — “community-safety conventions” — under the same heading as the ones naming GDPR. Rather than add a softer label, which would let the soft ones keep borrowing the authority of the hard ones, the generator extracts what each anchor actually cites, publishes it as anchorCites, and refuses to build an anchor that cites nothing.
  • Fifty-four raw hex values were painting the page. Forty-five of them in the glass and media clusters — the sections whose whole argument is that a value belongs to a token. The exemption is real (a material that follows --bg stops being a material); what was missing was a boundary. Thirteen compositing constants are now named — --on-media, --media-*, --glass-rim, --verdict-* — and the exemption ends there: zero opaque hex in a paint property outside the token layer, asserted. rgba() survives, because a scrim over arbitrary content is an alpha value rather than a colour choice.
  • Assertions 3,038 → 4,054. Every new guard was negative-tested by breaking the thing it protects: a reverted contrast cell, a dropped token, an advertised token nothing uses, a dangling IDREF, a removed label, a heading skip, an anchor stripped of its citation, a re-introduced hex, a stale entry count in both directions. Nine breaks, nine failures, all caught.
v1.36.0The offline tier — the same contract at five sizes
  • v1.35.0 made this contract worse for every model that runs on someone’s own hardware. Putting the full decision register on all 190 entries took the index from 172 KB to 296 KB — about 76K tokens, which is the entire window of most locally-run models. A better contract that a local model cannot open is not a better contract.
  • Four smaller tiers, written by the same generator run. cds/RULES.txt (4.2 KB — the protocol, all seven hard rules, all seven bundles, the non-negotiables, small enough to paste as a system prompt); cds/tokens.json (19.3 KB, unchanged); cds/domains/<domain>.json17 shards, 6.1 to 44.8 KB, the full register for one domain only; and cds/index.slim.json (54.1 KB) for choosing rather than building. Rules + tokens + one shard is 7.6K tokens at the smallest domain and 17.5K at the largest, and it is complete — nothing is fetched at build time.
  • Nothing here is an API. No server, no key, no schema compiler, no embedding index. Fetch once, commit the files beside your code, and the contract — including node cds/verify.mjs — keeps working with the network off.
  • The tiers are asserted against each other, not trusted. cds/verify.mjs now requires the 17 shards to hold 190 components between them, the slim index to list the same ids in the same order and at the same version, every shard entry to carry a full register, and the rules card to contain every hard rule by id. The first version of that last check passed a card with click-to-cancel deleted, because the phrase also appears inside a bundle reason — the negative test caught it and the check now matches [id]. Assertions 3,024 → 3,035.
v1.35.0The machine layer carries what the page calls binding
  • The when-not-to cell is now in the contract, not only on the page. This section has said for six versions that the when-not-to cell is binding. It was binding for a human reading the register and unenforceable for an agent reading JSON, because components.json carried only id, name, domain, summary, anchor and URL. Every entry now ships its whole decision register — whenToUse, whenNot, a11y, anchor — extracted by the generator from the page’s own cells, never hand-written, and the generator exits non-zero if a section is missing one. 190 of 190 carry a full register; the index is 172 KB → 295 KB and an agent no longer has to parse HTML to build compliantly.
  • The MCP surface is reproducible instead of asserted. components.json gained an mcp block with toolsToFileMapping — which slice of which file each tool reads — plus worksWithoutAServer, because every tool here is a way of slicing two static files. It also states plainly that there is no separate cds-mcp package and why one has not been written. A new fetchGuidance block tells a fetching agent the cheap path, that whenNot is a refusal condition rather than advice, and that if this file and the page disagree the page wins.
  • Re-branding is now a documented, provable operation. A new Make it yours block names the eight tokens that actually carry brand against the thirty-four that carry structure, and shows the loop that makes it safe: the generator reads the tokens back out of the page, recomputes all 44 contrast pairs per theme from your hex values, and refuses to write the file if any drops below 4.5:1. It also names the three tokens a re-brand may not repoint — --red, --green and the --csp-floor opacity — because they carry meaning that components’ when-not-to cells depend on.
  • Two places still said “142 entries”. Again. The v1.32.0 entry below records fixing exactly this, and it came back, because the number moves every release and nothing referred back to it. cds/verify.mjs now scans live prose for any entry count above the contract count — excluding this changelog, whose historical numbers are correct as written — and hard-pins the two sentences that speak for the whole index. Assertions 3,019 → 3,024.
v1.34.0Mobile · Device Layer — 180 → 190 · the conditions the hardware imposes
  • A new domain for the layer below the interface. The catalogue already carried mobile-shaped patterns — pull to refresh, bottom sheets, tab bars. What it did not carry was the set of conditions a phone imposes on any of them: a screen that is not the rectangle you were given, a keyboard that takes a third of it, a process the system can end without warning, a battery mode that revokes autoplay. New domain mobile: safe area and insets, keyboard avoidance, edge-swipe back, thumb reach, Dynamic Type, lifecycle and state restoration, low power and data saver, orientation and foldables, live activity, deep link and cold start.
  • Every entry demonstrates the failure, not only the fix. Each demo has a control that turns the contract off — ignore the insets and Compose sits under the home indicator, drop avoidance and the composer hides behind 132px of keyboard, disable restoration and a cold start returns an empty step 1. A specimen that only ever shows the correct state does not teach anything; the argument for the rule is what happens without it.
  • Anchored to platform rules, as the rest of the catalogue is. The ten platform anchors cite Apple HIG (layout, onscreen keyboards, gestures, Dynamic Type, Live Activities, Universal Links), Material 3 and Android (window insets, predictive back, window size classes, App Links, battery saver), W3C drafts actually in use (CSS Environment Variables, VisualViewport, Device Posture, Page Visibility, Network Information) and eleven distinct WCAG 2.2 success criteria — including 2.4.11 focus not obscured and 2.5.7 dragging movements, both of which exist because of exactly these conditions.
  • Contract v1.34.0 — 190 entries across 17 domains, every one anchored. cds/components.json and cds/tokens.json were regenerated from the page rather than hand-edited; the self-referential assertion count moved 2,908 → 3,019 as the new sections brought their own checks with them.
v1.33.0Glassmorphic · Liquid — 174 → 180 · a recipe, measured
  • Six components built to a platform material language. The three surface treatments follow Apple’s Liquid Glass logic (iOS 26), the same way the rest of this catalogue is built to Apple HIG and Material 3; the layer values were cross-checked against a public Figma Community reference rather than guessed at. The implementation is not that reference — applied as published it does not render the effect at all, so the surface was rebuilt from the blend model up. New domain glass: draggable surface comparison, contrast guard, floating tab bar, bottom sheet, segmented control, notification.
  • The copied CSS does not reproduce the design. Figma’s “copy as CSS” flattens the stack into one element with background-blend-mode. Rendered over the same wallpaper in Chromium that is a near-black slab, because background-blend-mode composites an element’s own background layers against each other and nothing else. The effect needs mix-blend-mode on real children, and the backdrop-filter has to sit on the topmost blended layer — on the container the stack paints over the blur and the blur is never seen. All four arrangements were rendered and compared before one was chosen.
  • Nine measurements instead of an adjective. White text on the composited surface, read back from the pixels: three recipes × three zones of the wallpaper. Three of the nine clear 4.5 : 1, and the two that clear it comfortably are the ones where the wallpaper was already dark. The matrix is on the page, with the verdict spelled out as well as coloured.
  • The floor is a dark veil, and the metric that argued against it was measuring the wrong thing. Words on glass need a floor: white text needs 4.5 : 1, the lightest region of these wallpapers reads 1.11 : 1 for white, so the composite has to land at or below 119/255 — a near-black veil at alpha 0.56. A white frost was built first instead, because the dark veil keeps only 0.10–0.16 of the backdrop’s luminance span where ink on a 0.46 frost keeps 0.47–0.70. Both figures are correct; the conclusion was not. Luminance span is not what makes a surface read as glass — hue is. Rendered side by side over the same wallpapers the dark veil keeps 140% of the wallpaper’s chroma and 0.40 of its structure, against 64% and 0.17 for the frost, which went milky and read as grey plastic. Adding saturate() to the backdrop-filter moved chroma retention by 1–4 points, because the veil sits above the blur and is the thing that desaturates. So: --cgl-veil with white labels, --cgl-scrim where a dark substrate is the point, and an opaque white chip for the selected item — unambiguous against a dark track anywhere on the wallpaper, which a translucent pill can never be.
  • The three recipes are not interchangeable, and two stages were parked on flat ground. The floating bar rendered grey on a coloured wallpaper: sampled against the wallpaper beside it, recipes 1 and 2 keep 14% of its chroma there and recipe 3 keeps 64%, so the bar uses recipe 3. Separately, the segmented control was sitting on a patch with a luminance spread of 0.035 when 0.28 was available a few percent away — a blur of a smooth gradient is a smooth gradient, and glass needs an edge behind it to be visible at all. Both stages moved, and the segmented one lost a third of its height: 874×382 around a 232×36 control was 97% empty photograph.
  • The control moved under the pointer. The selection mark was rendered only on the selected segment, which made it ~14px wider than its neighbours — measured three different segment layouts for three selections, so the thing you were about to click shifted as you arrowed through. The mark now reserves its space on every segment and is revealed rather than inserted: one layout, always. The segments are also equal width now (they were 62 / 92 / 90 — three different target sizes for three equivalent choices), the track has 4px of padding so the selected chip sits inside it rather than flush against its edge, and the focus ring moved inside the chip in the label’s own colour: at the default outline-offset: 2px it exactly filled the track’s gutter and collided with the track edge. Because the label colour is already solved against what is behind it, the ring is guaranteed visible on both the ink-on-white chip and the white-on-dark segments.
  • The recipe does not scale. Its inset rim is specified in absolute pixels — 16px, 12px and 2px offsets with a 22px inner glow — and was authored for a 32px-radius panel. On a 44px-tall pill those offsets span the whole element. Small surfaces carry the same rim re-specified at roughly a third.
  • Centring by hard-coded width. Avoiding the stacking-context trap, the bar was centred with margin-left:-110px — half of a width that is actually 200px, so it sat 10px off-centre. The individual translate property does not form a stacking context where transform does, so it centres itself now and measures 0.0px off.
  • Four of the six broke the way the domain says it breaks. A z-index on a positioned element forms a stacking context, which isolates the blend and collapses the surface. The tab bar, sheet, segmented control and notification were all stacked that way first and all four rendered as slabs. They stack by DOM order now, and the tab bar is centred with margin-left because transform forms a context where the individual translate property does not. cds/verify.mjs now asserts this instead of trusting it: both guards were tested by reintroducing the defect and watching them fail.
  • Five tokens were borrowed from the wrong design system. --gold, --r-xs, --text-xs, --weight-semibold and --weight-bold exist on the institutional showcase, not here, so every declaration using them was being dropped and falling back to inherited values. Translated to this page’s vocabulary (--amber, the *-soft tints, literal sizes and weights).
  • Two ARIA faults the markup argued with itself about. The bottom bar carried role="tablist" while its active item carried aria-current="page" — tab vocabulary and navigation vocabulary in one element, and tablist forbids plain buttons. It is role="navigation". Its scroller had also reused an aria-label that an existing component already owned, leaving two region landmarks indistinguishable. axe-core reports 0 violations in both themes.
  • Contract cds/components.json180 components across 16 domains, every one carrying a platform anchor. The self-test runs 2,908 assertions.
v1.32.0Audit · 178 → 174 · the self-test was the hole
  • The count was wrong, so the count changed. Four pairs of entries were the same component written twice — and two of those pairs shared an element id, so sec-datepicker and sec-upload each existed on the page twice and the second sidebar row for each silently scrolled to the first one. The catalogue holds 174 distinct categories, not 178. Date & Time Picker, Upload & Camera, Multi-Step Form and Coach Mark each absorbed the better half of their twin rather than discarding it: all four now carry two previews — a typed field and a range grid, a single upload and a per-file queue, a dot rail and a named-step list, an anchored tip and a floating-action tip — and the unique rules from both registers. Nothing was deleted except the duplication.
  • “Generated from the page so it can never drift” had no generator behind it. It was a description of intent, and the index had drifted: two duplicate ids, a count four too high, and 24 entry summaries that no longer matched the prose on the page. cds/generate.mjs now derives components.json and tokens.json from the page's own sidebar and :root blocks, refuses to write if an id repeats or a bundle points at nothing, and answers --check in CI. The sentence is now executable.
  • The contrast table was measuring the easy surfaces. It held nine pairs per theme and every one sat on --bg or --surface. The page paints text on --surface2, --surface3 and five tinted *-soft panels constantly, and there eight tokens sat between 3.22:1 and 4.44:1 — an axe-core pass over all 174 sections in both themes returned 1,506 contrast failures against a page that prints “AA” as a headline statistic. Nine tokens were re-solved at constant hue and saturation until every surface cleared 4.5:1, a --red-fg token was added because white on the dark theme's red measured 2.82:1, and the table went from 9 pairs per theme to 44 — every pair the page actually renders. The tokens moved; the claim did not.
  • Zero axe-core violations, both themes, all 174 sections — including best-practice rules. Calendar grids now carry the row and column-header structure role="grid" requires (via display:contents, so the semantics cost nothing visually); aria-expanded moved off a table row and onto the disclosure button that owns it; links inside prose are underlined rather than signalled by hue alone; the star-rating targets are declared at 28 px instead of inheriting whatever an icon font happened to load at.
  • 25 entries promised copyable code and did not carry any. They had a live preview and no code block at all, against a page — and a JSON-LD abstract — that says every entry carries one. All 25 now do, each with the rule the markup exists to enforce.
  • The self-test passed 1,180 assertions over every one of those defects. It read section ids into a Set, which deduplicates precisely the fault it was looking for. cds/verify.mjs now runs 2,656 assertions and asserts on raw lists: id uniqueness, sidebar uniqueness, contract → sidebar reachability, four decision cells and a code block per entry, one version string across the header and every JSON-LD block, and every size claim in prose, meta and the no-JavaScript hero fallbacks against the computed count. Run against v1.31.0 it reports 82 failures. Every assertion in it exists because a real defect got past the previous version.
  • Also: the hero read “fourteen domains” beside a stat tile reading 15; the no-JavaScript fallbacks — what a crawler without a JS engine actually reads — said 170 / 110 / 12; two JSON-LD blocks declared v1.30.0; two places still said “142 entries”; the Agent Library section had no sidebar row and was reachable only by typing its hash; and one block styled dark mode with @media (prefers-color-scheme: dark), so it ignored this page's own theme toggle.
v1.31.0Spec Parity · evidence from outside this page
  • A section whose evidence is not self-issued. Everything else here is a system checking itself. takes one Button out of a production component library and reads it twice — once from the Figma inspector, once from the machine-readable specification the shipped implementation was serialised into — on five numbers that either match or do not: height 60, horizontal padding 24, vertical padding 12, gap 8, radius derived as height/2. Width is excluded, in writing, because the frame hugs its content and the specification states none.
  • The enums are the same object twice. Three Figma variant dropdowns are the prop value lists member-for-member. The fourth, state, deliberately is not — code splits it into independent disabled and isLoading props — and that translation is documented rather than smoothed over. Drift is an undocumented difference, not a difference.
  • Workflow stated as workflow. The four-step loop (Figma variant set → written rules → serialised specification → agent-prepared updates over Figma MCP that merge only after a human authorizes) carries no cycle-time figure and no percentage, because none has been measured. The section also names what it does not prove: the specification is derived from shipped code rather than authored ahead of it, and 15 of its 53 entries carry hard pixel specifications.
  • Contract v1.31.0 — component count unchanged at 178; the new section is provenance, not a catalogue entry. cds/verify.mjs now runs 1,180 assertions.
v1.30.0New domain · Spatial & Motion · 170 → 178
  • A fifteenth domain, and the first one about time rather than about a widget. Eight entries — Motion Role Tokens, Focus Zoom, Attenuated Context, Anchored Control, Draw to Create, Elevation on Commit, Shortcut-Teaching Menu, Stack Gather — covering direct manipulation and the motion that makes it legible. Every one carries the same four-cell register as the rest of the catalogue, and every one is anchored: WCAG 2.2 §2.5.7 Dragging Movements and §2.1.4 Character Key Shortcuts do most of the work here, alongside §1.4.13, §2.4.11 and §2.3.3.
  • Motion typed by role, not by size. The existing --dur-1/2/3 scale answers “how big is this element”. Five new tokens answer “what is this movement for” — Navigation, Ambient, Reveal, Commit, Settle — and every one collapses to 1 ms under prefers-reduced-motion at the token, so no component can forget. The two scales coexist; the older one was never wrong, it was answering a different question.
  • Two rules the domain refuses to bend. Attenuated content is inert and aria-hidden for as long as it is attenuated — visually present is not programmatically present. And no creation, reorder or grouping gesture ships as a drag alone; the single-pointer path sits beside it at equal visual weight, because a secondary accessible path is a path a team eventually stops maintaining.
  • Contract v1.30.0 — 178 entries across 15 domains, every one anchored. cds/verify.mjs passes unchanged.
v1.29.0Skills Kit → 119 · on-prem agent infra
  • Two local-model agent methods. A Local Tool-Use Router (ReAct + a validated API interceptor so an open-weight model can trigger Python, DB queries and webhooks — allowlisted, schema-checked, human-gated on the irreversible) and a Dynamic LoRA Router (intent-classified hot-swap of task-specific adapters with a confidence threshold and a base-model fallback). The Skills Kit is now 119 methods.
  • Honest by construction. The router “proposes; the harness disposes” — irreversible tool calls stop for a human; and the LoRA method ships an adapter only if it beats the base model, reports measured switch latency rather than claiming “instant,” and frames adapters as task-shaped depth, not a claim of frontier general intelligence.
v1.28.0Skills Kit → 117 · corporate law & engineering
  • Eleven corporate-legal & engineering methods. Six for Business & Strategy — commercial & technology contracts, IP strategy & licensing, secured transactions & creditors’ rights, antitrust & competition law, a corporate compliance & ESG program, and corporate governance & M&A defense; plus five for Product Tools — DevOps & CI/CD, formal methods & QA, requirements engineering & scoping, distributed-systems architecture, and AI for code. The Skills Kit is now 117 methods across six categories.
  • Deduped, and honest about the line. Thirteen requested topics consolidated to eleven (contract-drafting merged with contracting-for-technologists; two IP topics into one). Every legal method states plainly that it is not legal advice and routes the determination to counsel; per se antitrust conduct is refused, not structured around; and the engineering methods keep the “model produces, the human governs” discipline with a human on every irreversible step.
v1.27.0Skills Kit → 106 · quantitative marketing
  • Eight marketing & market-expansion methods. A quantitative-growth cluster joins the kit — Python data science, statistical modeling, ML for business, and NLP/sentiment (Product Tools); plus marketing analytics science (CLV, attribution, churn, dynamic pricing), behavioral economics, brand positioning, and market research & intelligence (Business & Strategy). The Skills Kit is now 106 methods across six categories.
  • Deduped by construction. Eleven requested topics consolidated to eight distinct skills: predictive-analytics folded into ML + marketing analytics, customer-centricity into market research, and go-to-market was already covered by the existing GTM skill. Each new method keeps the kit’s honesty contract — a “what this is NOT” boundary, estimates labelled, and manipulation, covert data, and unsubstantiated claims routed to their guardrails.
v1.26.0AI-Ready Export · deeper, self-updating
  • The export keeps up with the kit by itself. With the Skills Kit now at 98 methods across six categories, the AI-Ready panel’s counts are read live from the contract: the skill-method picker shows a per-category count on every chip (and a running total on All), and the “what’s inside” manifest and its caption inject their numbers from the same source — so they can never drift stale again the way the old hard-coded “39” did.
  • Contract drift caught and fixed. components.json still declared 160 categories while its own array held 170 (the data & tables and product & metrics domains had been added without updating the count); the domain list was missing two entries and the anchored-entry count lagged. All three are regenerated from the array — 170 categories, 14 domains, 170 carrying a platform anchor — so the machine layer matches the page.
v1.25.0Skills Kit · 93 → 98 · people & org
  • Five skills for team scaling, org health, and a legacy-code audit. Employer branding & talent engineering (competitor talent flow from public signals + gameable-resistant interviews), cross-border workforce cost optimization (fully-loaded cost across two scales and geographies with hire ROI), org-health & retention guard, and a cultural-friction & retention guard join Business & Strategy; a long-context legacy codebase audit (a local model ingests 100K–300K tokens to find zombie code, breaking-upgrade risk, and design debt, proposing low-blast-radius refactor PRs) joins Product Tools.
  • The people skills carry the kit’s strongest privacy framing: attrition and burnout signals are aggregate, consented, and team-level — never covert surveillance of named individuals; per-person monitoring and scoring raise privacy, consent, and legal issues and route to HR and counsel; market comp is a public-source estimate, not a rival’s actuals; talent-flow reads use public signals only; and the codebase audit auto-applies nothing — every refactor is a reviewed, tested PR merged by a human. Component count unchanged at 160.
v1.24.0Skills Kit · 75 → 93 · product, growth & strategy
  • Eighteen skills for the full product loop. Product-conflict analysis & PRD writing, backend-data growth with GEO/SEO, a blog copy AI-tone humanizer, GitHub OSS discovery with a license audit, analytics event taxonomy, agile Epic & BDD tickets, and an autonomous A/B-testing engine join Product Tools; competitive analysis, global GTM, R&D cost accounting, high-net-worth client relationship (private-banking lens), game-theory counter-strategy, crisis-PR war room, and a consultant-grade MECE/7S strategy blueprint join Business & Strategy; user-journey friction mapping, an ethical dark-pattern audit, a WCAG 2.1 AA & i18n guard, and design-tokens & component-state expansion join Research & Craft.
  • Same honesty rules: not affiliated with or endorsed by any named firm (UBS, McKinsey) — their frameworks are a reference lens, not their brand; competitor DAU/MAU and download figures are labelled public or inferred estimates, never a rival’s actuals; SEO/GEO carries no ranking guarantee and no black-hat tactics; behavioral tagging is consented and privacy-respecting; and every legal, financial, security, and compliance decision routes to a qualified professional. Component count unchanged at 160.
v1.23.0Skills Kit · flagship
  • Organizational Memory Architect — the kit’s flagship method. Decision archaeology for a company: the agent reads what the org already has (tickets, epics, reports, project chat, version history, UAT and release records) and reconstructs the decisions and constraints — every claim cited to its source, every rationale labelled documented or inferred, veterans confirm before inference hardens into history. Output is role-scoped: PM, engineering and design briefs plus onboarding packs calibrated to the newcomer’s position and background. The method opens at the security gate (InfoSec scope and exclusions before the first fetch) and closes with a maintenance cadence — because a snapshot of institutional memory starts rotting the day it ships. The organizational generalisation of the single-designer onboarding protocol documented in the institutional showcase.
  • The head-level skills ItemList was regenerated from the contract — it had drifted to 53 while the kit grew to 75. Caught, fixed, and the drift is recorded here rather than papered over.
v1.22.0Product & Metrics · 160 → 170
  • A fourteenth domain: Product & Metrics. Five metric surfaces — DAU/MAU KPI cards that name their comparison window, a Stickiness Ratio that prints its definition of "active" (change the definition and the same product tells a different story — the definition is the metric), a retention cohort grid where unknown cells are em dashes rather than estimates, an activation funnel whose percentage base is part of the claim, and an annotated metric chart where the list of causes is the accessible record. Five roadmap surfaces — Now/Next/Later with no dates on Later, a confidence timeline (solid = committed, hatched = planned, outline = exploring — pattern + word, never colour alone), an append-only public changelog with rollback entries, a feature-request board where declining honestly beats ghosting, and a staged-rollout status whose opt-out survives every expansion.
  • All ten carry live deterministic demos. Contract v1.17.0 — 170 entries, every one anchored.
v1.21.0Skills Kit · 69 → 74 · design & agency health
  • Five design-industry health diagnostics. Two design/UX maturity reads join Research & Craft: InVision-lens design-maturity assessment (is design decoration or business strategy?) and NN/g-lens UX-maturity assessment (research process, design-system upkeep, quality control). Three agency-business health reads join Business & Strategy: Promethean-lens agency operations (time-tracking data, scope-creep, pricing structure), AMI-lens operational leverage (billable utilization against the ~60–70% range, overhead ratio, margin per biller), and Bureau of Digital-lens peer benchmarking (compensation, rates, margins against real peers).
  • Same honesty rules: not affiliated with or endorsed by any of these organizations, their published frameworks used as a reference lens and proprietary benchmark data not reproduced; utilization and benchmark figures cited as labelled industry rules of thumb, never precise facts about a specific studio; nothing audited, and compensation, pricing, and people decisions routed to the owner and qualified advisors.
  • Component count is unchanged at 160 — skills are a meta layer.
v1.20.0Skills Kit · 63 → 69 · signature enterprise-health diagnostics
  • Six signature enterprise-health diagnostics. Each firm’s best-known organizational-health lens, distinct from its existing skill: McKinsey organizational health (OHI & 7S — alignment, execution, renewal), Bain customer & founder’s-mentality health (NPS plus front-line vitality, catching “big-company disease”), PwC / Strategy& capabilities-driven strategy & Fit for Growth (does spend match the capabilities the strategy needs), Deloitte technology & operations health via process mining (bottlenecks and cash leaks in ERP and supply chain), EY transformation & resilience health (readiness for geopolitical, tech, and regulatory shock), and KPMG risk & compliance controls health (three-lines-of-defense control maturity).
  • Same honesty rules as the rest of Business & Strategy: not affiliated with or endorsed by any firm, the publicly understood framework used as a reference lens and proprietary instruments (the actual OHI questionnaire, benchmark databases) not reproduced; every score labelled an informed estimate; nothing audited or certified; and legal, compliance, cyber, HR, and financial judgments routed to qualified specialists and counsel.
  • All six join the Business & Strategy category in the filter and the AI-Ready Export. Component count is unchanged at 160 — skills are a meta layer.
v1.19.0Skills Kit · 53 → 63 · new Business & Strategy category
  • Ten business-analysis skills, and a sixth category to hold them. Enterprise finance, operations, risk, and market methods written in the analytical lens each firm is known for — McKinsey operations & value drivers, Deloitte enterprise health score (syncing spend, people, market, and product vitality into one transparent score), PwC P&L & EBITDA review, KPMG multi-lens risk, EY revenue forecast & FP&A, Moody’s financial-health & credit-risk grading, S&P market position & share, and Coupa enterprise spend analysis — plus two firm-agnostic methods: AI displacement risk and a product-logic adversarial review (steelman, red-team, data audit, pre-mortem).
  • Honesty is load-bearing here: every firm skill states it is not affiliated with or endorsed by the firm and uses the publicly understood analytical discipline as a reference lens, not their brand or proprietary methodology; every figure is labelled measured or estimated; nothing is audited, and financial, tax, legal, or investment decisions route to licensed professionals. The Moody’s method issues an internal analytical opinion, explicitly never a real credit rating.
  • New sixth filter category Business & Strategy in both the filter and the AI-Ready Export. Component count is unchanged at 160 — skills are a meta layer.
v1.18.0Data & Tables · 150 → 160
  • A thirteenth domain: Data & Tables. Ten entries for the surfaces where consumer products handle structure — Tree View (ARIA tree, four-arrow keyboard walk), Selection Tree (honest tri-state parents), Tree Table (rollups computed from children so parent and breakdown can never disagree), Editable Cells (Enter commits, Escape restores, errors explain in place), Table Pagination (exact ranges, honestly disabled ends), Column Manager (hide ink, never data), CSV Export (GDPR Art. 20 portability — the click states its row count), Table States (four truths, one frame, zero CLS), Sparkline Cells (an honest baseline by default — the cropped axis is a labelled toggle, because a chart is a claim), and Comparison Table (sticky column, glyph + word marks that survive grayscale).
  • All ten are live interactive demos, delegated vanilla JS, dual-theme by token, reduced-motion aware. The contract (cds/components.json v1.13.0) grew to 160 entries — every one carrying an anchor.
  • The contract is now self-testing. cds/verify.mjs (zero-dependency, Node 18+) runs 849 assertions — page ↔ index parity, anchors, bundles, skills, agent protocol — locally or against the live site. And the WCAG citations now carry their own evidence: an 18-measurement token-contrast table (both themes) lives in tokens.json and on the page. Its first run failed — light hint text measured 2.49:1 — so the tokens were fixed, not the claim.
v1.17.0Skills Kit · 48 → 53
  • Five skills for bots, product, data and digital wallets. Telegram and Discord bot development — from surface design to intents, rate limits, localization and a deployment architected for growth; a Notion thinking assistant that structures reasoning and drafts while the human keeps the decision; a Digital Wallet architecture method; and Datadog observability that a person or agent can operate on the team’s behalf.
  • The wallet skill ships with the strongest honesty framing in the kit: it is a design-and-architecture method with security humility, not a security guarantee — no rolling your own crypto, no claim of what-you-see-is-what-you-sign on a compromised host, and a professional audit and counsel required before real value moves. The bot and Datadog skills keep secrets out of code and gate destructive or billing-impacting actions behind a human.
  • Both the filter and the AI-Ready Export pick them up: the two bots join Agents & Coding, Notion / wallets / Datadog join Product Tools. Component count is unchanged at 150 — skills are a meta layer.
v1.16.0Skills Kit · 39 → 48
  • Nine skills for product, design, backend and data. A SEO & GEO self-audit method (drawn from strengthening this site’s own machine layer: fold out your own traffic, split bots from humans, then lift both search ranking and AI-answer discoverability); three design-system adoption methods that decide fit before adopting and then keep the whole system on-rails without drift — Material Design, Ant Design, Apple HIG; and five product/backend/data tools — Google Analytics (GA4), Trello, Confluence, MySQL, and Databricks.
  • Same discipline as the rest of the kit: each is a bounded working method, not official docs and not affiliated with any vendor, with every irreversible action (a production DROP, a destructive data op) and every privacy or regulated decision routed through a human. The backend and data skills make the backup-first, human-gated rule explicit.
  • Both the category filter and the AI-Ready Export pick them up: the three design-system methods join Research & Craft, the six tool methods join Product Tools. Component count is unchanged at 150 — skills are a meta layer.
v1.15.0AI-Ready Export · deeper
  • The Design.md pack now carries the skill methods. A skill-method picker (All / None / five categories) compiles the matching skills from the live components.json.skills index into the pack — name, description and downloadable .md URL, grouped by category. The export used to ship 150 components but ignored the 39 skills; now the pack is the whole toolbox, not half of it.
  • Three more target tools: Codex (AGENTS.md + ~/.codex/config.toml), Antigravity IDE (workspace rules + MCP), and Opencode (AGENTS.md + opencode.json) — each with an accurate setup snippet for the eds MCP server. Seven targets in all.
  • A "what the pack contains" manifest makes the depth legible at a glance, and the compile stat now reports components and skills separately. Still deterministic, still client-side, still no model.
v1.14.0Skills Kit · search + sort · 33 → 39
  • Search & sort on the Skills Kit. A search box filters the 39 skills by tool, task or domain; a sort control offers featured order (restored from the original DOM order, captured once), A–Z, and Z–A; a live count and an empty state complete it. Document-delegated, keyboard-accessible, token-themed for both modes.
  • Six new skills. NotebookLM Source Curation (assemble authority-scored primary sources for a domain before grounding), NVIDIA CUDA GPU Optimization (size rendering + VRAM to the real hardware, verified fully on-GPU), Graphic Design Award Research (verify winners against the official archive; dissect the design logic that won), Awwwards Site Analysis and FWA Award Analysis (design vs build difficulty rated separately ★1–5, inferred stack flagged as inference, 3–5 competitor sites anchored to your project), and Design System Analysis — Ed Chen's objective lens: no hype, no hate, facts and judgments labelled separately, anchored to the user's actual goal.
  • Same honesty spine as the rest of the kit: working methods, not official docs, not affiliated; award and difficulty ratings labelled as estimates; every winner verified against the primary source; no cloning of awarded work. Skills remain a meta layer — the component count stays 150; components.json ships all 39 in its skills index.
v1.13.0Skills Kit · 24 → 33
  • Nine tool-specific skills covering the full product-development loop, one per tool the agents already run: AWS (IaC + least-privilege IAM + cost guardrails + gated destroys), Firebase (security rules first, Emulator-tested, App Check, cost-aware), GitHub (PR-first, CI as the merge gate, protected main, SHA-pinned Actions), Google Jules (async-agent delegation + a QA-automation planner that turns edge cases into a committed test suite), Jira (acceptance criteria, capacity-honest sprints, human-owned status), Slack (specific alerts, self-assembling incident channels, in-channel approvals gated on a human click), VS Code (an agent cockpit with MCP wired to a real contract), Cloudflare (edge deploy with previews, tested WAF, DNS changes gated because they go global in seconds), and Perplexity (cited research you follow to the primary source and verify before shipping).
  • Each squeezes the tool for maximum leverage and keeps the same spine as the rest of the kit: a working method, not official docs, not affiliated — and every irreversible action (deploy, delete, DNS, IAM, prod data, money) and every regulated domain routes through a human, the way the AI & Agents domain already argues.
  • Skills are a meta layer, not catalogue entries — the component count stays 150.
v1.12.0Skills Kit · 16 → 24
  • Eight tool-specific skills, one per AI tool in the strip above. Two built to request: an n8n method that turns the automation tool into a standing test harness (scheduled smoke tests, schema assertions, loud alerts, workflow JSON in version control), and an Ollama method that deploys via Docker and recommends the model your GPU can actually run — a VRAM-first sizing heuristic (params × quant + KV overhead) with a pick per memory tier. Then one each for Hermes Agent (open-weight reasoning core, typed tools, human-gated loop), OpenClaw (computer-use agent operated safely — allowlists, no credentials, injection-aware), Monica (a trustworthy general-assistant workflow), OpenCode and Cline (terminal and VS Code coding agents — plan before edit, tests as the gate, MCP wired to a real contract), and Hugging Face (card before weights, licence before commercial use, pin the revision, prefer safetensors).
  • Each carries the same honest frame as the rest of the kit: a working method, not official docs, not affiliated — and every one routes irreversible actions and regulated domains through a human, the way the AI & Agents domain already argues.
  • Skills are a meta layer, not catalogue entries — the component count stays 150.
v1.11.0Universal core · 142 → 150
  • Eight of the most-used, most-often-botched components, done properly: a sortable data table (real <table> semantics, aria-sort shown by glyph not hue, a genuine indeterminate “select all”); a dropdown menu that is a list of actions, not a select in disguise (ARIA menu pattern, roving focus, Esc restores the trigger); a command palette (⌘K) built as a combobox with aria-activedescendant; a tooltip that explains but never contains; copy to clipboard that awaits the promise and never fakes a green tick; a rating input that is an accessible radiogroup, not clickable decoration; a tag input where every token is individually removable and announced; and relative time that keeps the exact, timezone-correct value one hover away.
  • Every one ships the four-cell decision register with a real platform anchor (WAI-ARIA patterns, WCAG 2.2 — 1.3.1, 1.4.1, 1.4.13, 4.1.3), a live demo, and copyable code. All 150 entries now carry an anchor.
  • Counts stay derived from the live navigation — hero, domain map, and cds/components.json (regenerated to v1.8.0) all read 150 without a number being typed by hand.
v1.10.0Design Agent Mode
  • New: Design Agent Mode — an adversarial six-persona operating mode (Market Gap Hunter, Product Architect, Business Logic Commander, Risk & Feasibility Slayer, Empathy & Validation Engineer, Delivery & Momentum Accelerator) with a strict four-step SOP: gap diagnosis → stress & build debate → empathy refinement → delivery & metrics lock, ending at a human go/no-go.
  • Binding constraints ship with it: zero AI-voice, everything quantified with estimate-vs-measured labels, zero tolerance for mediocrity, gap first. Downloadable as design-agent-mode.md — honest framing inside: personas are lenses, not credentials; the mode sharpens the decision, never makes it.
v1.9.0Skills ×6
  • Skills Kit grew 10 → 16: Product Design Kit (tokenize any product surface into a Design Kit document with a mandatory Known Gaps section), Dify Workflow Planner (LLM-app logic before nodes: three-lane graphs, per-node model routing, human gates), ComfyUI Pipeline Architect (graphs as pipelines: pinned seeds, VRAM budgets, versioned workflow JSON, license checks), Generative Image Direction and Generative Video Direction (briefs over slot machines, with binding honesty rules — synthetic never masquerades as evidence, disclosure per EU AI Act Art 50), and AI Asset Integration (provenance intake, C2PA that survives the CDN, versioning like code).
  • Tool skills state their independence plainly: not affiliated with Dify or ComfyUI — the platforms change; the planning discipline is what transfers.
v1.8.0Skills Kit + AI-Ready
  • Skills Kit: ten downloadable, CDS-native Markdown skills for designers and developers — Screen Composer, Component Author, Design Review, Dark-Pattern Audit, Accessibility Verification, UX Writing, Motion Choreography, Token & Theming, Framework Port, AI Surface Kit. Each references the machine contracts directly, so method and machine layer can't disagree; legal-adjacent skills route to counsel by construction. The institutional sibling keeps its 40 general skills; these ten are the catalogue's specialists.
  • AI-Ready Export: compile a Design.md pack client-side from the live contracts — pick a target tool (Claude Code / Claude Desktop / Cursor / any agent) and domains, get the agent protocol, hard rules, bundles, component index and dual-theme tokens in one copyable, downloadable file. Deterministic, no model involved; the Design.md packaging idea popularised by tools like Alpy Studio, implemented here from a live versioned system.
v1.7.0Text Animations ×10
  • New twelfth domain: Text Animations — ten original, zero-dependency motion components in vanilla CSS/JS: Split Reveal, Blur In, Typewriter, Decode, Count Up, Word Rotate, Gradient Flow, Shine Sweep, Wave, and Marquee Ticker. The category landscape is popularised by React libraries (ReactBits and company; the author's own ReactOmega ships the React take) — these are the component-form siblings, token-bound and contract-carrying.
  • Every entry ships the discipline the genre usually skips: real text in the accessible name with animation aria-hidden (screen readers hear one sentence, never letter soup), reserved layout space (zero CLS), self-terminating loops (rotate and wave stop by construction, the marquee ships a visible pause control per WCAG 2.2.2), and reduced-motion renders the final state instantly.
  • Live demos autoplay once on view via IntersectionObserver, with a Replay control per preview; Count Up carries the FTC note that motion is emphasis and emphasised claims need evidence.
v1.6.0Universal & social ×14
  • Social grew 8 → 14: Profile Header (stats are links), Message Inbox (Requests folder — consent applies to attention), Poll & Voting (results only after you vote), Mentions & Hashtags (entities, not text), Live Stream Chat (never steals your scroll), Link Preview Card (the domain is the security surface).
  • Core grew to 37 with the universal gaps: Date & Time Picker (native first, grid for ranges), File & Photo Upload (per-file lifecycle, retry), Multi-Step Flow (Back is free), Stats & Sparkline Card (honest baselines), Coach Marks (one tip, never a tour), Address Form (autocomplete is a shortcut, not a gate), Appearance Switcher (drives this page's real theme), Language & Region (self-named, never flagged).
  • Iconography rebuilt as a governed library — 155 glyphs across 16 semantic categories (navigation, actions, communication, commerce, media, status, trust, system, AI & agents, files, time, health, travel, food, home & devices, weather & environment), each with use/never rules, plus the four selection rules: filled = selected, sizes are stops, RTL mirrors direction not meaning, and the label test. components.json regenerated — 132 entries, all with anchors; eds-mcp consumer pack snapshot refreshed in lock-step.
v1.5.0Claude session
  • New: "Claude × CDS Session" — a step-through reconstruction of a real working session in Claude (Cowork): brief → index fetch with agentProtocol → bundle enforcement → live artifact → a growth request that hits the click-to-cancel hard rule and gets refused with a citation → compliant alternative → human signs. Honest framing: deterministic replay, no model called, no affiliation implied.
  • Setup made concrete: config snippets for Claude Desktop / Cowork (MCP server or zero-install read-the-AGENTS.md-URL) and Claude Code (claude mcp add + a binding CLAUDE.md line).
  • Machine Layer section now routes to both live demos — watch the session, then run the builds.
v1.4.0Agent surfaces ×8
  • AI & Agents grew 14 → 22: Tool & Data Grants (one revocable grant per capability, logged to the trace), Background Agent Tasks (queued / working / needs-you / done, cancel keeps partials), Mode & Model Picker (outcomes not parameter counts, price at the decision point), Ghost Text & Suggested Edits (never auto-commits), Human ↔ Agent Handoff (who's driving, delta recap on resume), Usage & Cost Meter (the cap disclosed before the cap), Generated Artifact Card (versions + provenance + C2PA export), Capability Disclosure (the anti-magic card).
  • components.json regenerated: 118 entries, all with anchors; bundles gained agent-confirm→agent-trace and agent-grants→agent-trace — an approval without an audit trail is theatre.
  • Four new live demos: background task with a needs-you gate state, ghost-text accept/dismiss, handoff take-over with delta recap, credit meter with the 80% announcement.
v1.3.0Agent read path
  • The read path: a seven-step rail documenting exactly how an agent consumes this system — discover → fetch index → select → read register → resolve bundles & hard rules → build from tokens → gate to a human.
  • The reading logic is now machine-carried: components.json ships agentProtocol, bundles, and hardRules alongside the 110 entries, so an agent that fetches the index receives its own instructions with the data.
  • MCP console grew from four to seven tools: find_by_anchor (regulation-first discovery), get_bundles (bundled-by-contract pairs), diff_since (sync the delta, not the world).
v1.2.0AI & Agents · 90 → 110
  • A new domain: AI & Agents (14 entries). Composer, streaming response, sources & citations, confidence & abstention, AI disclosure with C2PA content credentials, agent action confirmation, agent activity trace, prompt starters, feedback & correction, memory & context, training vs personalisation controls, AI summary in search, refusal & human escalation, voice agent. The through-line is the one the institutional sibling already argues: the model produces, the human signs — a gate authorises an exact payload, never a plan; a trace records what was blocked, not just what worked; abstention is a feature; and a refusal with no door is just a door closing on someone who came to you for help.
  • Six gaps closed where the catalogue was thin: date & time picker, upload & camera, multi-step form, coach mark, subscription management, and account deletion — the last because App Store Review 5.1.1(v) makes in-app deletion a requirement, not a courtesy. A system you can only join is not one anyone should trust.
  • The anchors are real and checkable: EU AI Act Art. 50 (transparency), Art. 14 (human oversight), Art. 12 (logging); GDPR Art. 7, 16, 17, 21, 22; NIST AI RMF; C2PA 2.x; FTC negative-option and substantiation rules; WCAG 2.2 — including 4.1.3, which is why the streaming region is aria-live="off" and the finished answer is announced once. Streaming a token at a time into a polite live region makes an answer less accessible than a spinner would have been.
  • Counts everywhere — hero, domain map, cds/components.json — are still derived from the live navigation. Nothing in this release was a number typed by hand.
v1.1.0AI & provenance
  • Machine layer strengthened: head-level JSON discovery links + Dataset structured data; AI-tool strip and a "Connect it" panel (direct fetch today, MCP via the eds-mcp production pattern) with copyable config; downloadable AGENTS.md drop-in brief for any agent.
  • New: "Build with an Agent" — a deterministic in-page simulation of an agent assembling four briefs from the catalogue, including a COPPA hard-block, against the real component index.
  • New: "Who Built This" — provenance section linking every capability claim to its artifact (institutional showcase, eds-mcp, Ed Agent, OSS, signed recommendations).
  • Chrome: portfolio link moved to the top-right and sidebar footer.
v1.0.0Initial release
  • 90 component categories across 10 domains — foundations, core, navigation, onboarding & identity, commerce, social, media, growth & engagement, trust & safety, patterns.
  • Every entry ships the four-cell decision register with a platform anchor (HIG, Material 3, WCAG 2.2, App Store / Play policy, GDPR, COPPA, FTC), a live tokenised preview, and copyable vanilla code.
  • Machine layer: cds/tokens.json + cds/components.json, generated deterministically from this page's navigation; illustrative MCP console shows the tool-call shape.
  • Dual theme (light default, dark), spring motion gated behind prefers-reduced-motion, 44px touch targets, WCAG 2.2 AA contrast baseline.

Foundations

Color

A near-neutral canvas with one working accent. Consumer color earns attention it doesn't demand: semantic colors mean the same thing on every screen, and no state is ever encoded by hue alone.

When to use

Accent for the one primary action per view and for selected states. Semantic green/red/amber only for outcome, danger, and caution. Everything else stays neutral so content owns the saturation.

When not to

Never use red for emphasis — it is reserved for destructive and error meaning. Never encode a state by color alone; pair with an icon, weight, or label (streaks, unread, live all double-encode here).

Behaviour & accessibility contract

Text tokens hold ≥ 4.5:1 against their surfaces in both themes; large text and icons ≥ 3:1. Both themes ship from the same token names — components never reference a raw hex.

Platform anchor

WCAG 2.2 · 1.4.3 contrast (minimum) and 1.4.1 use of color; HIG Color (semantic, adaptive colors); Material 3 color roles.

Neutrals & text

Live tokens — flip the theme toggle to watch them adapt
--bg
--surface
--surface2
--border
--text1
--text2
color: var(--text1); /* primary ink */ background: var(--surface); /* card on --bg canvas */ border: 1px solid var(--border);

Accent & semantics

Live tokens
--accent
--green
--red
--amber
--purple

Foundations

Typography

Two voices: a tight display face for moments, the native system stack for everything else. Consumer type is generous — bigger minimums, looser leading, tighter tracking as size grows.

When to use

Display face (Inter Tight, −0.03em and tighter) only for hero and section moments. Body is the system stack at 16px minimum — it renders instantly, matches the OS, and never needs a font download to be readable.

When not to

No display face below 20px — tight tracking collapses at small sizes. Never justify body text, never letter-space lowercase body copy, never ship text in images.

Behaviour & accessibility contract

All sizes in rem so user font-size preferences scale the whole system; layouts survive 200% zoom (WCAG 1.4.4) and reflow at 320px (1.4.10). Numerals in prices and counters are tabular.

Platform anchor

HIG Typography (Dynamic Type); Material 3 type scale; WCAG 2.2 · 1.4.4 resize text, 1.4.10 reflow, 1.4.12 text spacing.

Interactive specimen

Live specimen — drag the sliders, click the text to edit it
Design is how it works.

The scale

Type ramp
display · 800 · −0.04em · Inter TightMoments
title-1 · 700 · −0.03emScreen titles
title-2 · 700 · −0.02emSection headers
headline · 600Card and cell headlines
body · 400 · 16px minimumBody copy stays at the system size the reader chose — comfortable at arm's length, on a train, at 2 a.m.
footnote · 500 · 13pxMetadata, timestamps, helper text — never below 11px, never the only place a fact lives.
--t-display: clamp(2.6rem, 6vw, 4.6rem) / 1.02 'Inter Tight'; --t-title1: clamp(2rem, 4vw, 3.1rem); --t-body: 1rem / 1.6 system-ui; /* 16px floor */ font-variant-numeric: tabular-nums; /* prices & counters */

Foundations

Spacing & Layout

A 4px base grid with an 8-step scale. White space is the cheapest premium material there is — this system spends it deliberately.

When to use

Scale steps only — 4, 8, 12, 16, 24, 32, 48, 64. Related things sit one step apart, unrelated things two. Section rhythm comes from consistent vertical steps, not from dividers.

When not to

No off-scale values to "make it fit" — if a layout needs 13px, the layout is wrong. Don't add dividers and spacing; pick one separator per level.

Behaviour & accessibility contract

Interactive targets are at least 24×24 CSS px (WCAG 2.5.8); where a visual is smaller, the spacing exception applies — a 24px circle centred on it must not intersect another target's circle, which is a centre-to-centre rule, not a clear-gap one. List rows pad to a 44px minimum touch height.

Platform anchor

HIG Layout (44pt targets, safe areas); Material 3 4dp grid; WCAG 2.2 · 2.5.8 target size (minimum).

The scale
--s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px; --s-5: 24px; --s-6: 32px; --s-7: 48px; --s-8: 64px;

Foundations

Materials & Elevation

Three levels of shadow and one glass. Elevation answers exactly one question — what is above what — so it is spent like money.

When to use

Level 1 for resting cards, level 2 for raised or hovering elements, level 3 only for overlays that float above the page (sheets, toasts, menus). Glass for chrome that must show content scrolling beneath it.

When not to

Glass never carries body text over unknown content without a contrast fallback. No stacked glass — one translucent layer per screen region, or blur costs melt low-end phones.

Behaviour & accessibility contract

Every glass surface declares a solid fallback color for browsers without backdrop-filter, and text on it still meets 4.5:1 against the worst-case underlying content in the fallback.

Platform anchor

HIG Materials (vibrancy, translucency); Material 3 elevation; WCAG 2.2 · 1.4.3 contrast over variable backgrounds.

Elevation levels
--shadow-1Resting card
--shadow-2Raised / hover
--shadow-3Overlay
Glass over live content
Frosted chrome

Blur + saturation lifts the layer; the hairline sells the edge. Content stays the painting — this is just the frame.

background: var(--glass); /* solid-ish fallback */ -webkit-backdrop-filter: blur(22px) saturate(1.6); backdrop-filter: blur(22px) saturate(1.6); border: 1px solid var(--glass-border);

Foundations

Motion

One spring, three durations. Motion in this system is spatial reasoning — it tells you where things came from and where they went. Anything that only decorates gets cut.

When to use

Enter/exit of overlays, state changes the user caused, and continuity between views. The spring — cubic-bezier(0.32, 0.72, 0, 1) — is shared by everything, so the product feels like one object.

When not to

No ambient looping animation near reading content. No motion longer than 480ms on the critical path — the user came to do something, not to watch the interface perform.

Behaviour & accessibility contract

prefers-reduced-motion is a contract: transitions collapse to opacity or nothing, parallax and auto-playing movement stop. Nothing flashes more than three times per second, ever.

Platform anchor

WCAG 2.2 · 2.3.3 animation from interactions, 2.3.1 flash thresholds; HIG Motion; Material 3 motion (emphasized easing).

The house spring — click the tile
cubic-bezier(0.32, 0.72, 0, 1) · 480ms — a settle, not a bounce.
--spring: cubic-bezier(0.32, 0.72, 0, 1); --dur-1: 160ms; /* state feedback */ --dur-2: 280ms; /* local movement */ --dur-3: 480ms; /* overlays & spatial changes */ @media (prefers-reduced-motion: reduce) { /* collapse to opacity */ }
Canonical Motion Primitives · 12 Tokens & MCP Specs

12 Canonical Motion Tokens Click any tile to trigger its real-time spring curve, or inspect its MCP tool response payload serving cds/tokens.json.

01 · State Feedback

State Feedback Snap

Tactile press response with elastic recoil snap. Shared by buttons, toggles, and chip selections.

--spring-bounce · 160ms
02 · Spatial Morph

Spatial Layout Morph

Pill expands into expanded metadata card. Maintains spatial continuity across views.

Compact Pill →
Expanded Metadata Content Panel
--spring-smooth · 280ms
03 · Overlay Settle

Sheet & Overlay Settle

Bottom sheet slide-up with spring damping & backdrop blur fade. "The house spring".

Sheet Content Settle · 480ms
--spring-house · 480ms
04 · Magnetic Pull

Magnetic Hover Affordance

Tile follows cursor within magnetic pull radius with inertia recoil upon release.

Magnetic CTA
--spring-magnetic · 320ms
05 · Accordion Unfold

Accordion & Panel Unfold

Height auto-fit expansion with staggered opacity reveal for nested content.

Disclosed content panel with smooth height spring calculation.
--ease-out-back · 280ms
06 · Radial Ripple

Radial Liquid Ripple

Click anywhere inside tile to emit expanding translucent liquid wave from cursor coordinates.

Click Anywhere For Ripple
--effect-ripple · 600ms
07 · Skeleton Shimmer

Skeleton & Loading Shimmer

Shimmering gradient light beam scanning across loading skeleton placeholders.

--effect-shimmer · 1.6s infinite
08 · Toast Spring Pop

Toast & Notification Pop

Notification pill pops up with overshoot spring & auto-dismiss timer bar.

✓ Order executed · 0.002s
--spring-pop · 360ms
09 · Cascade Stagger

Staggered List Cascade

Items cascade into view sequentially with 60ms staggered delays and slide-up fade.

Item Alpha
Item Beta
Item Gamma
--stagger-step · 60ms step
10 · Specular Glint

Glass Specular Highlight

Diagonal specular light ray glints across dark glassmorphic card on hover/trigger.

Hover for Specular Beam
--effect-specular · 2.4s sweep
11 · 3D Card Flip

3D Perspective Flip

Card flips 180 degrees in 3D perspective to reveal machine MCP JSON payload.

Click to Flip (Front)
{"mcp": "ok"}
--transform-flip-3d · 520ms
12 · WCAG Reduced Fallback

Reduced Motion Contract

Instant collapse to zero spatial movement & 180ms opacity crossfade under prefers-reduced-motion.

Spatial Movement Stopped
WCAG 2.2 · 2.3.3 · 180ms opacity
/* Motion Tokens & MCP Server Payload (cds/tokens.json) */ --spring-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); /* 160ms */ --spring-smooth: cubic-bezier(0.25, 1, 0.5, 1); /* 280ms */ --spring-house: cubic-bezier(0.32, 0.72, 0, 1); /* 480ms */ --spring-magnetic: cubic-bezier(0.175, 0.885, 0.32, 1.275); --ease-out-back: cubic-bezier(0.34, 1.3, 0.64, 1); --effect-ripple: 600ms cubic-bezier(0, 0, 0.2, 1); --effect-shimmer: 1600ms linear infinite; --spring-pop: cubic-bezier(0.68, -0.55, 0.265, 1.55); --stagger-step: 60ms cubic-bezier(0.16, 1, 0.3, 1); --effect-specular: 2400ms cubic-bezier(0.4, 0, 0.2, 1); --transform-flip-3d: 520ms cubic-bezier(0.4, 0, 0.2, 1); --a11y-reduced-motion: opacity 180ms ease-out; /* WCAG 2.2 · 2.3.3 */

Foundations

Iconography

Icons are labels that got shorter, not decoration that got meaning. One family, one optical grid, and a text label whenever the action is destructive or unfamiliar.

When to use

Recognition-level concepts only — search, share, settings, play. Filled variants mark selected state against outlined resting state (the tab bar pattern).

When not to

Icon-only buttons for irreversible actions are forbidden — "delete" gets a word. Never invent a metaphor a first-time user must decode; if it needs a tooltip to be understood, it needs a label instead.

Behaviour & accessibility contract

Every functional icon carries an accessible name (aria-label or visually-hidden text); decorative icons carry aria-hidden. Minimum functional icon target: 44px including padding.

Platform anchor

HIG SF Symbols conventions (weight-matched, filled-selected); Material Symbols; WCAG 2.2 · 1.1.1 non-text content.

How to choose — the four rules

Selection logic

155 glyphs · 16 semantic categories · one optical grid
Filled = selected

Outlined is the resting state, filled is the active one — the tab-bar convention. Never mix weights inside one surface; the eye reads weight as meaning.

Sizes are stops, not a dial

16px inline with text, 20px in list cells, 24px in bars and buttons, 28px+ only as feature art. Functional icons keep a 44px touch target regardless of glyph size.

RTL mirrors direction, not meaning

Back/forward, chevrons, and send mirror in right-to-left locales. Checkmarks, playback, clocks, and brands never mirror — physics and convention don't flip.

The label test

If a first-time user needs a tooltip to decode the glyph, it needs a visible label instead. Destructive, irreversible, and money-moving actions always carry the word.

Navigation & wayfinding10 glyphs

Use Getting around. Directional glyphs mirror in RTL; position is a promise — back lives top-left forever.

Never A chevron discloses within a place; an arrow moves you to another one. Swapping them breaks the user's map. Close (×) dismisses without saving — never use it to mean done.

home
back
forward
close
menu
explore
drill in
expand
external
search
Actions & editing12 glyphs

Use Verbs the user performs on things. Every glyph here changes state — so every one must be undoable or confirmed.

Never Destructive actions (trash) are never icon-only — 'Delete' gets the word. Undo/redo arrows are never decoration. Send commits: don't reuse the paper plane for anything reversible.

add
edit
delete
undo
redo
copy
download
upload
refresh
send
filter
scan
Communication & social10 glyphs

Use People talking to people. These glyphs carry badges — which makes them attention surfaces with a budget.

Never The bell is for things that happened, never for marketing (see Badge & Count). The share glyph follows the platform — iOS and Android draw it differently; don't invent a third.

comment
alerts
email
mention
topic
call
video call
people
share
react
Commerce & money10 glyphs

Use Where glyphs meet wallets. Everything here appears near a price, so precision beats charm.

Never Pick bag or cart once and never mix them — they are the same concept wearing two coats. Currency symbols are locale, not decoration: $ is not universal, and the receipt glyph never decorates marketing.

bag
cart
payment
wallet
price
receipt
delivery
order
gift
discount
Media & playback11 glyphs

Use Fifty years of convention. Playback glyphs are the closest thing interface design has to traffic signs.

Never Never restyle play/pause into brand shapes — recognition is the entire value. CC is a first-class control, not a settings burial. The camera glyph means capture now, not photos-the-noun (that's image).

play
pause
next
capture
image
voice
sound
captions
fullscreen
audio
video
Status & feedback10 glyphs

Use The system talking back. Each of these is one third of a triple encoding: icon + word + color, always together.

Never Semantic glyphs are never decorative — a checkmark that doesn't mean success is a lie in vector form. The spinner means short waits only; anything longer earns a progress bar and a cancel.

success
failure
warning
info
pending
waiting
blocked
rating
report
help
Privacy, trust & safety9 glyphs

Use The glyphs users bet on. A lock is a claim about the world, so it renders only when the claim is true.

Never Never use the padlock as marketing garnish — it means this, here, is actually protected. Eye/eye-slash toggle visibility states honestly. The fingerprint never implies biometrics leave the device.

secure
unlocked
protection
visible
hidden
biometric
credential
account safety
block
System & settings11 glyphs

Use The machine's own furniture: preferences, connectivity, locale, accessibility.

Never OS status glyphs (signal, battery) belong to the OS — faking system UI inside an app is a trust violation. The globe means web or region by context; pair it with a label so it never has to mean both at once.

settings
connectivity
location
region
language
dark mode
light mode
accessibility
sync
power
device
AI & agents9 glyphs

Use Marking where the machine is. These glyphs are disclosure surfaces — their presence is a claim about who or what produced something.

Never Sparkles mean AI-generated or AI-powered, here, truthfully — never sprinkle them on ordinary features to look modern (that's the capability-overclaim FTC cares about). The robot renders only when a bot is actually speaking.

AI feature
agent
suggestion
on-device
fast mode
connections
usage
assistant
model
Files & documents10 glyphs

Use Things users keep. The folder is a place, the file is a thing, the paperclip attaches — three metaphors older than the web, still load-bearing.

Never Format glyphs (PDF) appear only when the format changes what the user can do with it. Print exists wherever a document does — receipts and tickets especially. Archive means retrievable; if it's actually delete, use delete.

file
document
folder
open folder
attach
pdf
archive
print
note
import
Time & scheduling8 glyphs

Use When things happen. Calendar glyphs distinguish looking (days), adding (plus), and confirmed (check) — three different promises.

Never History (the rewinding clock) is the user's own data trail — wherever it appears, a way to clear it must exist nearby (GDPR Art 17 adjacency). Recurring (repeat) on anything billable is a subscription disclosure, not decoration.

calendar
add event
booked
history
timer
in progress
recurring
snooze
Health & activity9 glyphs

Use The body's dashboard. Everything in this row marks special-category data — the glyph itself signals sensitivity.

Never Health glyphs flag GDPR Art 9 territory: where they appear, consent and controls follow. Never gamify the medical (streaks on medication adherence need clinical judgment, not growth instincts), and the flame never pressures eating behaviour.

vitals
workout
steps
strength
sleep
hydration
weight
record
medication
Travel & places10 glyphs

Use Modes and destinations. Transport glyphs are choices with prices — they sit next to money, so they carry the precision discipline of commerce.

Never The passport marks identity-document territory: wherever it appears, treat the flow like KYC, not like a form. Mode glyphs never editorialise — the bicycle isn't 'eco' unless the claim is substantiated (see Weather & environment).

flight
transit
drive
bike
stay
trip
travel ID
directions
destinations
nearby
Food & dining10 glyphs

Use Appetite at a glance. Category glyphs help scanning; dietary glyphs are safety information wearing a small coat.

Never Dietary markers (plant-based leaf, gluten wheat) are accuracy claims with allergy consequences — they render from verified data only, never from vibes. The scooter means live delivery tracking, not a marketing promise of speed.

dining
café
pizza
fast food
dessert
drinks
meal
delivery
plant-based
contains gluten
Home & devices8 glyphs

Use The connected household. Every glyph here mirrors a physical device's state — the interface is a remote control for reality.

Never IoT state must be verified state: a lock glyph showing 'locked' from a stale cache is a safety lie, not a rendering bug. Show last-updated time when confidence is stale, and never render a camera glyph as recording when it isn't — or vice versa.

smart home
lights
thermostat
power
climate
access
screen
device battery
Weather & environment8 glyphs

Use The outside world, and claims about caring for it.

Never Eco glyphs (leaf, seedling, recycle) are regulated marketing claims — the FTC Green Guides apply to a green leaf on a checkout as much as to an ad. 'Eco delivery' with no substantiation is greenwashing at 16 pixels.

partly sunny
rain
snow
wind
take one
heat
eco claim
recyclable
<i class="fas fa-arrow-left" aria-hidden="true"></i> <!-- a glyph is never the only label: the button that holds it carries the name --> [dir="rtl"] .ico-directional { transform: scaleX(-1); } /* chevron discloses in place · arrow moves you elsewhere · × dismisses without saving */

Honest scope. The glyphs on this page render in Font Awesome as a preview stand-in; in a shipped product each platform uses its native family (SF Symbols, Material Symbols) at matching optical weight. The contract this catalogue defines is the semantics and the limits — which glyph means what, where it may appear, and what it must never be used to imply — not the vendor of the vectors.

Foundations

Haptics & Sound

The invisible foundation. A web page can't fire a Taptic Engine, so this entry is the policy layer: when native surfaces of the same product should speak through touch and sound, and when they must stay silent.

When to use

Haptics confirm what the eye might miss: a toggle landing, a pull-to-refresh arming, a payment succeeding. Sound is rarer still — reserved for messages arriving and capture moments.

When not to

Never haptic on scroll, on every keystroke, or to celebrate the app itself. Duplicate meaning, don't invent it — a buzz with no visible counterpart is noise with extra steps.

Behaviour & accessibility contract

All haptic and audio feedback is redundant to a visible change — deaf and haptic-off users lose texture, never information. System silent-mode is always respected.

Platform anchor

HIG Playing haptics ("use haptics consistently, don't overuse"); Material guidance on haptic feedback; WCAG 2.2 · 1.3.3 sensory characteristics and 1.4.1 use of colour — instruction may not depend on a single sense. (There is no “1.1 redundancy principle”: 1.1 is Text Alternatives.)

The vocabulary — a policy table, honestly not a demo
selectionSegmented controls, pickers ticking past valuesLight tick · no sound
impact-lightToggle lands, sheet snaps to a detentSoft tap · no sound
successPayment confirmed, upload completeDouble tap · optional chime
warningDestructive confirm step appearsFirm tap · no sound
errorWrong passcode, failed actionBuzz · optional thud
/* haptics are typed by meaning, never by "how big does this feel" */ haptic('selection'); // a value ticked past — light, silent haptic('success'); // the money moved — double tap, chime optional haptic('error'); // the passcode was wrong — buzz // every call is a no-op when the OS reports reduce-motion or silent mode, // and no pattern is the only signal for anything: the screen says it too

Core · Control

Button

The pill is the promise: one obvious next step per view. Everything else steps down in weight until hierarchy does the persuading, not color.

When to use

Actions — buy, send, save, continue. One primary per view region; secondary and ghost carry everything else. Full-width primaries live at the thumb edge on mobile.

When not to

Navigation is a link, not a button. Danger styling is for destructive actions only — never for urgency theatre ("Buy now before it's gone" does not get to be red).

Behaviour & accessibility contract

44px minimum height. Loading sets aria-busy and disables the control — a double-tap on "Place order" is a duplicate charge. Focus ring is restyled, never removed.

Platform anchor

HIG Buttons (prominence hierarchy, 44pt targets); Material 3 button hierarchy; WCAG 2.2 · 2.5.8 target size, 2.4.7 focus visible.

Variants
<button class="cc-btn cc-btn-primary">Continue</button> <button class="cc-btn cc-btn-secondary">Secondary</button> <button class="cc-btn cc-btn-outline">Outline</button> <button class="cc-btn cc-btn-ghost">Ghost</button> <button class="cc-btn cc-btn-danger">Delete account</button>
Sizes & states — click "Toggle loading"
<button class="cc-btn cc-btn-primary loading" aria-busy="true" disabled> Placing order… </button>

Core · Form

Text Input

Labels stay visible, errors say what to do next, and the keyboard that appears is the one the field deserves. Most abandoned checkouts die in a form.

When to use

Free-form single-line entry with a permanent label above and helper text below. The right inputmode and autocomplete attributes are part of the component, not an optimisation.

When not to

Placeholder-as-label fails the moment typing starts. If the answer is one of five things, use a select or segmented control — don't make thumbs spell what a tap can choose.

Behaviour & accessibility contract

Error text is tied via aria-describedby and never conveyed by border color alone. Fields never clear on error. Labels are real <label> elements — tapping them focuses the field.

Platform anchor

WCAG 2.2 · 3.3.1 error identification, 3.3.2 labels, 3.3.7 redundant entry, 1.3.5 identify input purpose (autocomplete); HIG Text fields.

States
We'll only use this for receipts.
Card number looks too short — it should be 16 digits.
Available.
<div class="cc-field cc-field--error"> <label class="cc-label" for="card">Card number</label> <input class="cc-input" id="card" inputmode="numeric" autocomplete="cc-number" aria-describedby="card-err"> <span class="cc-help" id="card-err">Card number looks too short — it should be 16 digits.</span> </div>

Core · Form

Select

The native picker wearing the system's clothes. On phones the OS wheel and sheet pickers beat anything a div can fake — so this select stays a <select>.

When to use

Five to fifteen mutually exclusive options. Below five, use a segmented control or radios so all choices are visible; above fifteen, use search-in-a-sheet.

When not to

Never for two options (that's a toggle or radio pair), never for navigation, and never a custom-div dropdown when the native element would do — you inherit a decade of OS accessibility for free.

Behaviour & accessibility contract

Keyboard type-ahead, arrow navigation, and screen-reader announcement come from the native element and are never re-implemented worse. The label is external and permanent.

Platform anchor

HIG Pickers; Material 3 menus; WCAG 2.2 · 4.1.2 name-role-value (free with native semantics).

Native select, system skin
<label class="cc-label" for="speed">Delivery speed</label> <select class="cc-select" id="speed"> <option>Standard — free, 4–6 days</option> <option>Express — $4.99, 2 days</option> </select>

Core · Control

Slider

For values where the feel matters more than the digits — volume, brightness, price range. The current value is always readable, never guessed from thumb position.

When to use

Continuous or dense ranges adjusted by feel. Pair with a live output element; for money and quantities offer direct numeric entry too.

When not to

Not for precise required values (a date of birth on a slider is a usability crime) and not with fewer than ~10 steps — that's a segmented control.

Behaviour & accessibility contract

Native input[type=range]: arrow keys step, Home/End jump, aria-valuetext gives units ("$120"). The 26px thumb clears WCAG's 24px floor with a 44px touch halo.

Platform anchor

HIG Sliders; Material 3 sliders; WCAG 2.2 · 2.5.7 dragging movements — which requires a single-pointer, non-dragging alternative, not merely a keyboard one; keyboard operability is 2.1.1 and both are shipped; 2.5.8 target size.

Live value
50
<input type="range" class="cc-slider" min="0" max="100" value="50" aria-label="Volume" aria-valuetext="50 percent"> <output class="cc-slider-out">50</output>

Core · Control

Toggle Switch

The switch means now — flip it and the thing is on. If anything else must happen first (a save button, a payment), it isn't a switch.

When to use

Binary settings with immediate effect: notifications, dark mode, autoplay. The label states the thing controlled, not the state ("Wi-Fi", never "On").

When not to

Not in forms that submit later — that's a checkbox. Never pre-flip a switch that spends the user's money or shares their data; defaults are a consent question, not a growth lever.

Behaviour & accessibility contract

Built on a real checkbox input — Space toggles, state announces. Color is not the only state signal: the thumb position carries meaning in grayscale.

Platform anchor

HIG Toggles; Material 3 switch; WCAG 2.2 · 1.4.1 use of color, 4.1.2 name-role-value; GDPR Art 7 — data toggles default off.

Live
<label class="cc-switch"> <input type="checkbox" role="switch" aria-label="Notifications"> <span class="cc-switch-track"></span> </label>

Core · Form

Checkbox & Radio

The oldest controls on the web, kept native and kept honest: checkboxes for any-of, radios for one-of, and the whole row is the target.

When to use

Checkbox for independent opt-ins and multi-select lists; radio when exactly one of 2–5 visible options must be chosen and the choice should be comparable at a glance.

When not to

Pre-ticked marketing checkboxes are a consent violation, not a conversion tactic. Radios with one option are a lie; radios with twelve are a select.

Behaviour & accessibility contract

Native inputs with accent-color — full keyboard and screen-reader behaviour for free. Labels are clickable; groups get a fieldset/legend so the question travels with the answers.

Platform anchor

WCAG 2.2 · 1.3.1 info & relationships (fieldset), 2.5.8 target size; GDPR Art 4(11) with Recital 32 — consent is an unambiguous affirmative act, so a pre-ticked box is not consent (Planet49, C-673/17) — and Art 7(4) for unbundling; ePrivacy consent conventions.

Live
<label class="cc-choice"><input type="checkbox"> Marketing emails</label> <!-- unticked by default: consent is opt-in, GDPR Art 7 --> <label class="cc-choice"><input type="radio" name="fit" checked> Regular fit</label>

Core · Control

Segmented Control

All the options, all the time. The sliding thumb makes switching feel free — which is exactly when users explore.

When to use

2–5 short, mutually exclusive views or filters of the same content: List/Map, Weekly/Monthly, Delivery/Pickup. The selection changes the view immediately.

When not to

Not for actions (that's buttons) and not for navigation between unrelated destinations (that's tabs or a tab bar). Labels that truncate mean the control is the wrong choice.

Behaviour & accessibility contract

Arrow keys move selection; the group carries a single tab stop. Selected state is encoded by the raised thumb and weight, not color alone.

Platform anchor

HIG Segmented controls; Material 3 segmented buttons; WAI-ARIA radio-group pattern.

Live — click to switch
<div class="cc-seg" role="group" aria-label="View"> <button class="active" aria-pressed="true">List</button> <button aria-pressed="false">Grid</button> <button aria-pressed="false">Map</button> </div>

Core · Control

Quantity Stepper

Plus, minus, and a number that never surprises you. The humble stepper moves more units of physical goods than any carousel ever will.

When to use

Small integer adjustments with a sane range — cart quantities, tickets, guests. Bounds are enforced at the control: the minus disables at the minimum instead of erroring.

When not to

Not for large or unbounded numbers (use an input) and not for continuous values (use a slider). Removing the last item is a delete — hand off to a remove affordance, don't step to zero silently.

Behaviour & accessibility contract

Buttons carry aria-labels ("Increase quantity"); the value is an output element announced on change. Both buttons hold the 44px target floor.

Platform anchor

HIG Steppers; WCAG 2.2 · 4.1.3 status messages, 2.5.8 target size.

Live
1
<div class="cc-qty"> <button aria-label="Decrease quantity">−</button> <output aria-live="polite">1</output> <button aria-label="Increase quantity">+</button> </div>

Core · Control

Chip & Tag

Small, tappable, and plural. Chips are how consumer products let people say "these, not those" without a form.

When to use

Filter sets, interest pickers, removable selections. Multi-select by default; a chip row scrolls horizontally on mobile rather than wrapping into a wall.

When not to

Chips are not buttons — they select, they don't submit. If only one may be active, show it as a segmented control or radio chips with explicit single-select semantics.

Behaviour & accessibility contract

Toggle chips expose aria-pressed; removable chips make the ✕ its own labelled 24px+ target. Selection is encoded by fill inversion, not hue alone.

Platform anchor

Material 3 chips (filter/input/assist); HIG Tokens & tags conventions; WCAG 2.2 · 2.5.8.

Live — tap to select
<button class="cc-chip" aria-pressed="true">Vegan</button> <button class="cc-chip" aria-pressed="false">Under $15</button> <button class="cc-chip"><i class="fas fa-xmark cc-chip-x" aria-hidden="true"></i> Sneakers <span class="visually-hidden">Remove filter</span></button>

Core · Display

Badge & Count

Tiny, loud, and easy to abuse. A badge is a claim on attention — this system rations them like the currency they are.

When to use

Status that changes decisions (Shipped, Refunded, New) and unread counts that clear when acted on. A count badge must always be dismissible by doing the thing it points at.

When not to

Never manufacture urgency — a permanently red "3" that never clears trains users to ignore every badge you'll ever ship. Marketing does not get to borrow the notification badge.

Behaviour & accessibility contract

Counts carry accessible text ("3 unread messages"), truncate at 99+, and pair color with position and text — status badges always contain a word, never only a color dot.

Platform anchor

HIG notification badge conventions; Material 3 badges; WCAG 2.2 · 1.4.1 use of color.

Statuses & counts
Shipped Low stock Payment failed Pre-order New
<span class="cc-badge cc-badge--ok"><i class="fas fa-check" aria-hidden="true"></i> Shipped</span> <span class="cc-dot" data-count="3"> <button aria-label="Notifications, 3 unread">…</button> </span>

Core · Display

Avatar & Presence

The pixel-sized proof there's a person here. Initials fall back gracefully, groups stack, and the green dot never lies.

When to use

Anywhere content has an author or a counterparty — posts, chats, reviews, orders ("packed by"). Deterministic gradient + initials when no photo exists, so the same user always looks the same.

When not to

Don't show presence unless it's real and consented — a fake "online" dot is a trust forfeiture. No avatars for system messages; robots get glyphs, not faces.

Behaviour & accessibility contract

Images carry the person's name as alt text; presence pairs the dot with text nearby ("Active now"). Stacked groups expose a text count ("and 12 others").

Platform anchor

WCAG 2.2 · 1.1.1 non-text content, 1.4.1 use of color; GDPR — presence is personal data, off by default where jurisdictions require.

Sizes, groups, presence
JK AM RS LT JKAMRS
<span class="cc-avatar">AM</span> <!-- initials fallback --> <span class="cc-presence"><span class="cc-avatar">LT</span></span> <span class="cc-avatar-group">…<span>and 12 others</span></span>

Core · Display

Card

One idea per card, one card per idea. The hover lift is the system's handshake — content rises to meet you, gently.

When to use

Self-contained units in a browsable set — articles, places, playlists. The whole card is the link; inner actions stop propagation and get their own targets.

When not to

Not as a wrapper for everything — a settings screen of nested cards-in-cards is a spacing failure wearing borders. Sequential reading content wants a list, not a card grid.

Behaviour & accessibility contract

One tab stop per card (the primary link), inner actions reachable after it. The lift transition is transform-only and disappears under reduced motion.

Platform anchor

Material 3 cards; HIG content organisation; WCAG 2.2 · 2.4.4 link purpose in context.

Media card

Alpine mornings

Twelve cabins above the fog line, from $89 a night.

New

Focus mix

Two hours of instrumental momentum, refreshed weekly.

42 tracks
<article class="cc-card"> <div class="cc-card-media">…</div> <div class="cc-card-body"><h4>Alpine mornings</h4><p>…</p></div> <div class="cc-card-foot">…</div> </article>

Core · Display

List & Cell

The inset grouped list is the load-bearing wall of every consumer app: settings, inboxes, results, orders. Icon, two lines, trailing detail — a grammar everyone already reads.

When to use

Homogeneous, vertically scanned items. The chevron means "more screen behind this"; a trailing control (switch, count) means the row is the whole story.

When not to

Don't mix row semantics in one group — navigation rows and toggle rows live in separate sections. Rich comparison shopping wants cards; lists are for scanning, not weighing.

Behaviour & accessibility contract

Rows are ≥44px, entire row is the target, secondary text truncates rather than wraps at scan-breaking length. Semantics: a nav list is a list of links, not divs with click handlers.

Platform anchor

HIG Lists and tables (inset grouped); Material 3 lists; WCAG 2.2 · 2.5.8, 1.3.1.

Inset grouped list
Dark modeMatch system
<div class="cc-list"> <a class="cc-cell" href="…"> <span class="cc-cell-ico">…</span> <div class="cc-cell-main"><b>Account</b><span>june.k@example.com</span></div> <span class="cc-cell-side"><i class="fas fa-chevron-right"></i></span> </a> </div>

Core · Navigation-in-place

Tabs

Peer views of the same subject, underlined so the floor tells you where you stand. Tabs change the view; the tab bar (see Navigation) changes the world.

When to use

2–6 sibling facets of one thing — Posts / Replies / Media on a profile. State survives switching; nothing is lost by looking.

When not to

Not for sequential steps (that's a stepper flow) and not as an overflow bin — a "More" tab is information architecture giving up.

Behaviour & accessibility contract

ARIA tabs pattern: arrow keys move, the tablist is one tab stop, selection follows the roving focus. The underline is position-encoded, not color-encoded.

Platform anchor

WAI-ARIA tabs pattern; Material 3 tabs; HIG segmented alternatives guidance.

Live — click to switch
<div class="cc-tabs" role="tablist" aria-label="Profile views"> <button class="cc-tab active" role="tab" aria-selected="true">Posts</button> <button class="cc-tab" role="tab" aria-selected="false" tabindex="-1">Replies</button> </div>

Core · Feedback

Toast & Snackbar

Confirmation that doesn't ask for anything back. Glass pill, four seconds, gone — and when it carries an Undo, it is quietly the most humane component in the system.

When to use

Outcomes that need acknowledgment but not action: saved, sent, added to bag. Pair destructive actions with an Undo toast instead of a pre-confirm dialog (see the Undo pattern).

When not to

Never for errors that require action — those need a banner or dialog that waits. Never stack three toasts deep; queue them. A toast with two buttons is a dialog in denial.

Behaviour & accessibility contract

role="status" (polite announcement), pauses on hover/focus, and any action inside is reachable before timeout — WCAG gives users control over time limits, so Undo lingers.

Platform anchor

Material 3 snackbar; WCAG 2.2 · 2.2.1 timing adjustable, 4.1.3 status messages.

Live — fire one
<div class="cc-toast" role="status"> <i class="fas fa-check" aria-hidden="true"></i> Saved to your library </div> <div class="cc-toast" role="status"> Message deleted <button>Undo</button> </div>

Core · Overlay

Modal Dialog

The interface's interruption budget, spent one dialog at a time. If the user didn't cause it and can't lose anything, it doesn't get to be modal.

When to use

Decisions that block everything else: discard unsaved edits, confirm a payment change, sign in to continue. Title states the decision; buttons state outcomes, never "Yes/No".

When not to

Marketing does not get modals. Mobile prefers a bottom sheet for anything with options. And a modal on page load — before the user has done anything — is the fastest trust burn in the catalog.

Behaviour & accessibility contract

Focus moves in, cycles inside, and returns to the trigger on close. Esc closes, scrim-click closes non-destructive dialogs, and the page behind is inert to screen readers.

Platform anchor

WAI-ARIA dialog pattern; HIG Alerts (outcome-named buttons); WCAG 2.2 · 2.4.3 focus order, 2.1.2 no keyboard trap.

Live — open it
<div class="cc-overlay open" role="presentation"> <div class="cc-modal" role="dialog" aria-modal="true" aria-labelledby="dlg-t"> <h3 id="dlg-t">Discard this draft?</h3> <p>Your edits from the last 4 minutes will be lost.</p> <div class="cc-modal-actions"> <button class="cc-btn cc-btn-secondary">Keep editing</button> <button class="cc-btn cc-btn-danger">Discard</button> </div> </div> </div>

Core · Overlay

Bottom Sheet

The mobile-native answer to "show me options without leaving." It rises from where thumbs live, and the grabber is a promise: you can always push it back down.

When to use

Contextual actions, filters, and details on mobile — anything a desktop popover would do. Detents (half, full) let content and control share the screen.

When not to

Not for blocking decisions (that's a dialog — a sheet is dismissible by design) and not nested: a sheet opening a sheet means the flow wanted a full screen.

Behaviour & accessibility contract

Swipe-down, scrim tap, and Esc all dismiss; the grabber is decorative while a real close button carries the accessible name. Focus management mirrors the dialog contract.

Platform anchor

HIG Sheets (detents, grabber); Material 3 bottom sheets; WCAG 2.2 · 2.1.2, 2.5.7 (swipe has button alternative).

Live — open the sheet
Ceramic pour-over set$48 · Free shipping

Options

<div class="cc-sheet open" role="dialog" aria-modal="true" aria-label="Options"> <div class="cc-sheet-grab" aria-hidden="true"></div> <h4>Options</h4> … </div>

Core · Overlay

Popover & Tooltip

Small truths anchored to the thing they describe. Tooltips name, popovers act — neither ever holds information that exists nowhere else.

When to use

Tooltip: naming icon controls on hover/focus, desktop only. Popover: a short menu of contextual actions anchored to a "…" trigger.

When not to

Tooltips don't exist on touch — anything a phone user needs cannot live in one. Errors never go in tooltips. A popover taller than half the screen is a sheet with the wrong name.

Behaviour & accessibility contract

Tooltips appear on focus as well as hover, are dismissible with Esc, and stay hoverable (WCAG 1.4.13). Popover menus use the ARIA menu pattern with arrow-key navigation.

Platform anchor

WCAG 2.2 · 1.4.13 content on hover or focus; WAI-ARIA menu & tooltip patterns; HIG Popovers.

Live — hover the icon, click the dots
Download
<span class="cc-tipwrap"> <button aria-label="Download">…</button> <span class="cc-tip" role="tooltip">Download</span> </span>

Core · Feedback

Progress & Loading

Honesty about time. Determinate when the end is known, a spinner only when it's short, and a skeleton when there's a shape worth promising.

When to use

Bars for uploads and multi-step flows; rings for compact contexts; spinners for sub-2-second waits. Anything longer than ten seconds needs a cancel.

When not to

Never fake progress — a bar that crawls to 90% and parks is a lie the user physically feels. Don't spinner the whole page for one widget's fetch.

Behaviour & accessibility contract

role="progressbar" with aria-valuenow; long operations announce completion via a live region. Spinners respect reduced motion by slowing, not vanishing.

Platform anchor

HIG Progress indicators; Material 3 progress; WCAG 2.2 · 4.1.3 status messages.

Live — advance it
<div class="cc-progress" role="progressbar" aria-valuenow="35" aria-valuemin="0" aria-valuemax="100"><span style="width:35%"></span></div>

Core · Feedback

Skeleton

A promise of shape. The screen commits to its layout before the data arrives, so loading feels like focus resolving rather than construction work.

When to use

Content with a predictable silhouette — feed items, cards, profiles — loading in 0.5–5 seconds. Skeleton geometry must match the real content, or the layout shift double-charges the wait.

When not to

Not for instant loads (flash of skeleton is worse than nothing) and not for unpredictable content where a spinner is more honest.

Behaviour & accessibility contract

The container carries aria-busy="true" and a text status ("Loading feed"); the shimmer is decorative, aria-hidden, and static under reduced motion.

Platform anchor

Material loading guidance; WCAG 2.2 · 2.3.3 animation from interactions; Core Web Vitals CLS discipline.

Skeleton vs the card it becomes

Alpine mornings

Twelve cabins above the fog line, from $89 a night.

<div class="cc-card" role="status" aria-busy="true" aria-label="Loading"> <div class="cc-skel" style="height:140px"></div> </div> /* the skeleton is the shape of the answer, so nothing jumps when the answer lands */ @media (prefers-reduced-motion: reduce) { .cc-skel::after { animation: none } }

Core · Navigation-in-place

Pagination & Dots

Numbered pages for destinations you'll cite and return to; dots for carousels you'll drift through. Both tell you where you are — that's the entire job.

When to use

Pagination where position matters — search results, order history. Page dots for short swipeable sets (≤6 slides), always paired with swipe.

When not to

Feeds don't paginate (see Infinite Scroll pattern for the trade). Dots beyond six are noise — show a count ("3 / 12") instead.

Behaviour & accessibility contract

nav landmark with aria-label="Pagination"; the current page has aria-current="page". Dots are buttons with slide names, not empty spans.

Platform anchor

HIG Page controls; WCAG 2.2 · 2.4.8 location, 4.1.2 name-role-value.

Live
<nav class="cc-pages" aria-label="Pagination"> <button class="cc-page active" aria-current="page">1</button> <button class="cc-page">2</button> </nav>

Core · Display

Accordion

Progressive disclosure on native semantics. Built on <details>, so it works with JavaScript disabled, and searchable-in-page in modern browsers.

When to use

FAQs, size guides, shipping details — secondary content most people skip and some people need. Multiple panels may be open; users manage their own attention.

When not to

Never hide critical-path content (price, total, cancellation terms) behind a fold. Auto-closing siblings punishes comparison — leave panels where users put them.

Behaviour & accessibility contract

<summary> is natively focusable and toggles with Enter/Space; the chevron flips as a state signal in addition to the disclosure itself.

Platform anchor

WAI-ARIA disclosure pattern (native <details>); WCAG 2.2 · 4.1.2; HIG disclosure controls.

Live
What's your return policy?
Free returns within 30 days, in any condition. Refunds land in 3–5 business days on the original payment method.
Do you ship internationally?
To 42 countries. Duties are shown at checkout — the price you see is the price you pay.
How do I change my order?
Within an hour of ordering, edit anything from Orders → Edit. After that, chat with us and we'll catch it before it ships.
<details> <summary>What's your return policy?</summary> <div class="cc-acc-body">Free returns within 30 days…</div> </details>

Core · State

Empty State

The first screen most new users actually see. An empty state is onboarding wearing casual clothes — it names the value and hands over the first move.

When to use

First-run, cleared inbox, no results. Formula: what this space is for, why it's empty, one action that fills it. Zero-results always keeps the query editable and suggests a loosened filter.

When not to

Never a bare "No data". Never a dead end after a search — that's where shoppers leave. Don't guilt-trip ("It's lonely in here…") — charm, don't manipulate.

Behaviour & accessibility contract

The heading is a real heading in the page outline; the illustration is decorative and aria-hidden; the action is a first-class button, not a text link whisper.

Platform anchor

Material empty state guidance; NN/g empty-state conventions; WCAG 2.2 · 2.4.6 headings and labels.

First-run

Nothing saved yet

Tap the heart on anything you like and it lands here — synced across your devices.

<div class="cc-empty"> <span class="cc-empty-ico" aria-hidden="true">…</span> <h4>Nothing saved yet</h4> <p>Tap the heart on anything you like…</p> <button class="cc-btn cc-btn-primary">Start exploring</button> </div>

Core · Feedback

Banner & Inline Alert

Persistent context that waits. Where a toast taps you on the shoulder and leaves, a banner sits down until the situation changes.

When to use

Conditions that remain true: payment method expiring, feature in beta, order delayed. Inline alerts attach to the thing they describe — a form error banner lives above the form it indicts.

When not to

Not for one-off confirmations (toast) and not stacked three deep — if everything is urgent, nothing is. Dismissible only if the condition is genuinely ignorable.

Behaviour & accessibility contract

Errors use role="alert" (assertive), the rest role="status". Icon + word + color triple-encode severity; the dismiss button names what it dismisses.

Platform anchor

Material 2 banners — dropped in Material 3 and never replaced, which is why this entry specifies one; WCAG 2.2 · 4.1.3 status messages, 1.4.1 use of colour.

Severities
Heads up: prices now include import duties — what you see is what you pay.
Order confirmed. A receipt is on its way to june.k@example.com.
Card expiring: your Visa ···· 4242 expires this month. Update payment
<div class="cc-banner cc-banner--err" role="alert"> <i class="fas fa-circle-exclamation" aria-hidden="true"></i> <span><b>Payment failed.</b> Your bank declined the charge — no money moved.</span> </div>

Navigation

Navigation Bar

Glass over content: the bar defers to the page scrolling beneath it, and the active pill answers "where am I" without a map.

When to use

Top-level destinations on wide screens, five or fewer. Wordmark left, destinations right, one active at a time. On mobile these same destinations become the tab bar.

When not to

A nav bar is not a junk drawer — utilities, legal links, and settings live elsewhere. Never hide primary navigation behind a hamburger on desktop when there is room to show it.

Behaviour & accessibility contract

A <nav> landmark with aria-current="page" on the active item; a skip-to-content link precedes it. The glass carries a solid fallback for non-supporting browsers.

Platform anchor

HIG Navigation bars; WCAG 2.2 · 2.4.1 bypass blocks, 2.4.8 location.

Glass nav
<header class="cc-navbar"> <b>lumen</b> <nav aria-label="Primary"> <a href="/" class="active" aria-current="page">Home</a> <a href="/explore">Explore</a> </nav> </header>

Navigation

Tab Bar

Five doors at the bottom of the world. The tab bar is the most valuable real estate in a consumer app — every slot must earn daily use.

When to use

3–5 top-level, equally important destinations on mobile. Filled icon marks the active tab; each tab keeps its own navigation stack so switching never loses your place.

When not to

Never as an action bar — "Post" in slot three that opens a composer instead of a destination breaks the spatial model (if you do it, make it visually distinct and modal). Two tabs is a segmented control; six is a redesign.

Behaviour & accessibility contract

Labels always visible — icon-only tab bars fail recognition tests everywhere they've been tried. Badges carry accessible counts. Safe-area padding is part of the component.

Platform anchor

HIG Tab bars; Material 3 navigation bar (label conventions); WCAG 2.2 · 2.4.8.

Live — tap a tab
<nav class="cc-tabbar" aria-label="Primary"> <button class="active" aria-current="page"><i class="fas fa-house"></i>Home</button> <button><i class="far fa-compass"></i>Explore</button> </nav>

Navigation

Drawer & Side Menu

The overflow home for secondary destinations — account, orders, help, legal. A drawer is where things go so the tab bar doesn't have to lie about their importance.

When to use

Secondary and account-level destinations, or the primary nav of dense multi-section products on tablet and desktop. Grouped, labelled, with the user's identity anchored at top.

When not to

Not as the only path to daily-use destinations on a phone — burying "Search" in a hamburger costs engagement measurably. Don't mirror the tab bar inside the drawer; duplicates confuse the map.

Behaviour & accessibility contract

Opens as a modal region on mobile (focus trapped, Esc closes, scrim tap closes); on desktop it's a persistent nav landmark. The trigger announces expanded state.

Platform anchor

Material 3 navigation drawer; WCAG 2.2 · 2.1.2 no keyboard trap, 4.1.2.

Drawer anatomy (persistent form)
JK
June KimSee your profile
<nav class="cc-list" aria-label="Account"> <a class="cc-cell" href="/orders">…Orders…</a> <a class="cc-cell" href="/payments">…Payments…</a> </nav>

Navigation

Breadcrumb

The paper trail through a deep catalog. Mostly a desktop commerce citizen — on phones the back button carries this weight.

When to use

Hierarchies three levels or deeper — Home → Women → Shoes → Sneakers. Every crumb but the last is a real link; the last names the current page and links nowhere.

When not to

Not on flat products, not on mobile where it wraps into noise, and never as a step indicator in checkout — that's a progress stepper with different semantics.

Behaviour & accessibility contract

nav[aria-label="Breadcrumb"] with an ordered list; the current page carries aria-current="page". Separators are decorative and hidden from screen readers.

Platform anchor

WAI-ARIA breadcrumb pattern; schema.org BreadcrumbList (SEO); WCAG 2.2 · 2.4.8.

Live
<nav class="cc-crumb" aria-label="Breadcrumb"> <a href="/">Home</a> › <a href="/women">Women</a> › <span aria-current="page">Sneakers</span> </nav>

Navigation

Floating Action Button

One verb, promoted above the page. The FAB works when the screen has an obvious main thing to do — compose, add, capture — and fails everywhere else.

When to use

The single most common creative action of a screen: new message, add item, start workout. One per screen, thumb corner, persistent through scroll (or politely shrinking).

When not to

No FAB for destructive, rare, or ambiguous actions. Two FABs is zero FABs. If the action belongs to one list item rather than the screen, it belongs on the item.

Behaviour & accessibility contract

56px target with a text label for screen readers ("Compose message"); never overlaps the last list item — scroll containers pad their bottom by the FAB's height.

Platform anchor

Material 3 FAB; WCAG 2.2 · 2.5.8 target size, 2.4.7 focus visible.

Live
<button class="cc-fab" aria-label="Compose message"> <i class="fas fa-plus" aria-hidden="true"></i> </button>

Navigation

Large Title Header

The title starts as a headline and condenses into chrome as you scroll — orientation you feel rather than read. Scroll the demo.

When to use

Root screens of each tab — the moment of arrival. The large title gives the screen a name worth a headline; the condensed bar keeps it while content takes over.

When not to

Not on detail screens deep in a stack (content should lead) and not with titles longer than two words — "Order history & receipts" was never meant to be 34px.

Behaviour & accessibility contract

The h1 exists once (the large form); the condensed bar mirrors it aria-hidden. The collapse is scroll-driven positioning, not animation — reduced motion changes nothing because nothing plays.

Platform anchor

HIG Navigation bars (large titles); WCAG 2.2 · 1.3.1 heading structure.

Live — scroll inside the frame
Library
Saved places · 24
Playlists · 12
Downloads · 8
History
Purchases
Reviews you wrote
Following · 43
Archived
<div class="cc-lt-scroll" tabindex="0" role="region" aria-label="Library, scrollable"> <div class="cc-lt-bar" aria-hidden="true">Library</div> <!-- the collapsed copy --> <h1 class="cc-lt-big">Library</h1> <!-- the real heading --> </div> /* one heading in the accessibility tree, two on screen — the bar is decoration */

Onboarding & Identity

Welcome Flow

Three screens, one promise each, and a skip that actually works. The best onboarding is mostly the product, sooner.

When to use

First launch only, ≤3 screens, each selling one outcome (not one feature). Progress dots show the end is near; the last screen lands on the primary action.

When not to

Never gate the product behind a tour a user can't skip. Don't ask for permissions here (see Permission Primer) and don't ask for registration before showing any value.

Behaviour & accessibility contract

Skip is visible from screen one, tab-reachable first. Swipe advances but buttons exist for every gesture (WCAG 2.5.7). Seen-state persists — nobody sees the tour twice.

Platform anchor

HIG Onboarding ("get to the app quickly"); Material onboarding guidance; WCAG 2.2 · 2.5.7 dragging movements.

Live — advance the flow

Find your next favorite

Recommendations that learn what you love — not what advertisers wish you loved.

<section class="cc-welcome" aria-roledescription="carousel" aria-label="Welcome"> <h3>Find your next favorite</h3> <p>Recommendations that learn what you love…</p> <button class="cc-btn cc-btn-primary">Continue</button> <button class="cc-btn cc-btn-ghost">Skip</button> </section>

Onboarding & Identity

Sign-in & SSO

The fewest keystrokes between a person and their account. Platform sign-in first, email as the dignified fallback, passwords as the last resort they've become.

When to use

Offer the platform's native sign-in alongside any third-party login, then email. Order by user benefit, not by your data appetite.

When not to

Don't demand an account before demonstrating value — browse first, sign in to keep. Never dark-pattern the email option into 8px grey text to juice SSO adoption.

Behaviour & accessibility contract

Buttons follow each provider's brand rules (their guidelines are contractual); autocomplete="email" and "current-password" keep managers working; errors never say which of email/password was wrong.

Platform anchor

App Store Review Guideline 4.8 (offering an equivalent privacy-respecting login option); provider brand guidelines; WCAG 2.2 · 3.3.8 accessible authentication.

The stack
or
<div class="cc-sso"> <button class="cc-sso-dark">Continue with Apple</button> <button>Continue with Google</button> <div class="cc-sso-div">or</div> <button>Continue with email</button> </div>

Onboarding & Identity

One-Time Code

Six boxes and thirty anxious seconds. Everything about this component exists to make the code arrive, paste, and verify itself.

When to use

Verifying possession of an email or phone. Auto-advance per digit, paste fills all six, auto-submit on the last — the user should never hunt for a Verify button.

When not to

Don't use OTP as the only factor for high-value changes (SIM-swap is real); pair with passkeys where it matters. Resend appears after a countdown, not never.

Behaviour & accessibility contract

autocomplete="one-time-code" lets the OS offer the code from SMS; inputs are labelled "Digit 1 of 6"; WCAG 3.3.8: no transcription puzzle — paste always works.

Platform anchor

WCAG 2.2 · 3.3.8 accessible authentication (minimum); HIG security code AutoFill conventions.

Live — type or paste digits
<div role="group" aria-label="Enter 6 digit code" class="cc-otp"> <input inputmode="numeric" maxlength="1" autocomplete="one-time-code" aria-label="Digit 1 of 6"> … </div>

Onboarding & Identity

Passkey Prompt

The end of passwords, sold in one card. The design job is trust: say what a passkey is in one sentence, and make "not now" a real choice.

When to use

Right after a successful password or OTP sign-in — the moment the user has just felt the pain the passkey removes. One sentence of what, one of where it syncs.

When not to

Not during first-run before any sign-in exists, and never as a repeating nag — decline twice, stay quiet until the user visits security settings themselves.

Behaviour & accessibility contract

"Not now" is same-size, adjacent, honest. The biometric icon row is decorative; the copy never claims biometrics leave the device — because they don't.

Platform anchor

FIDO2 / WebAuthn UX conventions; HIG passkeys guidance; WCAG 2.2 · 3.3.8 (passkeys are the compliant path).

The card

Sign in with your face or fingerprint

A passkey replaces your password with the way you already unlock this device. It syncs through your device account and never leaves your hardware unencrypted.

<div class="cc-passkey" role="dialog" aria-labelledby="pk-t"> <h4 id="pk-t">Sign in with your face or fingerprint</h4> <p>A passkey replaces your password…</p> <button class="cc-btn cc-btn-primary">Create a passkey</button> <button class="cc-btn cc-btn-ghost">Not now</button> </div>

Onboarding & Identity

Profile Setup

Ask little, explain why, skip everything. Every optional field completed here is a gift the user gave you — design like you know it.

When to use

After account creation, one screen, only what the product needs on day one. Each request carries its reason inline ("your name appears on reviews").

When not to

No birthday "for your special offer", no gender without a product reason, no phone number as a growth hook. Data you can't justify inline is data you shouldn't collect.

Behaviour & accessibility contract

Everything after the display name is skippable and says so. Photo upload has a keyboard path and never requires a camera. Progress persists — abandoning costs nothing.

Platform anchor

GDPR Art 5(1)(c) data minimisation; App Store Guideline 5.1.1 (don't require unneeded personal data); WCAG 2.2 · 3.3.2.

One honest screen
<label class="cc-label" for="city">City <span>· optional</span></label> <input class="cc-input" id="city" autocomplete="address-level2" placeholder="For local recommendations"> <span class="cc-help">Only ever shown as a region, never an address.</span>

Onboarding & Identity

Permission Primer

You get one shot at the OS permission dialog. The primer spends its own screen to make sure you take that shot at the moment the user already wants to say yes.

When to use

Immediately before a system permission request, triggered by a user action that needs it — asking for location when they tap "Find stores near me", not at launch.

When not to

Never chain three permission asks at first launch. Never re-prompt after a decline with the same pitch — respect the no, offer settings later when the feature is touched again.

Behaviour & accessibility contract

The primer states exactly what is accessed, when, and what saying no costs. "Not now" keeps the app fully usable — degraded gracefully, never punitively.

Platform anchor

HIG "request permission at the moment of need"; Play policy on prominent disclosure; GDPR Art 5(1)(b) purpose limitation, with Art 6 supplying the lawful basis that purpose is bound to.

The primer

See stores near you?

We use your location only while you're using the app to sort stores by distance. It's never stored or shared.

<div class="cc-perm" role="dialog" aria-labelledby="perm-t"> <h4 id="perm-t">See stores near you?</h4> <p>We use your location only while you're using the app…</p> <button class="cc-btn cc-btn-primary">Allow while using</button> <button class="cc-btn cc-btn-ghost">Not now</button> </div>

Onboarding & Identity

Paywall & Plans

The most scrutinised screen in any subscription app — by users, by app review, by the FTC. This one converts by being legible: real prices, real terms, restore always visible.

When to use

At the moment of premium intent — tapping a locked feature — not as an ambush at launch. Plans show total billed price and period; the trial states when it converts and how to cancel.

When not to

No fake countdown timers, no pre-selected annual masquerading as monthly pricing, no hidden ✕. "Free trial" that means "we bill you in 72 hours unless you remember" gets rejected — by review and by users.

Behaviour & accessibility contract

Close is visible from render. Price per period appears exactly as billed ("$59.99/year", not "$4.99/mo billed annually" alone). Restore purchases and terms are reachable without scrolling tricks.

Platform anchor

App Store Review Guideline 3.1.2 (subscription clarity) & Schedule 2 rules; Play subscriptions policy; ROSCA 15 U.S.C. §8403 (clear disclosure before billing) — the surviving 16 CFR Part 425 reaches only prenotification plans, and the 2024 amendments that would have covered app subscriptions were vacated in July 2025.

Live — pick a plan

Listen without limits

Offline downloads · lossless audio · no ads

Billed to your store account. Cancel in Settings → Subscriptions in two taps. Restore purchases · Terms

<label class="cc-plan"> <input type="radio" name="plan"> <div><b>Yearly</b><span>Two months free vs monthly</span></div> <span class="cc-plan-price">$99.99/yr</span> <!-- the billed amount, always --> </label>

Commerce

Product Card

The unit of browsing. Image leads, price never hides, and the save affordance floats where thumbs already are.

When to use

Grid and carousel browsing. Image, name, one line of context, price, rating — nothing else survives the cut. The card links to detail; only save acts inline.

When not to

Never bury the price to "get them to detail" — hidden prices read as expensive. No fake "3 left!" scarcity unless inventory is genuinely tracked and true.

Behaviour & accessibility contract

One link per card with a name that includes the product ("Ceramic pour-over set, $48"); the wishlist button is a separate labelled target announcing its toggled state.

Platform anchor

WCAG 2.2 · 2.4.4 link purpose; FTC truth-in-advertising (scarcity claims must be true); schema.org Product markup.

Live — tap the heart

Ceramic pour-over set

Hand-thrown · 4 colors

$48$6425% off
4.0 · 212

Trail runners

Recycled knit · true to size

$129
4.9 · 1.2k
<article class="cc-product"> <div class="cc-product-media"> <img src="…" alt=""> <button class="cc-product-wish" aria-pressed="false" aria-label="Save Ceramic pour-over set">♡</button> </div> <div class="cc-product-body"> <h4><a href="/p/…">Ceramic pour-over set</a></h4> <div class="cc-price"><b>$48</b><s>$64</s></div> </div> </article>

Commerce

Price Display

Tabular, truthful, and complete. A price is a fact with typography — strikethroughs need a real former price, and the number you show is the number they pay.

When to use

Tabular numerals everywhere money appears so digits align in lists. Compare-at prices show savings as both amount and percentage; unit prices accompany multi-size goods.

When not to

No drip pricing — fees revealed at the last step are the single most-cited dark pattern in FTC enforcement. No fictitious strikethrough anchors that were never charged.

Behaviour & accessibility contract

Strikethrough prices carry accessible context ("was $64, now $48") — <s> alone is silent on most screen readers. Currency symbols travel with the number, locale-formatted.

Platform anchor

FTC Guides Against Deceptive Pricing (16 CFR 233); EU Price Indication Directive (prior-price rules); WCAG 2.2 · 1.3.1.

Scales & honesty
$1,299or $54.13/mo for 24 mo
$48Save $16 (25%)
$3.49$0.29 / oz
<div class="cc-price"> <b>$48</b> <s aria-hidden="true">$64</s> <span class="visually-hidden">was $64, now $48</span> <span class="cc-price-off">Save $16 (25%)</span> </div>

Commerce

Cart Line Item

Where second thoughts happen. Every line item makes changing your mind cheap — adjust, save for later, remove — because a trapped cart is an abandoned cart.

When to use

Cart and order review. Thumbnail, name, variant, unit price, quantity stepper, line total. Remove is one tap plus an undo toast — not a confirm dialog.

When not to

Don't inject upsells between line items — cross-sell lives below the list. Never auto-add items (warranty, insurance) the user must notice and remove; that's an FTC unfairness case with a design file.

Behaviour & accessibility contract

Quantity changes announce the new line total politely. Remove names its item ("Remove trail runners"). Price changes since adding are flagged, not silently swapped.

Platform anchor

FTC Section 5 unfairness (pre-checked add-ons); WCAG 2.2 · 4.1.3; Baymard cart usability conventions.

Live stepper
Ceramic pour-over setMatte white
1
$48
<div class="cc-cartitem"> <img class="cc-cartitem-thumb" src="…" alt=""> <div class="cc-cartitem-main"><b>Ceramic pour-over set</b><span>Matte white</span></div> <button aria-label="Remove Ceramic pour-over set">Remove</button> </div>

Commerce

Checkout Summary

No math surprises. Every fee has a name, the total is the largest number on screen, and it matches the charge to the cent.

When to use

The right rail (desktop) or pre-pay screen (mobile) of checkout. Subtotal, shipping, tax, discounts, total — each row aligned, tabular, updated live as options change.

When not to

Never a "service fee" that appears only here — junk fees at the summary stage are the FTC's current enforcement centerpiece. Never collapse the breakdown behind a tap on mobile.

Behaviour & accessibility contract

Live updates announce via a polite region ("Total updated: $53.24"). The pay button repeats the total — the last thing read before commitment is the number.

Platform anchor

FTC Rule on Unfair or Deceptive Fees, 16 CFR Part 464, in force since 12 May 2025 — its own scope is live-event tickets and short-term lodging, so all-in pricing is applied here as the honest generalisation of it rather than as a claim that it binds every checkout; EU Consumer Rights Directive Art 6 (total cost pre-order); WCAG 2.2 · 4.1.3.

The summary
Subtotal (2 items)$177.00
Shipping — standardFree
Estimated tax$14.60
Promo — WELCOME10−$17.70
Total$173.90
<div class="cc-summary" aria-live="polite"> <div class="cc-summary-row"><span>Subtotal</span><output>$177.00</output></div> <div class="cc-summary-row cc-summary-row--total"><span>Total</span><output>$173.90</output></div> </div> <button class="cc-btn cc-btn-primary">Pay $173.90</button>

Commerce

Payment Method

Radio rows for money. Wallets first (they convert best and typo least), saved cards next, new card last — and the selected row is unmistakable.

When to use

Checkout and settings. Each method shows its brand, its last four, and its expiry state. One is always selected; the default is the user's last successful method.

When not to

Never preselect a store credit line or BNPL the user didn't choose — payment defaults are a consent surface. Expired cards can't be selected, only fixed.

Behaviour & accessibility contract

A real radio group: label reads "Visa ending 4242, expires 12/27". Adding a card uses autocomplete cc-attributes and never blocks paste.

Platform anchor

PCI DSS v4.0 Req 3.4.1 — the standard permits BIN plus last four, so showing last four only is this system's own stricter rule rather than the requirement; card-network brand guidelines; WCAG 2.2 · 1.3.5 identify input purpose and 3.3.2 labels or instructions. (3.3.8 governs authentication steps; entering a card number is not one.)

Live — pick one
<label class="cc-payrow"> <input type="radio" name="pay" class="visually-hidden"> <b>Visa ending 4242</b><span>expires 12/27</span> </label>

Commerce

Promo Code

A tiny form with outsized feelings. Success celebrates with the exact amount saved; failure explains without blaming.

When to use

Checkout, collapsed behind "Have a code?" so full-price buyers aren't invited to leave and hunt coupons. Applies without page reload; the summary updates in the same breath.

When not to

Don't auto-apply a worse code over a better one. Expired codes say expired — "invalid code" for a code that worked yesterday teaches distrust.

Behaviour & accessibility contract

Uppercases as-you-type (display only), trims whitespace on paste, result announced via status region. The applied state shows the code and its effect together, removable in one tap.

Platform anchor

WCAG 2.2 · 3.3.1, 3.3.3 error suggestion, 4.1.3; FTC advertised-discount truthfulness.

Live — try WELCOME10 (or anything else)
<div class="cc-promo"> <input class="cc-input" aria-label="Promo code" autocapitalize="characters"> <button class="cc-btn cc-btn-secondary">Apply</button> </div> <div role="status">WELCOME10 applied — you saved $17.70</div>

Commerce

Ratings & Reviews

Social proof with a chain of custody. Stars summarise, distributions tell the truth, and "verified purchase" means the system checked.

When to use

Star input for collection (tap or drag, 1–5), star display plus count everywhere the product appears. Reviews sort by helpfulness with recency available.

When not to

Suppressing or reordering negative reviews violates the FTC's review rules — display practices are a legal surface now. No incentivised reviews without disclosure.

Behaviour & accessibility contract

Display stars carry text ("4.6 out of 5, 1,942 ratings"); the input is keyboard-operable radios styled as stars. Half-star precision displays, never collects.

Platform anchor

FTC Rule on Consumer Reviews and Testimonials (16 CFR 465); Consumer Review Fairness Act; WCAG 2.2 · 1.3.1.

Live — rate it
MR
Maya R. Verified purchaseMarch 2026 · size M

Survived a week of rain in Portland and still looks new. Runs slightly large — I sized down and it's perfect.

<div class="cc-stars" role="img" aria-label="Rated 4.6 out of 5, 1,942 ratings"> ★★★★☆ <span>4.6 · 1,942</span> </div> <span class="cc-badge cc-badge--ok">Verified purchase</span>

Commerce

Save & Wishlist

The lowest-commitment yes in commerce. A save is a bookmark on desire — instant, reversible, synced, and never guilt-tripped.

When to use

A heart on every product surface, toggling instantly with optimistic UI and a toast pointing at the list. Saved items remember price at save time so drops can be shown truthfully.

When not to

Saving must not require sign-in to feel instant — save locally, merge on auth. Never spam "items in your wishlist are selling fast!" pushes without explicit opt-in to that exact category.

Behaviour & accessibility contract

The heart is a toggle button with aria-pressed and a name including the product; the fill change pairs with the pressed state so color isn't the only signal.

Platform anchor

WCAG 2.2 · 4.1.2 name-role-value; GDPR-conscious local-first storage; HIG feedback conventions.

Live — toggle saves
Pour-over setSaved at $48 · now $44 ↓ $4
Trail runnersSaved at $129 · unchanged
<button class="cc-btn cc-btn-outline" aria-pressed="false" aria-label="Save Trail runners">♡ Save</button> <!-- price at save time enables honest "↓ $4 since you saved it" -->

Commerce

Order Tracking

The screen people open eleven times for one package. Its whole job is a truthful answer to "where is it and when" — everything else is decoration.

When to use

Post-purchase. Timeline of real states with timestamps, current state emphasized, delivery window as a range that narrows honestly as confidence grows.

When not to

No fake precision — "arriving 2:14 pm" from a model that knows ±3 hours erodes every future promise. A stuck state acknowledges being stuck and offers support before the user hunts for it.

Behaviour & accessibility contract

The timeline is an ordered list; each step names its state and time. Status color is tripled with icons and text. Updates announce politely if the screen is open.

Platform anchor

FTC Mail Order Rule (shipment timing representations); WCAG 2.2 · 1.3.1, 1.4.1.

The timeline
Order confirmedTue, Jul 7 · 9:41 am
PackedWed, Jul 8 · 2:03 pm
In transitLeft the regional facility · Oakland, CA
Out for deliveryExpected Fri, Jul 10 · 10 am – 2 pm
<ol class="cc-track"> <li class="cc-track-step done"><b>Order confirmed</b><span>Tue, Jul 7</span></li> <li class="cc-track-step now" aria-current="step"><b>In transit</b>…</li> </ol>

Social

Feed Post

The atom of the social web: who, what, and three verbs. The layout is a contract with the reader — authorship always above content, actions always below it.

When to use

Any authored unit in a scrolling feed. Avatar + name + time anchor trust; the action row keeps to three or four verbs with live counts.

When not to

Sponsored posts wear the same anatomy plus an unmissable "Sponsored" label — camouflaged ads violate FTC endorsement rules and reader trust in the same move.

Behaviour & accessibility contract

The post is an article with the author as its heading; action counts are part of each button's name ("Like, 1,204 likes"). Media carries author-provided alt text — the composer asks for it.

Platform anchor

FTC Endorsement Guides (ad disclosure); WCAG 2.2 · 1.1.1, 4.1.2; EU DSA transparency for recommender systems.

Live — like it
AR
Ana Reyes@anaruns · 2h
Sunrise loop before the fog burned off. The city gets quiet exactly once a day and it's worth the alarm.
<article class="cc-post"> <header class="cc-post-head">…author…</header> <div class="cc-post-body">…</div> <img alt="Runner's view of a foggy sunrise over the bay"> <div class="cc-post-actions"> <button aria-pressed="false" aria-label="Like, 1,204 likes">♡ 1,204</button> </div> </article>

Social

Comment Thread

Conversation with indentation as grammar. One level of nesting reads as reply; two reads as argument; three reads as nothing at all — so this system stops at one.

When to use

Discussion under any post or product. Bubble carries author + text; the meta row underneath holds time, like, reply. Deeper threads collapse behind "View 12 replies".

When not to

Don't autoplay into hostile territory — sort by "most liked" hides brigading better than "newest". Don't let the reply box push content; it stays docked.

Behaviour & accessibility contract

Semantic list structure preserves thread relationships for screen readers ("reply to Ana"). Collapsed counts are buttons, not links-that-lie. Report is reachable on every comment.

Platform anchor

WCAG 2.2 · 1.3.1; EU DSA Art 16 (notice mechanisms adjacent to content); community-safety conventions.

Thread
TK
Tomas K.

That stretch past the pier is my favorite mile in the city.

2h
AR
Ana Reyes

Right? Empty at 6am, chaos by 9.

1h
<ul class="cc-thread"> <li class="cc-comment">… <ul><li class="cc-comment cc-comment--reply">…</li></ul> </li> </ul>

Social

Reaction Bar

Six feelings on a glass shelf. The long-press reveal is one of the small delights consumer software is allowed — spent here because reacting is frequent, safe, and reversible.

When to use

Quick emotional response on posts and messages. Five or six options maximum — the palette is a vocabulary and vocabularies need to be memorisable.

When not to

Not for decisions or feedback with consequences (that's a survey or report). Don't A/B swap the emoji set weekly; meanings accrete through stability.

Behaviour & accessibility contract

Long-press has a click/keyboard path (the bar opens from a labelled button too — WCAG 2.5.7). Each emoji button carries its name ("Love"). Selection announces and dismisses.

Platform anchor

WCAG 2.2 · 2.1.1 keyboard — long-press is neither a dragging movement (2.5.7) nor a path-based gesture (2.5.1), so no criterion compels an alternative and this system ships one anyway; WAI-ARIA toolbar pattern; HIG feedback conventions.

Live — pick one
React →
<div class="cc-react" role="toolbar" aria-label="React"> <button aria-label="Love">❤️</button> <button aria-label="Laugh">😂</button> </div>

Social

Share Sheet

Distribution's front door. People first, then platforms, then plumbing (copy link) — ordered by what the user does, not what growth wants.

When to use

Any content worth leaving the app. On mobile, prefer invoking the OS share sheet; this custom grid is for in-app targets (send to a friend, add to story) layered above it.

When not to

Don't pack the first row with your own re-engagement targets while burying "Copy link" — link-copying is the most used share on every platform that measures honestly.

Behaviour & accessibility contract

Grid is keyboard navigable; every target is named; "Copy link" confirms via toast and writes a clean URL — no fifteen tracking parameters that make the link unreadable.

Platform anchor

HIG activity views (share sheets); Web Share API conventions; WCAG 2.2 · 2.1.1.

Live — copy the link
<!-- prefer the OS sheet when available --> if (navigator.share) navigator.share({ title, url });

Social

Chat Bubbles

Two voices, two sides, zero ambiguity. Yours right and tinted, theirs left and neutral — a convention so strong that breaking it reads as a bug.

When to use

1:1 and small-group messaging. Consecutive messages group under one timestamp; delivery state (sent, delivered, read) lives once at the thread's end, not on every bubble.

When not to

Read receipts are consent-gated and mutual — showing "read" to senders while hiding the setting is a dark pattern. Support bots must be visibly non-human.

Behaviour & accessibility contract

The transcript is a log region; new messages announce without stealing focus. Bubble color carries ≥4.5:1 text; the typing indicator stills under reduced motion.

Platform anchor

WCAG 2.2 · 4.1.3, 1.4.3; EU AI Act transparency (bot disclosure); messaging privacy norms (GDPR).

The transcript
Landed! Where should I drop my bags?
Lockers on level 2, then meet me at the north exit ☕
Read 4:12 pm
<div role="log" aria-label="Conversation with Sam"> <div class="cc-msg cc-msg--in">Landed!…</div> <div class="cc-msg cc-msg--out">Lockers on level 2…</div> </div>

Social

Notification Item

A sentence with a face. Actor, verb, object, time — and unread state you can see from across the room. The list is an inbox, so it behaves like one.

When to use

The in-app notification center. Group by day, batch same-actor events ("Ana and 3 others liked…"), and deep-link to the exact object, never the home screen.

When not to

Marketing does not wear the notification costume — "Someone might have viewed your profile 👀" as engagement bait poisons the channel that ships real information.

Behaviour & accessibility contract

Unread is background + dot + bolded text (triple-encoded); items mark read on visit and support mark-all. Each row is one link with a complete sentence as its name.

Platform anchor

HIG Notifications ("valuable, not promotional"); Play FCM policy; WCAG 2.2 · 1.4.1.

Read & unread
AR

Ana Reyes and 3 others liked your photo.

12 min ago
TK

Tomas K. replied: "That stretch past the pier…"

1h ago
MR

Maya R. started following you.

Yesterday
<a class="cc-notif cc-notif--unread" href="/p/8421"> <p><b>Ana Reyes</b> and <b>3 others</b> liked your photo.</p> <time datetime="2026-07-30T14:02">12 min ago</time> </a> <!-- unread is a dot AND a weight, never colour alone; the row is one link -->

Social

Follow Button

The smallest social contract. Following is loud, followed is quiet — the state change is the entire design, so it must be unmistakable and reversible in one tap.

When to use

Profiles, suggestions, discovery cards. Unfollowed is filled and inviting; following turns quiet secondary with the label changed — never just a color swap.

When not to

No confirm dialog on unfollow (it's reversible; asking makes it feel surveilled). Never follow anyone automatically — imported contacts get suggestions, not silent edges.

Behaviour & accessibility contract

aria-pressed carries the state; the label text changes (Follow ↔ Following) so screen readers and grayscale users read the same truth. Optimistic update, quiet rollback on failure.

Platform anchor

WCAG 2.2 · 4.1.2, 1.4.1; platform anti-spam norms (no auto-follow).

Live — toggle it
<button class="cc-btn cc-btn-primary cc-follow" aria-pressed="false"> Follow </button> <!-- pressed: class stays, label becomes "Following", style goes quiet -->

Social

Story Ring

A gradient halo that means "new, and it expires." The seen state — gray, quiet — is just as much the design as the rainbow.

When to use

Ephemeral content rails. Ring = unseen, gray = seen, no ring = no story. Your own slot leads with an explicit + affordance for creation.

When not to

Don't reset seen-state to farm taps — users notice, and the ring stops meaning anything. Don't put non-expiring content in the expiring costume.

Behaviour & accessibility contract

Each story is a button: "Ana's story, unseen, 2 hours ago". Seen state is encoded by ring treatment plus order (unseen first), not color alone. The rail is keyboard-scrollable.

Platform anchor

WCAG 2.2 · 1.4.1, 2.1.1; ephemeral-content honesty conventions.

The rail
Your story
ARana
TKtomas
MRmaya
<button class="cc-story" aria-label="ana's story, unseen"> <span class="cc-story-ring"><span>AR</span></span>ana </button> /* seen state is a ring that dims AND a label change — a grey ring alone is invisible to anyone who cannot compare two rings side by side */

Media

Video Player Controls

Controls that get out of the way and come back when called. Everything essential within one thumb arc: play, scrub, captions, volume.

When to use

Any owned playback surface. Controls fade after 3s of playback, return on tap/hover/focus. The scrubber shows buffered vs played; ±10s skips beat frame-precise dragging on touch.

When not to

Never autoplay with sound — muted autoplay with visible captions is the honest compromise. Don't hide captions behind a settings submenu; CC is a first-class control.

Behaviour & accessibility contract

Full keyboard map (space, arrows, C for captions, F for fullscreen); controls are real buttons with names; fading controls never fade while focused (WCAG 2.1.1, 1.4.13).

Platform anchor

WCAG 2.2 · 1.2.2 captions, 2.2.2 pause/stop/hide; HIG playback; Media Session API conventions.

The control surface
4:32 / 12:04
<div class="cc-video-scrub" role="slider" tabindex="0" aria-label="Seek" aria-valuetext="4 minutes 32 seconds of 12 minutes">…</div> <button aria-label="Captions" aria-pressed="true">CC</button>

Media

Mini Player

Playback that follows you around the app without blocking it. A glass bar above the tab bar: art, title, play, next — and a tap expands to the full stage.

When to use

Continuous audio/video while browsing elsewhere. Persistent across navigation, dismissible by swipe with a real stop — dismissing pauses, it doesn't secretly keep playing.

When not to

Not for content the user hasn't started. It never covers the tab bar or the last row of content — layouts reserve its height when active.

Behaviour & accessibility contract

The bar is a region ("Now playing"); controls are named; the expand affordance is a real button as well as a tap surface. Swipe-dismiss has a button equivalent.

Platform anchor

HIG now-playing conventions; WCAG 2.2 · 2.5.7, 4.1.2.

The bar
Slow Motion SundayCloud Atlas Quartet
<div class="cc-mini" role="region" aria-label="Now playing"> <img class="cc-mini-art" src="…" alt=""> <div class="cc-mini-main"><b>Slow Motion Sunday</b><span>Cloud Atlas Quartet</span></div> <button aria-label="Pause">…</button> </div>

Media

Live Badge

Two words that must never lie: LIVE means now, and the pulse means the signal is real. The moment the stream ends, the badge dies with it.

When to use

Actually-live streams and events, on thumbnails and in players, paired with a viewer count when it's meaningful. Red, uppercase, pulsing dot — the convention is universal; keep it.

When not to

Never on premieres, reruns, or "live-style" drops — "LIVE" on recorded content is a trust forfeiture regulators call deceptive. Ended streams switch to "Streamed 2h ago" instantly.

Behaviour & accessibility contract

The badge is text, not just color ("LIVE"), announced as part of the title. The pulse is decorative and stops under reduced motion without losing the label.

Platform anchor

FTC deception standard (liveness claims); WCAG 2.2 · 1.4.1, 2.3.3.

States
LIVE STREAMED 2H AGO 12.4k watching
<span class="cc-live">LIVE</span> <!-- only when it is --> <span class="cc-live is-past">STREAMED 2H AGO</span> /* LIVE styling on recorded content is a deception claim, not a style choice — FTC deception standard; the lint in cds/components.json rejects it */

Media

Captions

Most video is watched silent; captions are the soundtrack most people actually use. High contrast, two lines, and never covering the faces they transcribe.

When to use

All published video. Default on when the device is muted. Position bottom-center, lifting above controls when they're visible.

When not to

Auto-generated captions without a correction path aren't accessibility, they're liability diffusion — label them auto-generated and let creators fix them.

Behaviour & accessibility contract

User-adjustable size and background opacity, honoring OS caption preferences where exposed. ≥4.5:1 against any video via scrim. Max two lines, ~42 chars each.

Platform anchor

WCAG 2.2 · 1.2.2 captions (prerecorded), 1.2.4 (live); FCC caption quality rules; CVAA.

The overlay
And that's when I realized the recipe was never about the bread.
<video><track kind="captions" srclang="en" label="English" default src="…"></video> /* burned-in text is not a caption: it cannot be resized, restyled or read aloud */ .cc-cap { background: rgba(0,0,0,.72); padding: .2em .5em; } /* 4.5:1 over any frame */ /* two lines maximum, positioned clear of the controls, never over a face */

Growth · dark-pattern-free by construction

Onboarding Checklist

Progress you can feel. The checklist turns "set up your account" from a lecture into a game the user is already winning — because the first item arrives pre-checked.

When to use

Activation journeys with 3–5 real steps toward the product's core value. The endowed-progress head start (one item done at signup) is honest: they did create the account.

When not to

Steps that serve you, not them — "enable notifications" and "invite 3 friends" are not setup, they're acquisition wearing a checklist. Completion never gates paid-for functionality.

Behaviour & accessibility contract

Progress announced on change ("2 of 4 complete"); dismissible for good once completed or explicitly hidden; each item deep-links to the actual task.

Platform anchor

Endowed progress research (Nunes & Drèze); WCAG 2.2 · 4.1.3; EU DSA Art 25 (no manipulative pressure).

Live — check items off
Get set up1 of 4
<div class="cc-checklist" aria-label="Setup progress"> <div class="cc-progress" role="progressbar" aria-valuenow="25">…</div> <button class="cc-checkitem done"><i>✓</i><span>Create your account</span></button> </div>

Growth · dark-pattern-free by construction

Streak & Achievement

Celebration without hostage-taking. A streak marks showing up; this one is designed so missing a day stings less than the habit is worth.

When to use

Genuine practice loops — learning, fitness, journaling — where consistency serves the user's own goal. Streak freezes and grace days are part of the component, not a paid add-on.

When not to

Not on consumption ("7-day shopping streak" serves nobody but the funnel). No guilt copy on breaks — "your streak is hurt 😢" notifications at 11pm are the pattern this entry exists to forbid.

Behaviour & accessibility contract

The count carries text ("14-day streak, 5 of 7 days this week"); flame color pairs with the number. Celebration animation is one-shot and reduced-motion-gated.

Platform anchor

EU DSA Art 25; FTC dark-pattern report (guilt-driven engagement); WCAG 2.2 · 2.3.3.

The card
14 daysCurrent streak
M
T
W
T
F
S
S

Life happens — you have 2 streak freezes, applied automatically.

<p class="cc-streak-note">Life happens — you have 2 streak freezes, applied automatically.</p> /* the freeze is the whole design: a streak that punishes one bad day is a loss-aversion mechanic, not an encouragement. Losing it must never cost money. */

Growth · dark-pattern-free by construction

Referral Card

Both sides of the deal on one card. Referrals work when the invitation is a gift, not an extraction — so the friend's reward leads.

When to use

After a moment of earned delight (first great order, milestone hit). State both rewards, the code, and the terms link — the whole deal, no asterisk archaeology.

When not to

Never auto-import and mass-message contacts; every invite is user-sent. No fake "3 friends joined!" social proof. Referral rewards that expire in 48 hours are pressure, not generosity.

Behaviour & accessibility contract

Copy confirms via toast and status region; the code is selectable text, not an image; share uses the OS sheet with a clean pre-filled message the user can edit.

Platform anchor

FTC endorsement rules (incentivised referrals disclose); CAN-SPAM / TCPA (user-initiated sends); WCAG 2.2 · 1.4.5.

Live — copy the code

Give $15, get $15

Friends get $15 off their first order. Once they order, you get $15 in credit. Full terms

JUNE-15
<div class="cc-refcode"> <output>JUNE-15</output> <button class="cc-btn cc-btn-primary">Copy</button> </div> <div role="status" class="visually-hidden">Code copied</div>

Growth · dark-pattern-free by construction

Push Pre-Prompt

Permission with a menu. Instead of "allow notifications?", the primer asks which — orders only, or the social noise too — and the OS prompt fires only after a real yes.

When to use

At the first moment a notification would have helped ("your order shipped" beats any launch-time ask). Category toggles first, OS prompt second.

When not to

Never at first launch before value exists. Never re-prompt on a schedule. And the category promises are binding — routing marketing through the "order updates" channel is the lie that gets apps muted.

Behaviour & accessibility contract

"Not now" is prominent and final until the user visits settings. Chosen categories map to real notification channels the OS exposes (Android) or the app respects (iOS).

Platform anchor

HIG asking permission; Android notification channels; Play FCM policy; ePrivacy (marketing push is consent-based).

The primer
<!-- categories map to real channels; "Recommendations" labelled honestly as marketing --> <div class="cc-consent-row"> <div><b>Recommendations</b><span>Occasional picks. Honestly, marketing.</span></div> <label class="cc-switch"><input type="checkbox">…</label> </div>

Growth · dark-pattern-free by construction

In-App Review Prompt

Ask happy people, once, at the right moment — and never intercept the unhappy into a private complaint box while herding fans to the store. That fork is now a violation, not a growth hack.

When to use

After a success the user caused (finished workout, delivered order), rate-limited far below the OS caps. Use the platform's native review API — it handles the UI and the throttling.

When not to

No sentiment-gating ("Enjoying the app?" → happy to store, unhappy to a form). The FTC's review rule bans review suppression; Apple and Google both prohibit review gating explicitly.

Behaviour & accessibility contract

The prompt is dismissible, never repeats within the cooldown, and never blocks a task in progress. Feedback and rating are separate, both always reachable from settings.

Platform anchor

FTC 16 CFR 465.7 (review suppression) — 465.4 is the separate offence of buying reviews; App Store Guideline 3.2.2(x) (no coercing or incentivising ratings) with the SKStoreReviewController prompt limit; Play in-app review API rules.

The honest version

Enjoying it? Tell the store.

A rating takes two taps and genuinely helps. Having trouble instead? Talk to support — both paths are right here, no quiz first.

<p>Enjoying it? … Having trouble instead? <a href="/support">Talk to support</a></p> <!-- both doors on one screen. The "are you happy?" pre-screen that routes only happy users to the store is review gating — App Store Review 1.1.7 --> // once per user, after real use, never mid-task, never twice

Growth · dark-pattern-free by construction

Survey & NPS

One question, honestly asked, instantly dismissible. Survey fatigue is a design failure — this component is rationed like the interruption it is.

When to use

A single 0–10 or emoji-scale question after a completed experience, inline or as a quiet card — never a modal over a task. Follow-up text is optional and says so.

When not to

Not during first-run (nothing to rate yet), not after failures (measure that from support volume, don't make the wounded fill forms), and never twice in one quarter for the same person.

Behaviour & accessibility contract

The scale is a labelled radio group with keyboard flow; endpoints carry meaning text. Submitting acknowledges once, quietly. Declining is remembered.

Platform anchor

WCAG 2.2 · 1.3.1, 4.1.3; research-ethics conventions (voluntary, no dark nudges).

Live — pick a score
How likely are you to recommend us to a friend?
Not at all likelyExtremely likely
<div class="cc-nps-scale" role="radiogroup" aria-label="0 to 10, 0 is not at all likely, 10 is extremely likely"> <button role="radio" aria-checked="false">0</button> … </div>

Growth · dark-pattern-free by construction

What's New

Release notes people might actually read: three items, verbs first, each row deep-linking to the feature it announces. Shown once, then it's a page, not a prompt.

When to use

Meaningful user-facing changes after an update — new abilities, not refactors. One sheet per release maximum, dismissible instantly, revisitable from settings.

When not to

"Bug fixes and improvements" doesn't earn an interruption. Neither do upsells wearing a changelog costume — a "what's new" that's three premium pitches is an ad with a version number.

Behaviour & accessibility contract

Standard sheet semantics (dismissible, focus-managed); each item is a real link; the seen-flag is per-version and survives reinstalls via account state.

Platform anchor

HIG onboarding & release-note conventions; WCAG 2.2 · 2.4.4.

The sheet
What's newv4.2
Sleep modeDownloads finish overnight on Wi-Fi, silently.
Shared listsBuild a wishlist with anyone — live, like a doc.
Faster searchResults now land in under 100ms on-device.
<div class="cc-whatsnew" role="dialog" aria-labelledby="wn-h"> <b id="wn-h">What's new</b><span>v4.2</span> </div> <!-- three changes the user can act on, not a marketing changelog. Dismiss is one tap, remembered per version, and it never blocks the app underneath -->

Trust & Safety

Report & Block

The exits from a bad interaction, always two taps away. Report feeds the system; block protects the person — the component never confuses whose problem each solves.

When to use

On every piece of user-generated content and every profile, via the overflow menu. Reporting asks one clarifying question, confirms receipt, and says what happens next.

When not to

Don't bury it four screens deep to keep report volume flattering. Block never requires a reason — protection is not a form. Neither action notifies the reported party of who acted.

Behaviour & accessibility contract

Block takes effect immediately and optimistically — the content vanishes now, the network catches up. Both flows are fully keyboard-operable and confirm via status region.

Platform anchor

EU DSA Art 16 (notice and action) & Art 20 (appeals); App Store Guideline 1.2 (UGC apps must have report/block); Play UGC policy.

The flow's first screen
<!-- report = one question; block = zero questions --> <button class="cc-btn cc-btn-danger">Block @user</button> <div role="status">Blocked. They can't see your profile or message you.</div>

Trust & Safety

Content Warning

A blur that respects both the viewer and the content. It names the category, offers the choice, and remembers the preference — informed consent at feed speed.

When to use

Sensitive-but-allowed media: graphic news, medical content, spoilers. The veil names the category specifically ("Graphic injury") so the choice is informed, not a dare.

When not to

Not as an engagement mechanic — mystery-blurring benign content to farm taps teaches users to ignore real warnings. Policy-violating content is removed, not veiled.

Behaviour & accessibility contract

The reveal is an explicit button ("Show anyway"); the choice can persist per category in settings. The warning text is readable at full contrast on the blur.

Platform anchor

EU DSA systemic-risk mitigation; App Store 1.2 / Play sensitive-content rules; WCAG 2.2 · 1.4.3.

Live — reveal it
Sensitive content — medical procedure

This photo from a news story shows a surgical scene.

<div class="cc-cw"> <img src="…" alt="…"> <div class="cc-cw-veil"> <b>Sensitive content — medical procedure</b> <button>Show anyway</button> </div> </div>

Trust & Safety

Age Gate

A door, not a decoration. Where children's privacy law applies, the gate is designed to be neutral — it doesn't wink, hint, or make the underage path retryable until it passes.

When to use

Before collecting personal data where COPPA/GDPR-K applies, and before age-restricted content or purchases. Ask once, neutrally ("When were you born?"), without stating the passing age.

When not to

No "you must be 18, enter your age 😉" framing that coaches the answer. Failing users aren't offered a back-button retry loop — the session remembers.

Behaviour & accessibility contract

Neutral field order (year first coaches less), no data retained from failed gates beyond the session flag, and the under-age experience degrades kindly, never mocks.

Platform anchor

COPPA · 16 CFR Part 312 (neutral age screening, FTC guidance); GDPR Art 8; UK Age Appropriate Design Code.

Neutral by design

When were you born?

We use this to set up the right experience. It's not shown on your profile.

<!-- neutral: no target age stated, no retry coaching --> <div class="cc-agegate-fields"> <input inputmode="numeric" placeholder="YYYY" aria-label="Year"> <input inputmode="numeric" placeholder="MM" aria-label="Month"> <input inputmode="numeric" placeholder="DD" aria-label="Day"> </div>

Trust & Safety

Privacy Dashboard

Every promise the consent screen made, auditable in one place. What's collected, why, and the switch to stop it — the settings page as a trust document.

When to use

One screen from settings root. Each row: the data category, its plain-language purpose, its current state, and its control. Changes take effect immediately and confirm.

When not to

No euphemisms — "personalisation data" that means location history gets called location history. Controls that only pretend (off = still collected, just hidden) are the deepest dark pattern there is.

Behaviour & accessibility contract

Rows are grouped by data type with real switches; every state change announces. The page itself never requires consent to view.

Platform anchor

GDPR Arts 13–15 (transparency & access); CCPA right to know; Apple privacy nutrition labels (App Store 5.1).

The dashboard
LocationWhile using · sorts stores by distance
Usage analyticsAggregated · finds broken screens
Ad personalisationOff · ads still show, just generic
<label class="cc-switch"> <input type="checkbox" checked aria-label="Location access"> <span class="cc-switch-track"></span> </label> <!-- each row states what the permission buys the user, in their words. Off is a real state with a real consequence, printed here, not implied. GDPR Art. 7(3) — withdrawing is as easy as giving -->

Trust & Safety

Data Rights

Download everything, delete everything — as self-service buttons, not support tickets. The delete flow states its consequences once, plainly, then does what it says.

When to use

Account settings, one level deep. Export produces a machine-readable archive with an ETA. Deletion explains scope, offers a cooling-off window, and confirms completion out-of-band.

When not to

No retention theatre — five "are you sure" screens, surprise discount offers, and a mandatory phone call are the FTC's canonical obstruction examples. One honest confirm is the ceiling.

Behaviour & accessibility contract

The typed-confirm (for irreversibility) accepts paste and screen-reader input; deadlines and grace periods are stated in dates, not "soon".

Platform anchor

GDPR Art 17 (erasure) & 20 (portability); CCPA deletion right; App Store 5.1.1(v) — account deletion in-app if creation is in-app.

The two buttons
<button class="cc-cell"><b>Download your data</b> <span>Ready within 24h · JSON + media</span></button> <button class="cc-cell"><b>Delete your account</b> <span>30-day grace period, then permanent</span></button> <!-- both live in Settings at equal weight. GDPR Art. 15 (access) and Art. 17 (erasure) are one month and one screen — not a support ticket -->

Trust & Safety

Verified Badge

A checkmark is a claim the platform makes with its own reputation. This one means identity was checked — never that content is true, and never that a fee was paid unless the label says so.

When to use

Next to display names of identity-verified accounts, consistently everywhere the name appears. Tapping it explains exactly what was verified and when.

When not to

If verification is purchasable, the affordance must distinguish paid from vetted — one glyph for two meanings collapses both. Never auto-verify advertisers.

Behaviour & accessibility contract

The badge has a text name ("Verified account") in the accessible label of the name it decorates — not a mystery blue blob. It's never separable from the name in truncation.

Platform anchor

EU DSA Art 25 & trader traceability (Art 30); FTC deception standard; WCAG 2.2 · 1.1.1.

In context
Ana Reyes City Transit
<span class="cc-verified">Ana Reyes <i class="fas fa-circle-check" role="img" aria-label="Verified account"></i> </span> <!-- the badge must be tappable and say what was verified — identity, or org, or payment. A badge that means "subscriber" and looks like "identity" is the deception; the label is where you stop lying about which one it is -->

Trust & Safety

Security Checkup

An annual physical for the account, readable in ten seconds. Green means done, amber means one tap to fix — and the score never shames, it invites.

When to use

Reachable from settings; surfaced gently after security events (new device, password change). Each row pairs a state with its one-tap remedy.

When not to

Never cry wolf — "your account is at risk!" for an unused promo email address burns the channel needed for real alarms. Upsells (VPN, premium monitoring) don't wear the security costume.

Behaviour & accessibility contract

States triple-encode (icon + word + color); every fix lands on the exact screen that fixes it; completed items confirm and re-verify on return.

Platform anchor

NIST 800-63 authenticator guidance; WCAG 2.2 · 1.4.1; platform account-security conventions.

The checkup
Good shape3 of 4 checks passing
Passkey enabled
Recovery email confirmed
No unknown devices
Password reused elsewhereFix
<svg role="img" aria-label="Security score: 3 of 4 checks passing">…</svg> <div class="cc-security-item"><i class="warn"></i> <span>Password reused elsewhere</span><a href="/security/password">Fix</a></div> <!-- every finding ships with the one action that closes it. A score with no Fix link is an anxiety generator; the ring is never the only signal -->

Patterns

Pull to Refresh

The gesture that taught a generation how feeds work. Pull, arm, release — with a visible threshold so the hand always knows whether letting go will act.

When to use

Time-ordered content the user plausibly wants fresher — feeds, inboxes, scores. The arrow flips at the arming threshold; a spinner holds until real content lands.

When not to

Not on content that doesn't change (settings) and never as the only refresh path — a visible refresh control exists for keyboards, switch access, and everyone the gesture excludes.

Behaviour & accessibility contract

WCAG 2.5.7: single-pointer gesture with a button alternative. Completion announces ("Feed updated, 4 new posts"). Rubber-banding respects reduced motion.

Platform anchor

HIG refresh content controls; WCAG 2.2 · 2.5.7 dragging movements, 4.1.3.

Live — drag the list down (or click the header)
Pull to refresh
Morning update12 new items since 8:00
Weekend recapYesterday
<!-- gesture + a real button; the gesture is never the only way --> <button aria-label="Refresh feed">⟳</button> <div role="status">Feed updated, 4 new posts</div>

Patterns

Infinite Scroll vs Pages

Infinite scroll is a loan against the user's sense of time. This system spends it only where content is truly endless — and always installs a footer the scroll can land on.

When to use

Entertainment feeds with no meaningful end. Load-more happens ahead of the viewport; position restores perfectly on back-navigation — losing your place is the cardinal sin.

When not to

Goal-directed contexts — search results, order history, anything comparable — get pagination or "Load more" buttons: they preserve place, footer, and the feeling of progress. The footer must be reachable; content that flees the scrollbar fails WCAG and patience alike.

Behaviour & accessibility contract

New batches announce count, focus is never stolen, and a "Back to top" affordance appears after two screens. Session-length nudges ("you're all caught up") are honest stopping points, not shame.

Platform anchor

EU DSA Art 25 & recital 67 (attention-exploiting design); WCAG 2.2 · 2.4.1; NN/g infinite-scroll research.

The honest footer
Post 412 days ago
Post 422 days ago
You're all caught up — everything since your last visit.
<div class="cc-inf-foot" role="status">You're all caught up — everything since your last visit.</div> <!-- an end exists and is stated. Anything below the feed (footer, legal, settings) gets a real route, because a keyboard user cannot out-scroll a loader. WCAG 2.2 · 2.4.1 bypass blocks -->

Patterns

Undo, Not Confirm

Confirmation dialogs make everyone pay a tax because someone might err. Undo charges only the error, only when it happens — act immediately, offer the way back.

When to use

Reversible destructive actions: delete a message, archive a thread, remove from cart. The action executes optimistically; a toast holds the undo for ~6 seconds; the model really can restore.

When not to

Truly irreversible or high-stakes actions (delete account, send money, publish to everyone) keep a real confirm — undo is for the reversible, and pretending otherwise is worse than the dialog.

Behaviour & accessibility contract

The undo control is focusable before timeout, the timer pauses on hover/focus, and the same action is also available in an activity/trash surface for the slow.

Platform anchor

WCAG 2.2 · 2.2.1 timing adjustable, 3.3.4 error prevention; HIG undo conventions; NN/g forgiveness principle.

Live — delete something
Draft: birthday plansEdited 2h ago
Draft: apartment notesEdited yesterday
// act now, allow regret row.hidden = true; toast("Draft deleted", { action: "Undo", onAction: () => row.hidden = false });

Patterns

Error Recovery

Errors are moments of maximum attention — the one time you're guaranteed a careful reader. Say what happened, what it cost, and what to do, in that order.

When to use

Every failure surface. The formula: plain-language what ("Couldn't send"), the stakes ("your message is saved as a draft"), the action ("Try again"). Input is never thrown away.

When not to

No error codes as headlines (keep them small, for support), no blame ("you entered an invalid…"), and no dead ends — every error screen has at least one button.

Behaviour & accessibility contract

role="alert" for blocking failures; retry buttons debounce; after two failed retries the surface changes shape and offers support instead of hope.

Platform anchor

WCAG 2.2 · 3.3.1, 3.3.3; NN/g error-message guidelines.

The recovery card
<div class="cc-err" role="alert"> <h3>Couldn't upload your photo</h3> <p>The connection dropped mid-upload. Your edit is saved on this device — nothing was lost.</p> <button>Try again</button><button>Save for later</button> <p>Error 1006 — mention this to support if it keeps happening.</p> </div> <!-- four parts, in this order: what failed, what survived, what to do now, what to quote. "Something went wrong" has none of them. WCAG 2.2 · 3.3.1 -->

Patterns

Offline Mode

The network is a feature the world sometimes turns off. Offline design keeps reading, queues writing, and tells the truth about both.

When to use

A quiet banner when connectivity drops; cached content stays readable and marked fresh-as-of; outgoing actions queue visibly ("will send when online") instead of failing.

When not to

Don't block the whole app behind a full-screen dinosaur when 90% of it is cached and useful. Don't retry forever silently — surface the queue so users can cancel stale intents.

Behaviour & accessibility contract

The state change announces once (not per-scroll); queued items are inspectable; reconnection syncs and reports ("2 messages sent"). Timestamps mark cached content honestly.

Platform anchor

HIG & Material offline guidance; WCAG 2.2 · 4.1.3; PWA offline-first conventions.

The states
You're offline — showing your library from 20 minutes ago.
"See you at 7!"Queued · will send when you're back online
<div class="cc-offline" role="status">You're offline — showing your library from 20 minutes ago.</div> <!-- the staleness is a number, never the word "recently". Queued writes are visible, cancellable, and survive a restart; a spinner that waits forever for a network that is gone is a lie about whose fault this is -->

Patterns

Cancellation Flow

The truest test of a product's ethics: leaving must be as easy as arriving. One path, two screens, zero guilt — the FTC calls it click-to-cancel; this system calls it the default.

When to use

Subscriptions and memberships. Cancel lives where billing lives, works in-app end-to-end, states what's lost and when, and confirms in writing. One save-offer maximum, skippable in the same tap.

When not to

No retention mazes: chat-agent-only cancellation, guilt copy ("we'll miss you 😢" over a shrunken cancel link), surprise pause-instead defaults, or five-screen offer gauntlets. Each is an enforcement citation with a UI attached.

Behaviour & accessibility contract

The path is discoverable by search ("cancel"), completable by keyboard, and ends in a dated confirmation ("Access until Aug 12, no further charges"). Resubscribing is equally easy — symmetry both ways.

Platform anchor

ROSCA 15 U.S.C. §8403 and FTC Act §5 — note the 2024 “click-to-cancel” amendments to the Negative Option Rule were vacated in full by the Eighth Circuit on 8 July 2025, days before the compliance date, so ROSCA is what is actually in force; App Store 3.1.2 / Play subscription cancellation rules; EU Consumer Rights Directive 2011/83 Art 6.

The whole flow, honestly

Cancel your membership?

You'll keep full access until Aug 12. After that, no further charges. Your playlists and history stay saved for 12 months in case you return.

<!-- equal-weight choices, dated consequences, one optional alternative --> <button class="cc-btn cc-btn-danger">Cancel membership</button> <button class="cc-btn cc-btn-secondary">Keep membership</button>

Patterns

Search & Filter

Query, refine, count — the loop behind every marketplace. Filters are chips you can see and remove one at a time; the result count updates with every change so cause never loses its effect.

When to use

Catalogs beyond one screen. Applied filters render as removable chips above results; a filter sheet batches complex refinement behind one "Show 142 results" commit button that always shows its count.

When not to

Never auto-apply filters the user didn't pick ("sponsored first" is a sort disclosure, not a default to hide). Zero results with active filters offers to loosen them, never a dead end.

Behaviour & accessibility contract

Count changes announce politely; each chip's remove is a named target; "Clear all" appears at two or more filters. Filter state lives in the URL — shareable, restorable, honest.

Platform anchor

WCAG 2.2 · 4.1.3, 2.5.8; EU DSA Art 27 (ranking transparency); Baymard filtering conventions.

Live — toggle filters, watch the count
142 results
<div class="cc-sfilter-chips"> <button class="cc-chip" aria-pressed="true">Under $100</button> </div> <span class="cc-result-count" role="status">104 results</span>

Core

Date & Time Picker

The control people fight with most, because it is the one place a product insists on its own format instead of accepting yours. Take typing. Take pasting. Take “next Friday” if you can.

When to use

Any date the user must supply: a booking, a birthday, a delivery window. Pair a text field with a calendar — the field is the fast path for people who know the date, the calendar is the browsing path for people who are choosing one. For a range — a stay, an availability window — a two-month grid that carries state on the days themselves: price, sold out, closed.

When not to

Never for a date the user already knows by heart and can type in three seconds (a birthday behind a twelve-tap calendar is hostile). Never a calendar-only control on mobile web where the native <input type="date"> already carries the platform's own accessible picker. Never a dropdown-triplet for a date of birth: typing “1994” beats paging back 360 months.

Behaviour & accessibility contract

Grid is role="grid"; arrow keys move by day, PageUp/Down by month, Home/End to week ends. The focused cell is the only tab stop. Disabled dates state why. Locale decides day-of-week order and the DD/MM vs MM/DD reading — never the developer's timezone. Typed entry parses forgivingly — 3/7, 03-07 and “Mar 7” all land on the same day.

Platform anchor

WAI-ARIA Authoring Practices · Date Picker Dialog; WCAG 2.2 · 2.1.1 keyboard, 3.3.2 labels or instructions; HIG Pickers; Material 3 Date pickers.

Live — type it or pick it
MTWTFSS

Weekends are shown, not hidden — a disabled day still says why.

<input type="text" value="14 Aug 2026" aria-describedby="fmt"> <p id="fmt">Any format. We'll understand it.</p> <div role="grid" aria-label="August 2026">…</div> // arrow = day · PageUp/Down = month · one tab stop
Native on touch — and a range grid when the dates are a stay
Native picker — the OS does this better than we ever will.
MTWTFSS
Jul 14 – 16 · 2 nights · sold-out days say so
<input type="date"> <!-- single dates: let the OS win --> <button class="cc-cal-day" disabled aria-label="July 18, sold out">18</button>

Core

Upload & Camera

Uploading is the moment a user hands you something they cannot easily replace. Show the file before you take it, show the progress while you take it, and let them take it back.

When to use

Photos, documents, avatars, receipts. Offer drag-and-drop and a real <input type="file"> button — on mobile the button also opens the camera, and on desktop it is the only path a keyboard user has. More than one file means a list, not a counter: each row carries its own state and its own Remove.

When not to

Never a drop zone alone. Never a spinner with no bytes-so-far on a large file — an indeterminate spinner on a 90 MB video is a lie about whether anything is happening. Never silently strip EXIF without telling a user you did, and never silently keep GPS EXIF without telling them either. Never state a limit only by failing — size, count and type belong on the zone before the pick, not in the error after it.

Behaviour & accessibility contract

Drop zone is a labelled button; progress is role="progressbar" with a live percentage; failures name the reason (too large / wrong type / network) and keep the file selected so retry costs one click, not a re-pick. Cancel is available for the whole upload. Drag is an enhancement and never the only path (WCAG 2.5.7); Remove works mid-flight, not just after.

Platform anchor

WCAG 2.2 · 4.1.3 status messages, 3.3.1 error identification; HIG Photos & camera access; App Store Review 5.1.1 (purpose strings); GDPR Art. 5(1)(c) data minimisation — location EXIF is personal data. WCAG 2.2 · 2.5.7 dragging movements.

Live — drop, or press the button

Location data in this photo will be removed before upload. Why

<input type="file" accept="image/*" capture="environment"> <div role="progressbar" aria-valuenow="42" aria-valuemin="0" aria-valuemax="100"> // failure keeps the file selected — retry is one click, not a re-pick
Per-file life cycle — queued, uploading, done, removable
<button class="cc-upload-zone">Drop photos here or browse</button> <!-- drag is the shortcut; the button is the door (WCAG 2.5.7) --> <div role="progressbar" aria-label="review-photo.jpg, 62% uploaded">…

Core

Multi-Step Form

Splitting a long form into steps does not make it shorter. It makes it survivable — but only if the user can see how far it goes, go back without loss, and leave without starting over.

When to use

Forms with more than roughly ten fields, or with branching (identity → address → payment). Group by the user's mental model, not by your database tables. Name the steps rather than numbering them: “Delivery” tells a user what is coming, “Step 2” does not.

When not to

Never to hide length — a five-step flow that never showed “5” is a dark pattern with a progress bar on it. Never validate a step only on submit of the last one. And never make Back destructive: if going back loses what was typed, it isn't Back, it's Restart. Never validate step N against something the user only learns at step N+2, and never lock Back to protect a funnel metric.

Behaviour & accessibility contract

Each step is a <form> with its own heading; focus moves to that heading on advance and the step is announced (“Step 2 of 4, Address”). Progress is a real role="progressbar". State persists across reload — a network blip is not the user's fault. The rail is an ordered list with aria-current="step", so position is structure rather than decoration.

Platform anchor

WCAG 2.2 · 2.4.3 focus order, 3.3.4 error prevention, 3.3.7 redundant entry (don't ask twice for what you already have); Material 2 steppers — not carried into Material 3; NN/g progressive disclosure.

Live — and Back really goes back

Step 1 of 4 · Your details

What you type is kept when you go back — and when you close the tab.

<p role="status">Step 2 of 4 · Address</p> <h2 tabindex="-1">Address</h2> // focus lands here on advance <div role="progressbar" aria-valuenow="2" aria-valuemax="4"> // draft persisted on every change — Back is never Restart
Named steps — and completed ones stay revisitable
  1. Bag
  2. Delivery
  3. Payment
  4. Review
Delivery

Address and speed — your details from last time are prefilled, not re-asked.

<ol class="cc-wizard-steps"> <li class="done">Bag</li> <li class="now" aria-current="step">Delivery</li> </ol> <!-- Back is free: nothing typed is ever lost -->

Core

Coach Mark

A tooltip you didn't ask for. It is the most abused component in consumer software, so it gets the strictest rule in this system: one, at the moment it is useful, dismissible forever.

When to use

Exactly one genuinely non-obvious affordance — a gesture, a hidden long-press, a new control in a place people already know. Trigger it on the screen where it is used, not on launch.

When not to

Never as a tour. A five-step carousel of coach marks on first launch is a confession that the interface failed, and nobody reads it — they tap Skip and now they've been taught that your hints are noise. Never to point at something you want to sell. Never twice. Never coach the obvious — a tip spent on “Tap search to search” teaches people to dismiss unread.

Behaviour & accessibility contract

Focus moves into it and returns to the anchor on dismiss; Esc dismisses; it never traps. The dismissal is persisted per-user, not per-session. It is a non-modal role="dialog", so a screen-reader user is not silently stuck behind an overlay they can't perceive. It never covers the control it describes.

Platform anchor

HIG Onboarding (“teach in context”); WCAG 2.2 · 1.4.13 content on hover or focus, 2.1.2 no keyboard trap; Material 3 Tooltips (rich).

Live — one hint, then never again
Saved items

Dismiss it and it is gone for good — stored against the account, not the session.

// one hint, at the moment of use, dismissed forever if (!user.hints.sortReorder) showCoach(anchor); onDismiss(() => api.patch('/hints', { sortReorder: true })); // per-user, not per-session
Anchored to a control the user is already looking at
New: group orders

Start one and friends add items to the same cart — one delivery fee, split at checkout.

<div class="cc-coach" role="status"> <b>New: group orders</b>…<button>Got it</button> </div> <!-- once per account, never a five-stop tour -->

Commerce

Subscription Management

The screen that decides whether a customer trusts you, because it is where they find out what you have been charging them and whether you made it hard to stop.

When to use

Any recurring charge. Show the plan, the exact next charge amount and date, the payment method, and the full charge history. Cancel sits on this screen — not three levels down, not in an email, not on a phone line.

When not to

Never let the renewal price differ from the price on this screen. Never bury cancel behind a chat widget or a “call us”. Never auto-upgrade a plan the user did not choose. Introductory pricing must state the price it becomes, and when — on the same screen, in the same size.

Behaviour & accessibility contract

The next charge is a single unambiguous sentence, not a table the user must assemble. Cancellation is reachable in the same number of clicks it took to subscribe. A cancelled subscription still shows what remains paid-for and when access actually ends.

Platform anchor

FTC negative-option / click-to-cancel rulemaking (cancel as easy as sign-up; confirm dates with counsel); EU Consumer Rights Directive 2011/83 (pre-contract information); App Store Review 3.1.2; Google Play Subscriptions policy.

Live — the whole truth, one screen
Premium · AnnualSince 12 March 2025
Active

Your card ending 4242 will be charged $89.00 on 12 March 2027. Nothing changes before then.

12 Mar 2026$89.00Receipt
12 Mar 2025$69.00Receipt

Two taps to subscribe. Two taps to leave. The cancellation flow →

// the only sentence that matters, in plain language "Your card ending 4242 will be charged $89.00 on 12 March 2027." // cancel is a sibling of 'change plan' — same screen, same weight class

Trust & Safety

Account Deletion

A door that only opens inwards is not a door. If a user can create an account in the app, they must be able to end it in the app — and know exactly what that ends.

When to use

Every app that supports account creation. The entry point lives in account settings, not support. State what is deleted, what is retained and why (tax records, fraud logs, legal hold), and when access actually stops.

When not to

Never offer “deactivate” as the only option and call it deletion. Never require an email to support, a phone call, or a web page the app refuses to link to. Never delete silently: an active subscription, an outstanding balance, or shared content owned by others must be surfaced before the button, not discovered after.

Behaviour & accessibility contract

This is the one place a confirm is right — deletion is irreversible, so undo does not apply. Confirm by re-authenticating, not by typing DELETE in a box. Offer the data export first. Send a confirmation to the address on file, with a real grace window.

Platform anchor

App Store Review 5.1.1(v) — in-app account deletion is required; Google Play Data deletion policy (in-app and web route); GDPR Art. 17 right to erasure and Art. 20 portability; CCPA/CPRA deletion rights.

Live — and the export comes first
Delete your account
  • Your profile, posts and saved items are removed within 30 days.
  • You have 14 days to change your mind. After that it cannot be undone.
  • Purchase records are kept for 7 years — we are required to.
  • Your Premium subscription is active. It will be cancelled; no further charges.

Confirmed by re-authenticating — not by typing the word “DELETE”. Typing a word proves nothing about who is holding the phone.

// re-auth, not a typed word — a typed word proves nothing about who is holding the phone await reauthenticate(); // passkey / Face ID / password await api.delete('/account', { graceDays: 14 });

AI & Agents

AI Composer

The blank box is the hardest interface in consumer software: infinite capability, zero affordance. The composer's job is not to look clever — it is to tell you what this thing can do and where its edges are, before you waste a turn finding out.

When to use

Any open-ended AI surface. Show scope in the placeholder (“Ask about your orders” beats “Ask me anything”), show what it can see (attached context as removable chips), and give an explicit Stop while it is generating.

When not to

Never as a replacement for a control that already works — a chat box is a worse date picker than a date picker. Never claim “ask me anything” on a model that is scoped to your catalogue. Never send hidden context the user cannot see and cannot remove.

Behaviour & accessibility contract

Enter sends, Shift+Enter is a newline, and the field grows to a cap then scrolls. Attached context is announced and each chip is individually removable by keyboard. Stop is focusable the instant generation starts — the ability to interrupt is part of the contract, not a nicety.

Platform anchor

WCAG 2.2 · 3.3.2 labels or instructions, 2.1.1 keyboard; EU AI Act Art. 50 transparency (the user must know they are interacting with an AI system); HIG Machine Learning — set expectations, show the limits.

Live — scope in the placeholder, context you can remove
Order #4821 Return policy It can see these. Nothing else.

Answers are generated by AI and can be wrong. Order data is real.

<textarea placeholder="Ask about your orders, returns or delivery…"> // Enter sends · Shift+Enter newline · Stop is focusable the instant generation starts // every piece of context is a visible, removable chip — nothing hidden is sent

AI & Agents

Status Orb

A spinner has one state. An agent doing real work has several, and the user's patience depends on knowing which one they're in — listening is a different wait than searching, and searching is a different wait than writing code. The orb's job is to spend that difference, not hide it.

When to use

Any agentic surface with more than one kind of latency — a chat that sometimes reads context, sometimes calls a tool, sometimes writes code. Name the state in both a visual and a plain-language string ("Agent searching…"), so the indicator never has to be decoded to be understood.

When not to

Don't invent a state the backend can't actually report — a "reasoning" animation over a system that only ever waits is theatre, and theatre erodes trust faster than a plain spinner would. Don't run more than four or five states in one product; past that, users stop learning the vocabulary.

Behaviour & accessibility contract

role="img" with an aria-label derived from the state's status text, not the state's internal id. Reduced motion freezes the current frame rather than hiding it — the state stays legible without motion. Off-screen and backgrounded instances stop animating; nothing here should cost a tab that isn't visible.

Platform anchor

WCAG 2.2 · 2.3.3 animation from interactions, 1.4.2 audio/motion control by proxy; EU AI Act Art. 50 transparency (the state string doubles as an AI-disclosure surface, see AI Disclosure).

Live — five of twenty states

Agent listening…

Five of the full set's twenty hand-tuned states — the other fifteen (reasoning, planning, verifying, routing, syncing…) live in the standalone component. See all 20 in the live playground

<ai-sphere state="thinking" size="64"></ai-sphere> // role="img" + aria-label auto-derived from the state's status text // prefers-reduced-motion freezes the frame instead of hiding it // off-screen instances pause via IntersectionObserver

AI & Agents

Streaming Response

Streaming solves latency and creates an accessibility problem in the same instant: text that changes sixty times a second is unusable to a screen reader. The fix is to stream to the eye and announce to the ear once.

When to use

Any generation long enough that a spinner would feel broken. Stream tokens into a region that is visually live but aria-live="off", and announce the completed answer once, when it is done.

When not to

Never mark the streaming region aria-live="polite" — you will fire hundreds of announcements and make the answer less accessible than a spinner. Never stream a legal, medical or financial figure that could be misread mid-sentence. Never remove Stop.

Behaviour & accessibility contract

“Generating” is announced via role="status"; the answer text is announced once on completion. Stop halts immediately and keeps the partial output — discarding what the user already read is a second failure on top of the first. Copy and Regenerate appear only when the answer is whole.

Platform anchor

WCAG 2.2 · 4.1.3 status messages, 2.2.2 pause, stop, hide (moving content); ARIA live-region practice; prefers-reduced-motion for the caret.

Live — press Stop mid-answer
AI Idle
Press Generate to see it stream.

Stopping keeps what you already read.

<p role="status">Generating…</p> // announced once <div aria-live="off">…tokens…</div> // streams to the eye, silent to the ear onDone(() => announce(fullText)); // announced once, when it is whole

AI & Agents

Sources & Citations

A citation is not decoration. It is the difference between an answer you can check and an answer you must simply believe — and a fabricated citation is worse than none, because it buys trust it hasn't earned.

When to use

Whenever the answer is grounded in retrievable material. Attach the source to the claim, not to the bottom of the message, and let the user open the passage that produced it — not just the domain it came from.

When not to

Never render a citation the system did not actually retrieve. Never cite a source the model didn't use because it “looks about right”. And never show a bare favicon row as a proxy for grounding — an unlinked logo is theatre.

Behaviour & accessibility contract

Each citation is a real link with a discernible name (not “[1]” alone), states the date the source was retrieved, and opens the exact passage. Ungrounded sentences are visually distinguished and labelled — not by colour alone.

Platform anchor

WCAG 2.2 · 2.4.4 link purpose, 1.4.1 use of colour; FTC guidance on substantiation and deceptive claims; EU AI Act Art. 50 transparency; NIST AI RMF (Measure — traceability of outputs).

Live — hover a claim to see what it stands on

Your return window for order #4821 closes on 29 August. Items must be unworn and in original packaging. Most customers get their refund within about a week.not grounded

Source 1 · Order #4821

“Delivered 15 Aug 2026. Returns accepted within 14 days of delivery.”

Retrieved from your account · today

The last sentence has no source, so it says so. An unsupported sentence dressed as a supported one is the failure mode this component exists to prevent.

// the citation attaches to the CLAIM, not the message <a href="/orders/4821#returns" aria-label="Source: order 4821, returns policy">1</a> // sentences with no retrieved source are labelled, not silently blended in

AI & Agents

Confidence & Abstention

The most valuable thing an AI product can say is “I don't know — here is someone who does.” Abstention is a feature. A confidently wrong answer costs more than no answer, and it costs the user, not you.

When to use

Whenever the cost of being wrong is real: money, health, legal, safety, or an irreversible action. Route low-confidence answers to a human, a verified document, or a deterministic tool — and say which.

When not to

Never render a raw model probability as a percentage badge — it reads as calibrated precision the model does not have, and users treat “87%” as a fact. Never use confidence to hedge every answer; a system that always says “I might be wrong” has said nothing.

Behaviour & accessibility contract

Uncertainty is expressed in words and shape, never colour alone. The escalation path is a real control with a real destination and a stated response time. The state is announced — a screen-reader user must not miss the one sentence that says this answer is not reliable.

Platform anchor

NIST AI RMF (Manage — risk-informed escalation); EU AI Act Art. 14 human oversight (high-risk uses); WCAG 2.2 · 1.4.1 use of colour; FTC Section 5 — unsubstantiated capability claims.

Live — three answers, three honest postures

No percentage badges. A model's raw probability is not a calibrated confidence, and printing it as one is a lie with a decimal point.

// three postures, in words and shape — never a colour, never a bare % GROUNDED → answer + citation PARTIAL → answer + what it could not verify + how to verify it ABSTAIN → no answer + a route to a human, with a response time

AI & Agents

AI Disclosure & Content Credentials

If it was generated, say so — on the artefact, at the moment of viewing, in a form that survives a screenshot's worth of doubt. This is now law in some markets and it will be law in more.

When to use

Any synthetic or substantially AI-modified image, audio, video or text presented to a consumer; any chat interface where a person could reasonably think they are talking to a human. Attach provenance metadata (C2PA Content Credentials) as well as the visible mark.

When not to

Never bury the disclosure in a settings page or a footer. Never make it a hover-only tooltip. Never let it be strippable by a crop — the visible mark and the embedded credential are two mechanisms, and you need both because either alone can be defeated.

Behaviour & accessibility contract

The disclosure is real text in the accessibility tree, not baked into a pixel. It is announced with the media, not after it. Tapping it explains what was generated and by what — a badge with no explanation is a shrug.

Platform anchor

EU AI Act Art. 50 (transparency; machine-readable marking of synthetic content — confirm dates and scope with counsel); C2PA Content Credentials 2.x; FTC guidance on AI claims and endorsements; platform synthetic-media policies.

Live — on the artefact, not in the settings

Two mechanisms, because either alone can be defeated: a visible mark a person can read, and an embedded credential a machine can verify after the crop.

<img alt="AI-generated landscape" src="…"> <button aria-describedby="cred">AI-generated</button> // real text, in the a11y tree // + C2PA manifest embedded in the file — survives the screenshot the badge doesn't

AI & Agents

Agent Action Confirmation

An agent that can act on your behalf is a system that can be wrong on your behalf. The interface question is not “how do we make this smooth” — it is which actions a human must sign, and how we make that signature meaningful rather than reflexive.

When to use

Before any action that spends money, sends something to another person, changes a permission, or cannot be undone. Show the exact effect — the amount, the recipient, the scope — not a summary of the intent. The user is signing the payload, not the plan.

When not to

Never gate reversible reads and drafts — a confirm on everything is a confirm on nothing, and you will train the user to press Approve without looking. Never let a gate be satisfied by the agent itself. Never batch ten irreversible actions behind one Approve.

Behaviour & accessibility contract

Approve is never the default focus. The consequence is stated before the buttons and is announced. Editing the payload is possible from the gate — if the only options are Approve and Cancel, the user with a nearly-right action has to start over, and will approve the wrong thing instead.

Platform anchor

EU AI Act Art. 14 human oversight; NIST AI RMF (Govern — accountability for automated actions); PSD2 SCA for payment initiation; WCAG 2.2 · 3.3.4 error prevention (legal, financial, data).

Live — the human gate
Found your booking · read-only
Drafted the change · reversible
Needs you · this one spends money
Rebook flight — you are approving this exact payload
Charge$212.00 to card ••42
New flightNH 106 · 21 Aug · 09:55
RefundableNo

Approve is not the default focus, and the button carries the amount — a button that just says “Approve” can be pressed without reading.

// the agent proposes; the human signs the payload, not the plan if (action.spendsMoney || action.irreversible || action.sendsToOthers) { await humanGate(action.payload); // exact amount · exact recipient · editable }

AI & Agents

Agent Activity Trace

When something acts for you while you are not looking, the receipt is the product. A trace is how a user audits a decision they didn't watch — and how they find the moment it went wrong.

When to use

Any agent that runs multi-step or in the background. Log what it read, what it did, what it was refused, and what it cost — with timestamps and a link back to each artefact it touched.

When not to

Never as raw chain-of-thought — it is not a log, it is unverified narration, and showing it invites the user to trust reasoning nobody checked. Log tool calls and effects, which are facts. Never omit the failed and blocked steps: a trace that only records successes is marketing.

Behaviour & accessibility contract

The trace is a semantic list, newest-first, readable without expanding. Blocked steps state the rule that blocked them. Every entry is addressable, so a support conversation can point at a line rather than describe it.

Platform anchor

EU AI Act Art. 12 record-keeping / logging; NIST AI RMF (Measure — traceability); GDPR Art. 22 (a route to contest an automated decision needs a record of it); SR 11-7 effective challenge, in the regulated sibling of this system.

Live — including what it was not allowed to do
09:41Read booking #A7-2210 · read-only
09:41Compared 3 alternative flights
09:42Blocked — auto-charge $212.00. Rule: spend > $0 requires a human.
09:44You approved the $212.00 payload
09:44Rebooked NH 106 · confirmation sent

The blocked line is the most important line on this screen. A trace that only shows successes is not a record, it is a brochure.

// log effects, not narration — chain-of-thought is not an audit trail trace.push({ t, tool: 'payments.charge', status: 'BLOCKED', rule: 'spend > $0 requires a human' });

AI & Agents

Prompt Starters

Starters are not decoration on an empty state — they are the only honest documentation an AI surface has. Each one is a promise: this is a thing I can actually do.

When to use

On the empty state, and as follow-ups after an answer. Derive them from real capability and real user context — “Where is order #4821?” beats “Tell me a joke” on a retail app, because one is what the surface is for.

When not to

Never suggest something the system cannot do — the user tries it, it fails, and now every suggestion is suspect. Never make them the only path (the box must still take free text). Never use them to upsell: a “starter” that opens a paywall is an ad wearing a helpful hat.

Behaviour & accessibility contract

Real buttons in a list, reachable by keyboard, each with a full accessible name (not truncated). Selecting one puts the text in the composer — editable, not sent — so the user keeps authorship.

Platform anchor

HIG Machine Learning — “set expectations, offer examples”; WCAG 2.2 · 2.4.6 headings and labels, 2.5.8 target size; NN/g empty-state guidance.

Live — it fills the box, it doesn't send it
What can it actually do?
// a starter is a promise: this is a thing it can actually do onSelect(s => composer.value = s); // fills, never sends — the user keeps authorship

AI & Agents

Feedback & Correction

A thumbs-down that goes into a dashboard nobody reads is a complaint box in an empty building. Feedback earns its place only if the user can see what it changed — at minimum, for them.

When to use

On every generated answer. Offer a fast signal (helpful / not) and a real correction path — the user usually knows the right answer and is willing to type it if you make it worth two seconds.

When not to

Never let the thumbs be the only route for harm — harmful, unsafe, or defamatory output needs a Report path with a human at the end, not an aggregate metric. Never say “Thanks, this helps us improve” if it does not. Never treat the correction as training data without saying so.

Behaviour & accessibility contract

Buttons are toggles with aria-pressed and unique names (“Mark this answer unhelpful”, not “thumbs down”). Submitting is confirmed via role="status". The correction is applied to the conversation immediately, so it is worth the effort now, not hypothetically in the next model.

Platform anchor

GDPR Art. 16 right to rectification (if the output is personal data, correction is a right, not a courtesy); DSA Art. 16 notice-and-action for illegal content; NIST AI RMF (Manage — incident feedback loops); WCAG 2.2 · 4.1.2 name, role, value.

Live — correct it and watch it take
Your return window closes on 29 August.

Corrections apply to this conversation immediately. Harm reports go to a human, not a metric.

<button aria-pressed="false" aria-label="Mark this answer unhelpful"> // the correction lands in THIS conversation now — not hypothetically, in the next model // "report harm" is a separate path with a human at the end

AI & Agents

Memory & Context

An assistant that remembers is useful. An assistant that remembers invisibly is surveillance with a friendly voice. The rule is simple: if it remembered it, you can read it, and you can delete it.

When to use

Whenever the system carries anything across sessions. Every remembered item is a discrete, human-readable row — what it learned, when, and from which conversation — with a delete on the row itself.

When not to

Never store an inference the user never stated as if it were a fact they told you (“has children” inferred from a search is a guess, and it will be wrong about someone in a way that hurts). Never remember special-category data by default. Never make deletion “forget in future answers” while the row stays on your servers.

Behaviour & accessibility contract

Memory is a list, not a paragraph. Deletion is immediate and confirmed via role="status". A global off switch exists and is honoured. New memories are announced when written, not discovered later in an answer.

Platform anchor

GDPR Art. 5 (purpose limitation, minimisation), Art. 9 (special categories), Art. 17 erasure; CCPA/CPRA right to delete; App Store Privacy Nutrition Labels; COPPA (no behavioural profiling of children).

Live — delete a memory, it's gone
What it remembers about you
Prefers window seatsYou said this · 2 Aug
Ships to the Sydney addressYou said this · 14 Jul
Size M in outerwearInferred from 3 orders · 9 Jun

Inferred memories say inferred. A guess presented as something you told us is the quiet way a product starts lying to you about yourself.

// every memory is a row: what · when · source · delete { text: "Size M in outerwear", source: "inferred", from: "3 orders", at: "2026-06-09" } // delete is a real DELETE — not "won't mention it again"

AI & Agents

Training & Personalisation Controls

Two different questions get collapsed into one toggle by most products, and it is not an accident: “use my data to personalise my experience” and “use my data to train your model” are not the same deal.

When to use

Wherever user content can feed personalisation, training, or human review. Split the switches, because they are separate decisions with separate beneficiaries — and state the consequence of each in the row itself.

When not to

Never bundle training consent into the terms of service and call it a choice. Never default training on for minors. Never pre-tick. Never make opting out of training silently degrade an unrelated feature — that is a penalty dressed as a consequence, and consent obtained under it is not freely given.

Behaviour & accessibility contract

Switches are symmetrical: on and off are the same number of taps, the same visual weight, no “are you sure you want to lose out?” interstitial. Each row states what changes. Changes take effect immediately and are confirmed.

Platform anchor

GDPR Art. 7 (freely given, unbundled, as easy to withdraw), Art. 21 objection, Art. 22 automated decisions; EU AI Act transparency duties; App Tracking Transparency; COPPA (verifiable parental consent).

Live — two switches, because they are two deals

No “are you sure you want to lose out?” screen on the way out. If leaving costs more taps than joining, it was never consent.

// two switches, because they are two different deals personalisation: default ON — the benefit is the user's modelTraining: default OFF — the benefit is ours; opt-in, unbundled, symmetrical to leave

AI & Agents

AI Summary in Search

Summarising other people's work at the top of a results page is a redistribution of attention. Do it in a way that still sends the reader — and the credit — onward.

When to use

When a query has a synthesisable answer across sources the user would otherwise open one by one. The summary sits above the results, not instead of them, and each claim links to the source that produced it.

When not to

Never for a query where being wrong is expensive (dosage, legal deadlines, financial figures) — show the source, not a paraphrase of it. Never summarise a source you are also burying. Never present a summary with no route to what it summarised: that is not search, it is a wall.

Behaviour & accessibility contract

The summary is labelled AI-generated before it is read, is skippable by a screen-reader user in one keystroke (it is a landmark, not an unnamed div), and never occupies the entire first viewport on mobile — the results must be visible without scrolling past an answer the user did not ask for.

Platform anchor

EU AI Act Art. 50 transparency; FTC guidance on deceptive presentation; WCAG 2.2 · 2.4.1 bypass blocks, 1.3.1 info and relationships; copyright and DSM Art. 15 press-publisher rights — substance, not just attribution.

Live — above the results, never instead of them
AI summary

Most carriers allow one cabin bag up to 7 kg1, but the size limit varies by airline2.

Cabin baggage — official policycarrier.example · updated Aug 2026
Size and weight limits by airlineairports.example
<section aria-label="AI summary"> // a landmark — skippable in one keystroke <a href="#results">Skip to results</a> // never taller than the fold: the results must be visible without scrolling past an // answer the user did not ask for

AI & Agents

Refusal & Human Escalation

Every AI product eventually meets the request it should not answer. The design question is what happens in that second — because a refusal with no exit is just a door closing on someone who came to you for help.

When to use

Regulated advice, safety-critical questions, disputes, distress, and anything the system has been instructed not to do. Say that it won't, say why in one plain sentence, and hand over to a path that actually resolves it.

When not to

Never refuse into a void — a refusal with no route is worse than an unhelpful answer, because the user now has neither. Never moralise. Never refuse a legitimate question because it merely resembles a forbidden one, and never make the user re-explain everything to the human you finally hand them to.

Behaviour & accessibility contract

The escalation control is a real button with a real destination and a stated response time. Context transfers with the user — the human sees the conversation. If the topic is distress, the route to a person is offered immediately and is not buried under a policy paragraph.

Platform anchor

EU AI Act Art. 14 human oversight; DSA Art. 20 internal complaint-handling; App Store Review 1.1/5.1 (safety and sensitive content); NIST AI RMF (Manage — escalation and incident response).

Live — a refusal with a door in it
Not something I should answer

I can't tell you whether to dispute this charge — that's a decision with money and a deadline attached, and it needs a person who can act on your account.

They'll see this conversation — you won't start over.

One plain sentence of why. One real door. No lecture.

// a refusal with no route is worse than an unhelpful answer refuse({ why: "money + a deadline + account access", route: { to: 'human', sla: '4 min', carryContext: true } });

AI & Agents

Voice Agent

Voice removes the screen, and with it every affordance you were relying on. What is left has to carry the whole contract: that it is listening, that it is a machine, and that you can cut it off.

When to use

Hands-busy, eyes-busy, or accessibility-first contexts. The mic state must be unambiguous and continuously visible — listening, thinking, speaking — and barge-in must work: the user can interrupt mid-sentence and the agent stops instantly.

When not to

Never hot-mic without an unmistakable indicator. Never keep a recording the user wasn't told about — in two-party-consent jurisdictions that is not a UX choice, it is a legal one. Never make voice the only channel: a person who cannot speak, or cannot be overheard, must have the same product.

Behaviour & accessibility contract

A live transcript runs alongside the audio — deaf and hard-of-hearing users get the same product, and everyone else gets a record. State changes are announced. A visible Stop is always present, and the agent identifies itself as an AI at the start of the call, out loud.

Platform anchor

EU AI Act Art. 50 (a person must be told they are talking to an AI); two-party-consent recording laws (e.g. CA Penal Code §632 — confirm per jurisdiction with counsel); WCAG 2.2 · 1.2.4 captions (live), 2.5.4 motion actuation; HIG Siri & voice.

Live — a transcript, always

Not listening

Agent — This is an AI assistant. This call is not recorded.

The transcript is not an accessibility afterthought — it is the record. Speak over it and it stops mid-word, because a machine that has to finish its sentence is a machine that isn't listening.

// barge-in: the user always wins onUserSpeech(() => tts.stop()); // stops mid-word — no "let me finish" // and the transcript runs regardless, because it is the record, not a fallback

AI & Agents

Tool & Data Grants

The permission surface of the MCP era. An agent's power comes from its tools — so every tool and every data source is its own grant: scoped, separately revocable, and visible in one place. "Allow everything" is not a setting, it's a surrender.

When to use

Wherever an agent can touch user data or external tools. One row per grant, each stating its scope in operational terms — "read your calendar", "draft but never send" — with the narrowest scope as the offered default.

When not to

Never a single master toggle that bundles read, write, and spend. Never expand a grant silently in an update — new capability means a new ask. Payments and sends are never part of any bundle; they are always their own explicit grant.

Behaviour & accessibility contract

Each grant is a labelled switch announcing scope and state; revoking takes effect immediately and is confirmed. Every grant change is written to the activity trace — permissions and audit are one system, not two.

Platform anchor

GDPR Art 5(1)(b) purpose limitation and Art 5(1)(c) data minimisation — a grant is a purpose, and its scope is the minimisation; EU AI Act Art 14 human oversight; least-privilege conventions from OAuth scope design.

Live — one grant per capability
CalendarRead events · never creates or edits
EmailDraft replies · you send every one
PaymentsOff by default · every charge gated even when on
Every change is logged to the activity trace.
<!-- one grant per capability, scope stated operationally --> <label class="cc-switch"> <input type="checkbox" role="switch" aria-label="Email: draft replies only, you send every one"> <span class="cc-switch-track"></span> </label>

AI & Agents

Background Agent Tasks

Agents work in minutes, not milliseconds — so the queue is a first-class surface. Four honest states: queued, working, needs you, done. "Needs you" is the interesting one — it means the agent hit a gate and stopped, which is the system working, not failing.

When to use

Any agent task longer than a few seconds: research, multi-step bookings, batch edits. The user leaves, the task continues, completion notifies through the channel the user already approved.

When not to

Never run irreversible steps while unattended — those park the task in "needs you" until the gate is signed. Never show a spinner with no cancel: a task you can't stop isn't yours, you're its.

Behaviour & accessibility contract

State changes announce politely; cancel is instant and keeps partial results; "needs you" deep-links to the exact gate. Estimated time is a range, honestly widened when unknown.

Platform anchor

EU AI Act Art 14 (interruptibility as effective oversight); WCAG 2.2 · 4.1.3 status messages; HIG background task conventions.

Live — run one
Compare 12 flights for the Tokyo tripEst. 2–4 min · will pause at anything that spends money
Ready
<span class="cc-badge cc-badge--warn">Needs you</span> <!-- the agent reached a payment gate and stopped — deep-links to the exact confirmation, not the home screen -->

AI & Agents

Mode & Model Picker

Fast or thorough is a real decision users can make — if the trade-off is stated in outcomes, not model names. Nobody should need to know what a parameter count is to choose how carefully their question gets answered.

When to use

When modes differ in ways the user can feel: speed, depth, cost. Describe each in outcome language ("answers in seconds, may miss nuance") and show the price difference exactly where the choice is made.

When not to

Don't market version numbers to people who can't evaluate them — "v5-turbo-max" is noise wearing a spec. Don't default to the most expensive mode, and never switch modes silently mid-conversation to save cost (see Usage Meter: degradation is disclosed).

Behaviour & accessibility contract

A radio group with outcome-labelled options; the active mode is visible in the composer at all times, not buried in settings. Changing modes announces and applies to the next turn, stated explicitly.

Platform anchor

EU AI Act Art 50 transparency; FTC clarity on charges (paid modes state their price at the decision point); WCAG 2.2 · 4.1.2.

Live — pick a mode
<!-- outcomes, not parameter counts --> <b>Thorough</b><span>takes about a minute, checks its own sources</span> <b>Deep research</b><span>~10 min · 3 credits — price at the decision point</span>

AI & Agents

Ghost Text & Suggested Edits

The most intimate AI surface: inside the user's own sentence. The contract is absolute — suggested text is unmistakably not-yet-yours, accepting it is always an explicit act, and your words are never overwritten.

When to use

Completions in composers, reply suggestions, edit proposals. Ghost text renders in the suggestion style; Tab or a button accepts, Esc or typing through dismisses — both directions keyboard-first.

When not to

Never auto-commit a suggestion on blur, send, or timeout — an unaccepted suggestion that ships is the system putting words in the user's mouth. No suggestions in password, legal-name, or payment fields, ever.

Behaviour & accessibility contract

The suggestion is distinguished by more than color — style plus an announced prefix ("Suggestion:") so screen-reader users hear the boundary sighted users see (WCAG 1.4.1). Accept and dismiss both have visible button equivalents on touch.

Platform anchor

WCAG 2.2 · 1.4.1 use of color, 2.1.1 keyboard; HIG Machine Learning — corrections (user stays the author).

Live — accept or dismiss

Thanks for the invite — I'd love to come. What can I bring?

Ghost style + announced "Suggestion:" prefix — the boundary is audible, not just visible.

<span class="cag-ghost" aria-label="Suggestion: I'd love to come."> I'd love to come. </span> <!-- never auto-commits on blur, send, or timeout -->

AI & Agents

Human ↔ Agent Handoff

When an agent can drive a session — filling a form, working a screen — exactly one question must never be ambiguous: who is driving right now. The handoff banner answers it at all times, and the wheel is always within reach.

When to use

Any surface where an agent acts on the user's behalf in real time. A persistent banner states who's driving; take-over is one tap and instant; the agent announces intent ("filling shipping address") as it works.

When not to

Never let the agent keep driving on a screen the user has started touching — human input always wins the wheel. No invisible sessions: an agent acting with no banner is indistinguishable from a compromise.

Behaviour & accessibility contract

Pause/take-over is keyboard-reachable at all times and works mid-action. On resume, the user gets a delta recap — what changed while the agent drove — before anything else happens.

Platform anchor

EU AI Act Art 14 (oversight must be effective, not nominal — interruptibility is the test); WCAG 2.2 · 2.1.1; EU AI Act Art 50 (the agent's presence is disclosed).

Live — take the wheel
Agent is driving — comparing checkout options · step 2 of 4

Intent is announced as it works; your first touch on the screen also takes the wheel.

<div class="cc-banner cc-banner--info" role="status"> <b>Agent is driving</b> — comparing checkout options <button>Take over</button> <!-- one tap, works mid-action --> </div>

AI & Agents

Usage & Cost Meter

AI features have a meter running, and hiding it is a pricing dark pattern with extra steps. The meter shows what's spent, what's left, and — critically — what happens at the cap, before the cap.

When to use

Anywhere AI usage is limited or billed: credits, daily message caps, minutes. Visible from the surface that spends it, not three screens away — the cost of an action is part of the action.

When not to

Never let a user discover the cap by hitting it mid-task. Never silently degrade to a cheaper model at the limit — if quality drops at the cap, the meter says so where the choice is made (see Mode Picker).

Behaviour & accessibility contract

The meter is a labelled progressbar with numbers, not just a bar; crossing 80% announces once. Reset timing is a date ("resets Aug 1"), never "soon". Paid top-ups state price before the tap.

Platform anchor

FTC all-in pricing discipline (the cost is disclosed where it's incurred); App Store 3.1 (in-app purchase clarity); WCAG 2.2 · 4.1.3, 1.4.1.

Live — spend a credit
Deep-research credits 7 of 10 left

Resets Aug 1 · past the cap, Fast mode stays free — stated here, not discovered there.

<div role="progressbar" aria-valuenow="30" aria-label="7 of 10 credits left">… <!-- crossing 80% announces once; reset is a date, never "soon" -->

AI & Agents

Generated Artifact Card

When AI output is a thing — an image, a document, an itinerary — it stops being a message and becomes an object with duties: versions that don't destroy each other, provenance you can open, and a disclosure that survives export.

When to use

Any generation the user will keep, edit, or share. The card carries version history (regenerate is a new version, not an overwrite), an openable provenance panel, and export that embeds the credential.

When not to

Never overwrite v1 because the user asked for a tweak — regret is a workflow, not an edge case. Never strip the provenance on export to make the output look hand-made; that's the disclosure rule inverted.

Behaviour & accessibility contract

Versions are a labelled radio row; switching announces. Provenance (model, prompt, edits) is a disclosure panel, not a tooltip. The AI-made mark meets contrast on any artwork via scrim.

Platform anchor

EU AI Act Art 50 (synthetic content marking); C2PA 2.x content credentials; GDPR Art 17 (deleting an artifact deletes its versions).

Live — switch versions

Birthday invite art

AI-made

Regenerating never overwrites — v1 and v2 are one tap back. Export embeds the content credential.

<span class="cc-badge">AI-made</span> <!-- visible mark… --> export.credentials = c2pa.sign(artifact) // …and one that survives export

AI & Agents

Capability Disclosure

The first screen of any AI feature decides every later disappointment. Three things it can do, three it can't, one line about data — the anti-magic show. Underclaiming here is the cheapest trust you will ever buy.

When to use

First run of any AI surface, and permanently reachable from its header. Capabilities in user outcomes, limits in plain sentences, and the data line links straight to the grants screen.

When not to

No "magic" framing, no ∞ sparkles promising everything — capability overclaim is an FTC deception issue now, not just a UX smell. Don't bury the limits below the fold of an unscrollable card.

Behaviour & accessibility contract

Can and can't lists are real lists with icon + text (never icon-only); the card is dismissible and revisitable; the data line is one sentence with a working link, not a policy PDF.

Platform anchor

EU AI Act Art 50 transparency; FTC deception standard (capability claims); HIG Machine Learning — set accurate expectations.

The anti-magic card
<div class="cc-consent"> <b>Can</b> <span>Track orders, compare items you're viewing</span> <b>Can't</b> <span>Spend money without you</span> <b>Data</b> <span>Your orders and current page</span> </div> <!-- Can / Can't / Data, before first use, at equal visual weight. EU AI Act Art. 50 transparency; NIST AI RMF GOVERN-1. "Can't" is the load-bearing row -->

Social

Profile Header

A person's front door: identity above the fold, numbers that mean something, one primary action. Everything else on the profile is furniture — this is the handshake.

When to use

The top of any profile: avatar, display name + handle, verified state, a two-line bio, three stats maximum, and the follow/message pair. Stats are tappable — a count you can't inspect is a boast.

When not to

Don't stack six stats — followers, following, and one domain number (posts, reviews, runs) is the ceiling. Never hide the handle in favor of display name only; the handle is the verifiable identity.

Behaviour & accessibility contract

The header reads as one landmark: name is the page's h1, stats are labelled links ("1,204 followers"), and the follow button announces its state change. Bio links are real links.

Platform anchor

WCAG 2.2 · 1.3.1 headings, 2.4.4 link purpose; EU DSA Art 30 (trader identity where accounts sell); verified-badge conventions (see Trust & Safety).

The handshake
AR

Ana Reyes

@anaruns

Sunrise runner · city photographer. Everything before 7am.

<header class="cc-profile"> <h1>Ana Reyes <i role="img" aria-label="Verified account"></i></h1> <a href="/followers"><b>12.4k</b> followers</a> <!-- counts are links --> </header>

Social

Message Inbox

The conversation list is triage, not archaeology: who, the last line, how long ago, and whether it needs you — readable in a glance per row. Strangers wait in Requests, not in your face.

When to use

Any messaging surface. Rows: avatar + presence, name, one truncated preview line, timestamp, unread count. A Requests folder holds non-connections — consent applies to attention too.

When not to

No promotional "conversations" from the platform injected into the inbox — a brand that fakes being your friend burns the surface for everyone. Muted threads never bubble to the top on new messages.

Behaviour & accessibility contract

Unread is triple-encoded (dot + bold + count in the accessible name). Swipe actions (mute, archive) all exist in the long-press/overflow menu too — gestures are shortcuts, never the only door.

Platform anchor

WCAG 2.2 · 2.5.7 dragging movements, 1.4.1; ePrivacy (unsolicited messaging consent); platform anti-spam norms.

Triage at a glance
<button class="cc-cell"> <b>Ana Reyes</b><span>See you at the north exit ☕</span> <small>4:12 pm</small><span class="cc-badge">2</span> </button> <!-- requests from people you do not follow are a separate room, not a mixed inbox: consent to receive is the feature. Read receipts are opt-in, both ways -->

Social

Poll & Voting

The lightest form of speech. One tap to vote, results the moment you have — and never the moment before, because seeing the crowd first changes the answer.

When to use

Quick opinion capture in feeds and stories, 2–4 options, with a visible close time. Results render as labelled bars with percentages and the total count — your choice stays marked.

When not to

Results are hidden until you vote (or the poll closes) — pre-voting visibility is a conformity machine. Never for consequential decisions wearing casual clothes; a poll is not consent, feedback, or a survey.

Behaviour & accessibility contract

Options are a radio group before voting and a results list after; the flip announces ("You voted Trail. Trail 64%, Road 36%, 2,140 votes"). Bars pair width with printed percentages — never width alone.

Platform anchor

WCAG 2.2 · 1.4.1, 4.1.3; research-method honesty (no result-peeking bias); EU DSA Art 25.

Live — cast a vote
Better sunrise run? 2,139 votes · closes in 6h · results after you vote
<!-- radio group before the vote, results list after --> <div role="status">You voted Trail. Trail 64%, Road 36% — 2,140 votes.</div>

Social

Mentions & Hashtags

The wiring of the social graph: @ points at a person, # points at a room. Both are entities the moment they're typed — colored, linked, and accountable.

When to use

Composer autocomplete triggers on @ and # with a ranked shortlist (people you know first). Rendered mentions and tags are real links with distinct styling that survives grayscale.

When not to

Mentioning is not consent to be dragged into anything — mass-mention spam is rate-limited and mentions from strangers are a notification setting, not a right. Never linkify inside code or quoted text.

Behaviour & accessibility contract

The autocomplete is an ARIA combobox (arrow keys, Enter, Esc); inserted entities are single tokens — one backspace removes the whole mention. Mentioned users can untag themselves.

Platform anchor

WAI-ARIA combobox pattern; WCAG 2.2 · 1.4.1; EU DSA (notice mechanisms extend to tags of people).

Entities, not text
Sunrise crew assemble — @anaruns found a new route through #northpier, who's in for Saturday?
@an…
<a class="cc-entity" href="/@anaruns">@anaruns</a> <div role="listbox" aria-label="Mention suggestions"> <button role="option" aria-selected="true">Ana Reyes <span>@anaruns</span></button> </div> <!-- a mention is an entity with an id, not matched text — renaming the account must not silently repoint the link. The picker is arrow-navigable and shows the real handle, because @ana and @an.a are an impersonation vector -->

Social

Live Stream Chat

Thousands of voices in a phone-width column. The design job is legibility under flood: pinned context stays put, moderation is one tap, and the flow never steals your scroll position.

When to use

Chat beside live video. New messages flow at the bottom; scrolling up pauses auto-follow and shows a "back to live" pill with the missed count. The creator's pinned message holds the top.

When not to

Never auto-scroll a user who scrolled up — stolen scroll is the fastest way to make chat unreadable. Slow mode and follower-only mode are creator tools, disclosed in the composer, not silent filters.

Behaviour & accessibility contract

The stream is a log region with announcements throttled (a screen reader must never receive 40 messages a second); report lives on every message via long-press and overflow alike.

Platform anchor

WCAG 2.2 · 4.1.3 (throttled announcements), 2.2.2; EU DSA Art 16 (report adjacency); App Store 1.2 (UGC moderation).

Under flood, still legible
@anaruns Route map + pace groups: link in bio 🏃

MOD · Tomas Keep it kind, keep it moving

runwithmaya that ridge view 😍😍

coastal.kev pace group for 6:00/km?

anika_t joining from Osaka 👋

<div class="cc-livechat-flow" role="log" aria-live="polite" aria-relevant="additions">…</div> <button class="cc-livechat-newer">Back to live · 23 new</button> <!-- under flood the log stops announcing every line and the pin carries the one message that matters. Scrolling up pauses autoscroll and says so — it never yanks the reader back to the bottom mid-sentence -->

Core · Display

Stats & Sparkline Card

Consumer analytics — screen time, spending, steps — is one number, one direction, one tiny chart. The card answers "how am I doing" before the eye finishes landing on it.

When to use

Personal dashboards and summaries. Anatomy: the number (tabular, big), the delta with an explicit direction word, and a sparkline for shape — labelled with its time range, always.

When not to

No unlabelled axes pretending to be neutral — a sparkline that starts at yesterday's low is an argument, not a chart. Deltas on spending say whether up is bad; green ≠ universally good.

Behaviour & accessibility contract

The sparkline is decorative with the story in text ("Spending down 12% vs last month, $342 of your $400 budget"); direction pairs arrow + word, never color alone (WCAG 1.4.1).

Platform anchor

WCAG 2.2 · 1.4.1, 1.1.1; chart-honesty conventions (labelled ranges, honest baselines).

One number, one direction, one shape
Spending · July
$342 down 12%
vs June · $400 budget
Screen time · today
3h 24m up 40m
vs daily average · 7-day range
<b>$342</b><span class="cc-stat-delta cc-stat-delta--good">down 12%</span> <svg class="cc-spark" role="img" aria-label="Spending trend, four weeks, trending down">…</svg> <span class="cc-stat-foot">vs June · $400 budget</span> <!-- the direction is a word and an arrow, never green-versus-red alone, and "good" is decided by the metric: spending down is good, screen time up is not. A sparkline with no baseline in the footer is a shape, not a fact -->

Core · Form

Address Form

The most abandoned form in commerce, and the most international. One search field does most of the work; the rest adapts to the country instead of arguing with it.

When to use

Checkout and profile. Lead with autocomplete search; expand to editable fields on selection. Field set, order, and labels follow the chosen country — ZIP is not a universal concept.

When not to

Never validate international addresses against US assumptions (required state, 5-digit postal). Never block manual entry — autocomplete is a shortcut, not a gate; new buildings exist.

Behaviour & accessibility contract

Full autocomplete attributes (address-line1, postal-code, country) keep browser autofill working — the fastest address form is the one the browser fills. The suggestion list is an ARIA combobox.

Platform anchor

WCAG 2.2 · 1.3.5 identify input purpose, 3.3.7; postal-format conventions (UPU); Baymard address research.

Search first, fields second
<input autocomplete="address-line1"> <input autocomplete="postal-code"> <!-- the fastest address form is the one the browser fills -->

Core · Settings

Appearance Switcher

Light, dark, or follow the system — with the system as the default, because the user already told their OS what they want and asking twice is a bug.

When to use

A three-option segmented control in settings (and optionally a topbar shortcut). "Auto" tracks the OS live, including scheduled sunset switching, and says that's what it does.

When not to

Never dark-only as a brand statement for a reading-heavy product; never flash the wrong theme on load (persist the choice where first paint can read it). Don't ship a toggle that fights the OS.

Behaviour & accessibility contract

The switch applies instantly, announces, and respects prefers-contrast and forced-colors beyond the binary. Both themes hold the same AA contrast contract — dark mode is not an excuse.

Platform anchor

HIG dark mode; Material color schemes; WCAG 2.2 · 1.4.3 (both themes), prefers-color-scheme.

Live — this one actually switches the page
<div class="cc-seg" role="group" aria-label="Appearance"> <button>Light</button><button>Dark</button><button>Auto</button> </div> <!-- Auto is the default: the OS was told once already -->

Core · Settings

Language & Region

Every language names itself in itself — 日本語, not "Japanese" — because the person who needs the switch is precisely the one who can't read your current language.

When to use

Settings, and reachable from onboarding. Language and region are separate choices (English · Japan is a real life); currency and units follow region with a manual override.

When not to

Never a flag for a language — flags are countries, and Spanish has twenty. Never IP-force a locale the user changed away from; the explicit choice always outranks geolocation.

Behaviour & accessibility contract

Each option carries its own lang attribute so screen readers pronounce it natively; switching announces in the new language; layout survives RTL and 35%-longer German strings.

Platform anchor

WCAG 2.2 · 3.1.2 language of parts; BCP 47; HIG internationalization; W3C i18n best practices.

Self-named, never flagged
<button class="cc-cell"><b lang="ja">日本語</b><span lang="ja">日本 · JPY</span></button> <!-- every language names itself in itself, and carries lang= so a screen reader switches voice. Never a flag: a flag is a country and language is not. Locale is chosen, never inferred from an IP and never locked to it -->

Text Animations

Split Reveal

The headline arrives one character at a time — a stagger so short it reads as texture, not theatre. The workhorse entrance for hero copy.

When to use

Hero and section headlines, once per view, on first reveal. Total duration stays under 700ms — the reader came for the sentence, not the choreography.

When not to

Never on body copy or anything the user must read urgently (errors, prices, legal). Never re-trigger on every scroll pass — an entrance happens once.

Behaviour & accessibility contract

The real sentence lives in the element's aria-label; animated characters are aria-hidden — a screen reader hears one sentence, never letter soup. Space is reserved: zero layout shift. Reduced motion renders the final state instantly.

Platform anchor

WCAG 2.2 · 2.3.3 animation from interactions; Core Web Vitals CLS discipline; HIG Motion (purposeful, brief).

Live
<h1 class="ta-split" aria-label="Design is how it works."> <span aria-hidden="true"><i>D</i><i>e</i>…</span> </h1> .ta-split i { display:inline-block; opacity:0; transform:translateY(.35em); animation: taRise .5s var(--spring) forwards; animation-delay: calc(var(--i) * 22ms); }

Text Animations

Blur In

Words resolve from soft focus to sharp, like the eye settling. The gentlest entrance in the set — calm products earn it.

When to use

Editorial intros, wellbeing and premium surfaces, word-level stagger (never per character — blur reads at word scale). Pairs with slow content like photography.

When not to

Not on data or numbers — blurred digits read as wrong digits for a frame. Not on low-power devices at long lengths: blur is a filter, and filters cost frames.

Behaviour & accessibility contract

Same screen-reader contract as Split Reveal (real text in aria-label, animation aria-hidden). Blur animates filter only — no movement, so it's the safest choice for vestibular-motion sensitivity short of none.

Platform anchor

WCAG 2.2 · 2.3.3; vestibular-safe motion guidance (filter over transform); HIG Motion.

Live
.ta-blur span { display:inline-block; opacity:0; filter:blur(10px); animation: taFocus .6s ease forwards; animation-delay: calc(var(--i) * 90ms); } @keyframes taFocus { to { opacity:1; filter:blur(0); } }

Text Animations

Typewriter

Text that appears to be written, caret and all. Fifty years old and still the most honest way to say "something is composing this."

When to use

Short phrases where authorship is the message — an AI drafting, a terminal, a search suggestion. One line, under 60 characters, then the caret stops blinking and rests.

When not to

Never for paragraphs — nobody watches 400 characters get typed. Never as fake AI theatre on content that arrived instantly (see AI Disclosure: pretending to compose is a small lie with a big pattern).

Behaviour & accessibility contract

The full string is exposed to assistive tech immediately (aria-label) — the typing is visual only, never a reading-speed gate. The caret blink stops at rest (nothing blinks forever), and reduced motion prints the line whole.

Platform anchor

WCAG 2.2 · 2.2.2 (nothing blinks indefinitely), 2.3.3; EU AI Act Art 50 adjacency (composition theatre must be truthful).

Live
<p class="ta-type" aria-label="Ask about your orders…"> <span aria-hidden="true" data-out></span><span class="ta-caret"></span> </p> // JS types ~34ms/char; caret stops blinking at rest

Text Animations

Decode

Characters churn through noise and settle into the message, left to right. The cyberpunk entrance — loud, memorable, and strictly rationed.

When to use

One moment per product: a reveal, an unlock, a launch. Settle direction is always left-to-right so the eye can start reading before the animation finishes.

When not to

Never on numbers users act on (a churning price is a slot machine), never on security codes (churn reads as tampering), and never twice on the same screen.

Behaviour & accessibility contract

Assistive tech gets the final string only — the noise frames are never announced. Fixed-width character cells prevent reflow during churn. Total under 900ms; reduced motion skips straight to the message.

Platform anchor

WCAG 2.2 · 2.3.3, 4.1.3 (announce once, final only); Core Web Vitals CLS.

Live
// churn random glyphs per cell, settle L→R, < 900ms total // SR hears only the final string — noise is aria-hidden

Text Animations

Count Up

Numbers that travel to their value. Movement says "this was measured, watch it arrive" — which is exactly why it may only ever land on the true figure.

When to use

Stats, milestones, totals — once, on first view, easing out so the last digits are readable. Tabular numerals so nothing jitters horizontally.

When not to

Never animate money the user is about to pay (the total is a fact, not a performance), and never count to a rounded marketing number the data doesn't support — motion amplifies the claim.

Behaviour & accessibility contract

The final value is in the accessible name from frame zero; intermediate values are visual only. Width is reserved for the final figure — no shift. Reduced motion shows the number, done.

Platform anchor

WCAG 2.2 · 2.3.3, 4.1.3; FTC substantiation discipline (motion is emphasis — emphasised claims need evidence).

Live
<b class="ta-count" aria-label="12,482 runs logged">0</b> // rAF + ease-out cubic; tabular-nums; width reserved for the final figure

Text Animations

Word Rotate

One slot in the sentence cycles through alternatives — "built for runners / riders / climbers". The rotation makes a list feel like a welcome.

When to use

Hero taglines where the product genuinely serves every word in the list. Three to five words, each readable for ≥1.8s, and the cycle ends — three loops, then it rests on the primary word.

When not to

Never rotate words users might need to reference ("free" appearing every third cycle is a rotating claim), and never in body text — moving targets punish readers mid-sentence.

Behaviour & accessibility contract

The accessible sentence names all variants once ("built for runners, riders, and climbers") instead of announcing every swap. The slot reserves the widest word — no reflow. Stops within five seconds, which is the actual 2.2.2 exception — a cycle count is not one, and three rotations usually exceed five seconds. Instantly at rest under reduced motion.

Platform anchor

WCAG 2.2 · 2.2.2 pause/stop/hide (self-stopping), 2.3.3; FTC (rotating claims are still claims).

Live
<h1 aria-label="Built for runners, riders, climbers and walkers"> Built for <span class="ta-slot" aria-hidden="true">runners</span> </h1> // slot width = widest word; 3 cycles, then rests on the primary

Text Animations

Gradient Flow

A slow aurora inside the letterforms. Color moves so the type doesn't have to — the quietest way to make one line feel alive.

When to use

One brand moment per screen — the hero word, a plan name, a launch title. The drift is slow (≥8s per pass) and the gradient's darkest stop still meets contrast on its background.

When not to

Never on interactive labels (a shifting button label reads as a state change) and never on more than one element per view — two auroras compete, and both lose.

Behaviour & accessibility contract

Pure background-position animation — the glyphs never move, so it's vestibular-safe. Every gradient stop holds ≥4.5:1 against the surface; reduced motion freezes the gradient (color stays, drift stops).

Platform anchor

WCAG 2.2 · 1.4.3 (every stop passes), 2.3.3; HIG color (decorative color never carries meaning).

Live — drifts continuously, freezes under reduced motion
Aurora, quietly.
.ta-gradient { background: linear-gradient(100deg, var(--accent), var(--purple), var(--accent)) 0/200% 100%; -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; animation: taDrift 9s linear infinite; } @media (prefers-reduced-motion: reduce) { .ta-gradient { animation:none; } }

Text Animations

Shine Sweep

A highlight passes across the word once, like light across brushed metal. The premium accent — and like all premium things, scarce by design.

When to use

Achievement names, tier labels (Gold, Pro), unlocked states — one sweep on reveal, optionally one on hover. The sweep is the reward's punctuation, not its substance.

When not to

Never looping — a permanently shimmering word reads as an ad and burns the effect for the real moment. Never on discount or urgency copy; shine is celebration, not pressure.

Behaviour & accessibility contract

The base text is fully readable before, during, and after the sweep — the highlight adds, never subtracts, contrast. One-shot with a replay affordance; nothing under reduced motion.

Platform anchor

WCAG 2.2 · 1.4.3 (base state passes alone), 2.3.3; EU DSA Art 25 (no shimmer-as-urgency).

Live
Gold member
.ta-shine.run { background: linear-gradient(110deg, var(--text1) 42%, var(--accent) 50%, var(--text1) 58%) -200%/200% 100% no-repeat text; -webkit-text-fill-color: transparent; animation: taSweep .9s ease forwards; } /* one sweep, then rest */

Text Animations

Wave

Characters take turns bouncing — playful energy for playful products. The party trick of the set, which is precisely why it self-stops.

When to use

Celebration and kids-adjacent surfaces: "You did it!", loading mascots, empty-state charm. Short words, small amplitude (≤0.18em), three passes, done.

When not to

Nowhere serious — a waving "Payment failed" is a tonal catastrophe. Not while the user reads adjacent content; bouncing in the periphery is distraction, not delight.

Behaviour & accessibility contract

Self-terminates within the five seconds 2.2.2 allows — the exception is a time limit, not a pass count; per-character transform only, layout space reserved. Reduced motion: still text, full stop — charm is optional, reading isn't.

Platform anchor

WCAG 2.2 · 2.2.2 (self-stopping), 2.3.3; UK Age Appropriate Design Code adjacency (kid surfaces avoid compulsive motion).

Live
.ta-wave i { display:inline-block; animation: taWave 1.1s ease-in-out 3; /* three passes, then rest */ animation-delay: calc(var(--i) * 60ms); } @keyframes taWave { 30% { transform: translateY(-.18em); } }

Text Animations

Marquee Ticker

The infinite ribbon — logos, headlines, drop announcements. The only permanently moving text this system allows, and it pays for the privilege with a real pause control.

When to use

Peripheral, skimmable, repeating content: partner logos, "as seen in", event tickers. Slow enough to read (≥12s per loop), duplicated seamlessly, edges faded.

When not to

Never for content users must not miss — motion is the opposite of guaranteed delivery. Never faster to create urgency, and never two tickers on one screen.

Behaviour & accessibility contract

WCAG 2.2.2 in full: moving >5s means a visible pause button, plus hover and focus both pause. The duplicate strip is aria-hidden; the content is also available as a static list to assistive tech. Reduced motion parks it as a scrollable row.

Platform anchor

WCAG 2.2 · 2.2.2 pause/stop/hide (the letter of it), 2.3.3; HIG Motion.

Live — hover, focus, or the button pauses it
Sunrise 10K this Saturday·New trail maps for Osaka·Group orders now live·Streak freezes are automatic·
.ta-marquee-track { display:inline-flex; gap:28px; white-space:nowrap; animation: taLoop 14s linear infinite; } .ta-marquee:hover .ta-marquee-track, .ta-marquee:focus-visible .ta-marquee-track, .ta-marquee.paused .ta-marquee-track { animation-play-state: paused; } /* >5s of motion ⇒ a visible pause button ships with it (WCAG 2.2.2) */

Data & Tables

Tree View

Hierarchy you can walk with four arrow keys. The canonical pattern for folders, categories, and settings trees — one tab stop, not forty.

When to use

Nested structures the user browses and picks from: file trees, category managers, org settings. Depth is real and parents are meaningful groupings, not decoration.

When not to

Flat lists dressed as trees — if everything sits at level one, use a list. And never for primary navigation on mobile: a drill-in list per screen beats a tiny tree.

Behaviour & accessibility contract

ARIA tree pattern: one tab stop with roving focus; ↑↓ move, → expands or enters, ← collapses or exits, Home/End jump. Every branch carries aria-expanded and aria-level; the whole tree is useless if only the mouse can open it.

Platform anchor

WAI-ARIA APG tree view pattern; WCAG 2.2 · 2.1.1 keyboard, 4.1.2 name/role/value.

Live
  • 📁design-system
    • 📄AGENTS.md

Click a row or focus the tree and drive it with ↑ ↓ → ← · Home · End.

<ul role="tree"> <li role="treeitem" aria-expanded="true" aria-level="1" tabindex="0">tokens <ul role="group">…</ul> </li> </ul> // one tab stop; roving tabindex moves with arrow keys

Data & Tables

Selection Tree

Checkboxes with a parent that tells the truth: fully checked, fully clear, or honestly mixed. Selecting a branch selects its children — and says so.

When to use

Bulk selection over a hierarchy — permission sets, folder sync, category filters — where "select the parent" legitimately means "select everything inside".

When not to

When children carry different consequences (billing vs. deletion), don’t hide them behind a parent toggle: one careless click should never quietly select something expensive or destructive.

Behaviour & accessibility contract

Parent checkboxes expose three real states; the mixed state is aria-checked="mixed", never a styling trick. Checking a parent checks all descendants; un-checking one child drops the parent to mixed immediately. A live count announces the running total.

Platform anchor

WAI-ARIA APG checkbox pattern (tri-state); WCAG 2.2 · 4.1.2 name/role/value, 4.1.3 status messages.

Live
Analytics access
View dashboards
Export reports
Manage settings

0 of 3 permissions selected

<button role="checkbox" aria-checked="mixed">…</button> // parent = checked | unchecked | mixed — computed from children, // never persisted on its own. The count is a live region.

Data & Tables

Tree Table

Hierarchy inside a real table: parent rows expand, indent carries the level, and the parent’s numbers equal the sum of its children — a rollup that doesn’t reconcile is a lie in a grid.

When to use

Budgets, storage usage, org rollups — tabular data where rows have children and the parent aggregates them. The user needs both the total and the breakdown.

When not to

Arbitrary grouping the user didn’t ask for. If the hierarchy is only two levels and always expanded, plain visual grouping with subheader rows is simpler and calmer.

Behaviour & accessibility contract

A real <table>; expandable rows carry aria-expanded and indentation encodes aria-level. Rollup cells are computed from child rows in one place, so the parent can never drift from its children. Expand state is announced, not just drawn.

Platform anchor

WAI-ARIA APG treegrid pattern; WCAG 2.2 · 1.3.1 info & relationships.

Live
Team Seats Spend / mo

The parent row is computed from the children on load — expand to check the math.

<tr aria-expanded="false">…rollup cells…</tr> // rollup = children.reduce(…) — computed in one place, // so parent and children cannot disagree.

Data & Tables

Editable Cells

A grid you can type into without fear: Enter commits, Escape restores, bad input explains itself in place — and every edited cell wears a mark until it’s saved.

When to use

High-volume numeric corrections — budgets, quantities, forecasts — where a form-per-row would take ten times as long and the user lives in spreadsheets anyway.

When not to

One-off edits of complex records (open a form), or anything destructive. And never auto-commit on blur alone for consequential values — a stray click should not silently change a number.

Behaviour & accessibility contract

Cells are buttons until activated (click or Enter), then a labelled input. Enter commits, Escape cancels and restores the previous value, invalid input keeps focus with an inline message. Edited cells carry a visible dot and an "edited" text flag — colour is never the only signal.

Platform anchor

WAI-ARIA APG grid pattern (editable); WCAG 2.2 · 3.3.1 error identification, 3.3.3 error suggestion, 1.4.1 use of color.

Live
Line item Budget (USD)
Research incentives
Usability tooling
Type licences

Enter commits · Esc restores · letters are refused with a reason.

<td><button data-edit>4,800</button></td> // activate → input; Enter commits, Esc restores old value; // invalid keeps focus + inline reason; edited cells marked dot + text

Data & Tables

Table Pagination

"Showing 11–20 of 47" — a table that tells you where you are and how much is left. The honest counter is the component.

When to use

Bounded result sets the user scans and returns to — orders, invoices, tickets. Pagination gives position, a stable URL per page, and a reachable footer.

When not to

Feeds designed for drift (that’s the infinite-scroll entry, with its own honesty rules), or under ~20 rows — just show them all; a pager on one page of data is ceremony.

Behaviour & accessibility contract

The range line is a live region and always exact — never "lots" or "1000+" when the system knows the number. Previous/Next disable honestly at the ends instead of hiding. Page size changes keep the current first row visible, not reset to page one.

Platform anchor

WCAG 2.2 · 2.4.8 location, 4.1.3 status messages; honest counts per FTC deception standards (no invented totals).

Live
Order Status Total
// range line is computed, exact, and a live region: // `Showing ${start}–${end} of ${total}` // prev/next disabled at the true ends — not hidden

Data & Tables

Column Manager

Let the reader shape the table — show, hide, and keep what matters. Hiding a column hides ink, never data: the export still carries everything.

When to use

Wide operational tables where different roles read different columns. The user’s choice persists, so Tuesday’s table looks like Monday’s.

When not to

Tables under five columns (the manager costs more than it saves), or as a way to bury a column users need for an honest read — hiding the "fees" column by default is a dark pattern wearing a feature’s clothes.

Behaviour & accessibility contract

A disclosure lists every column as a real checkbox; at least one column stays on — the control refuses to empty the table. Changes announce "column hidden/shown" and headers stay in a real <th> structure for screen readers.

Platform anchor

WCAG 2.2 · 1.3.2 meaningful sequence, 1.3.1 info & relationships; FTC dark-pattern guidance (no burying material facts).

Live
Columns
Order Status Region Total
#3021 Shipped EU $182
#3022 Packed US $96
#3023 Shipped JP $240

Hidden means hidden from view — exports still include every column.

// toggling a column flips visibility on every cell in that index; // the last visible column refuses to hide. // data lives in the model — the view is just a projection

Data & Tables

CSV Export

One click, and the user owns their data — with the export honestly declaring whether it’s the filtered view or everything.

When to use

Any table the user might reasonably take elsewhere: transactions, contacts, usage. Portability is a right in consumer products, not a premium feature.

When not to

Never as a paywall for the user’s own records, and never exporting silently more than the screen showed (hidden PII "coming along for the ride") without saying so.

Behaviour & accessibility contract

Two labelled actions — "Export view" and "Export all" — each states its row count before the click. The file is generated client-side, named with the date, and completion is announced. What you were told is exactly what lands in the file.

Platform anchor

GDPR Art. 20 data portability; WCAG 2.2 · 4.1.3 status messages.

Live
Orders table47 rows total · 12 in current filter

CSV is built client-side and downloads immediately.

const csv = rows.map(r => r.map(quote).join(",")).join("\n"); const url = URL.createObjectURL(new Blob([csv], {type:"text/csv"})); // filename: orders-2026-07-17.csv — the click tells you the row count first

Data & Tables

Table States

Loading, empty, error, loaded — four truths, one silhouette. The table’s frame never moves while its story changes.

When to use

Every asynchronous table. The skeleton reserves the exact geometry; empty explains and offers one action; error says what failed and offers retry.

When not to

Skeletons for instant local data (a flash of fake loading erodes trust), and never an empty state that blames the user or an error state that says only "something went wrong".

Behaviour & accessibility contract

The region is aria-busy while loading and announces each arrival once (WCAG 4.1.3) — a screen reader hears "12 orders loaded", not a spinner. All four states share one fixed-height frame: zero layout shift. Retry is a real button, focus lands on it.

Platform anchor

WCAG 2.2 · 4.1.3 status messages, 2.4.3 focus order; Core Web Vitals CLS discipline.

Live
<div aria-busy="true">skeleton rows…</div> // four states, one fixed-height frame — zero CLS. // error names the failure and focuses the Retry button

Data & Tables

Sparkline Cells

A trend at reading speed — and an axis that tells the truth. Toggle the cropped version to watch honest data become theatre.

When to use

Dense monitoring tables where direction matters more than magnitude: uptime, spend, engagement. The sparkline is a glance; the number is the fact.

When not to

As the only representation of a value (a sparkline without its number is decoration), or with a cropped axis that manufactures drama out of a 2% wiggle.

Behaviour & accessibility contract

Every sparkline sits beside its current value and a direction word — the image is aria-hidden, the text carries the meaning (WCAG 1.1.1). The y-axis baseline is honest by default; any zoomed axis must be labelled as such. Rendered as inline SVG, no canvas tax.

Platform anchor

WCAG 2.2 · 1.1.1 non-text content, 1.4.1 use of color; FTC substantiation — a chart is a claim.

Live
Metric Trend Now
Uptime 99.7% · steady
Support load 22/day · falling

Same numbers, different axis. The cropped view is labelled — because it is a different claim.

<td><svg aria-hidden="true">…</svg> 99.7% · steady</td> // image decorates; text carries meaning. // baseline honest by default — a zoomed axis is a labelled choice

Data & Tables

Comparison Table

Plans side by side with a first column that never scrolls away and marks that read in grayscale. A comparison is a promise matrix — every cell is a claim.

When to use

Pricing pages and feature matrices where the user genuinely chooses between tiers. Pick a column to hold it highlighted while you read down.

When not to

Never against competitors with cells you can’t substantiate, and never a "recommended" column that’s just the most expensive one wearing a halo — that’s FTC territory, not persuasion.

Behaviour & accessibility contract

A real <table> with scope-correct headers; the plan column is sticky so rows stay legible on scroll. Included/not is double-encoded — ✓ plus the word, — plus the word — colour never carries it alone. Selecting a plan highlights the column and announces it.

Platform anchor

WCAG 2.2 · 1.4.1 use of color, 1.3.1 info & relationships; FTC deceptive-comparison and substantiation standards.

Live
Feature
Projects 3 20 Unlimited
Export ✓ yes ✓ yes ✓ yes
Audit log — no — no ✓ yes
Price / mo $0 $12 $29

Plus selected. Marks are glyph + word — the table survives grayscale.

<td><span>✓ yes</span></td> <td><span>— no</span></td> // double-encoded marks; sticky first column; // selected plan = aria-pressed + column class, announced once

Product & Metrics

DAU / MAU KPI Card

The numbers a product team wakes up to — with the comparison window named, the delta in words, and no axis tricks. A KPI card is a claim with a decimal point.

When to use

Team dashboards and exec views where daily and monthly active users drive decisions. The card states its window ("vs. previous 7 days"), its timezone basis, and its direction in words.

When not to

Public marketing surfaces without substantiation — a KPI card that rounds 41.2K up to "50K users" is an advertising claim, not a dashboard. And never a delta without its window: "+12%" against nothing is theatre.

Behaviour & accessibility contract

Value, delta and window are one accessible sentence ("DAU 41,280, up 4.2% versus the previous 7 days") — the arrow is aria-hidden decoration. Direction is encoded by word and glyph, never colour alone. Numbers are tabular; the card never reflows as digits change.

Platform anchor

WCAG 2.2 · 1.4.1 use of color, 1.1.1 non-text content; FTC substantiation for any externally published figure.

Live
DAU41,280▲ up 4.2%
MAU312,400▲ up 1.8%
New signups / day2,140▼ down 3.1%

Window: trailing 7 days, UTC. "Active" = performed a core action (defined below, not just an app open).

<div aria-label="DAU 41,280, up 4.2% versus the previous 7 days">… // delta always names its window; direction is a word + glyph, // never colour alone. tabular-nums so digits don’t dance.

Product & Metrics

Stickiness Ratio

DAU / MAU with the part everyone hides: what "active" means. Change the definition and watch the same product tell a different story — the definition is the metric.

When to use

Product health reviews where frequency of genuine use matters. The ratio ships with its activity definition printed beside it, so two teams can never quote two different "stickiness" numbers from the same data.

When not to

As a vanity comparison across products with different definitions of active — a meditation app and a banking app have structurally different honest ceilings. And never as a target detached from value ("raise DAU/MAU" is how streak-abuse gets designed).

Behaviour & accessibility contract

The ratio, the window, and the definition of "active" render as one unit — the definition is not a tooltip, it is part of the number. Changing the definition re-computes the ratio and announces it. The gauge is aria-hidden; the sentence carries the value.

Platform anchor

GDPR Art. 5(1)(c) data minimisation (measure what you need, define what you measure); WCAG 2.2 · 1.1.1, 4.1.3 status messages.

Live
"Active" means

13.2% DAU / MAU · trailing 28 days · active = performed a core action

Same product, same month. Loosen the definition and stickiness "improves" without a single user behaving differently — which is why the definition ships with the number.

// ratio = DAU/MAU under ONE printed definition of "active". // definition changes are announced — a metric that quietly // redefines itself is a lie with continuity.

Product & Metrics

Retention Cohort Grid

Weekly cohorts against weeks-since-signup — the table that tells you whether the product keeps anyone. Shade helps scanning; the printed percentage carries the truth.

When to use

Retention reviews and activation experiments. Each row is a signup cohort; each cell prints its percentage — the heat shading is an accelerator, never the message.

When not to

Cohorts too young to read (a 1-week-old cohort has no week-4 story — show an em dash, not a hopeful blank), and never extrapolated forward: a cohort grid is history, not a forecast.

Behaviour & accessibility contract

A real <table> with scope-correct headers; every cell prints its value so grayscale, print, and screen readers get identical truth (WCAG 1.4.1). Unknown future cells render "—" with an accessible "not yet measurable". Shading is background-only and passes contrast against its printed text.

Platform anchor

WCAG 2.2 · 1.4.1 use of color, 1.3.1 info & relationships; honest-baseline discipline — an empty cell is unknown, not zero.

Live
Cohort W0 W1 W2 W3 W4
Jun 15 100% 46% 34% 29% 27%
Jun 22 100% 48% 36% 31%
Jun 29 100% 52% 38%
Jul 6 100% 51%

Shading accelerates the scan; the printed number is the record. "—" means not yet measurable — never zero, never projected.

<td data-h="46">46%</td> <td class="na">—</td> // heat = background only; the % is always printed. // future cells are unknown — an em dash, not an estimate.

Product & Metrics

Activation Funnel

Visited → signed up → activated → retained, with absolute counts and honest step percentages — and a toggle that shows why "% of previous step" and "% of top" tell different stories.

When to use

Growth reviews and onboarding experiments. Every bar carries its absolute count; percentages state their base ("of previous step" vs. "of visitors") because the base is half the claim.

When not to

Never present a step-percentage as an overall conversion — "80% activation" that is really 80% of the 4% who signed up is how funnels lie. And no smoothed or idealised bars: the funnel is the data, not the aspiration.

Behaviour & accessibility contract

Bars are aria-hidden; a list of real numbers carries the meaning. The percentage base is printed on the toggle and announced when switched. Counts are exact — a funnel that says "~10K" when the system knows 9,412 is rounding toward a story.

Platform anchor

FTC substantiation (a conversion claim is a claim); WCAG 2.2 · 1.1.1 non-text content, 4.1.3 status messages.

Live

Base: each percentage is measured against the previous step.

// steps = [visited 48,200 · signed up 4,120 · activated 2,890 · retained 1,760] // the toggle changes the BASE, and says so — the base is half the claim.

Product & Metrics

Annotated Metric Chart

A DAU line with its causes pinned to it — releases, incidents, campaigns. A spike without its annotation is a story half-told; worse, it gets retold wrong.

When to use

Any metric chart a team will discuss later. Releases, incidents and campaigns are pinned at their dates so the chart carries its own context into every screenshot and slide.

When not to

Never annotate selectively — pinning the release before every rise but omitting the outage before every dip curates a fiction. If annotations exist, they are complete for the window shown.

Behaviour & accessibility contract

Annotations are markers plus a real text list below the chart — the list, not the hover, is the accessible record. The line is aria-hidden; a summary sentence states start, end, and the named events. Marker shapes differ by type; colour never carries it alone.

Platform anchor

WCAG 2.2 · 1.1.1, 1.4.1; SEC 17a-4-adjacent record honesty for anything a team will audit later — the chart is the minutes of the meeting.

Live
  • Jul 2 · v2.4 release — onboarding rebuilt; DAU +9% over the following week.
  • Jul 10 · 4-hour outage — auth provider incident; the dip is the outage, not the release.

The list is the record — complete for the window, incidents included. Curated annotations are curated truth.

<svg aria-hidden="true">…</svg> <ul><li>Jul 2 · v2.4 release — …</li></ul> // the text list is the accessible record; markers differ by shape

Product & Metrics

Now / Next / Later Board

A roadmap that promises sequence, not dates. "Later" has no quarter attached — a date you can't keep is a promise you shouldn't print.

When to use

Public and internal roadmaps where priorities are real but dates would be guesses. Three honest buckets: building now, next in line, on the horizon — each item carries its problem statement, not a feature codename.

When not to

When a commitment is contractual (enterprise deliverables have dates and belong in a timeline, not a mood board), or as a wish list — a Later column with forty items is a backlog wearing a roadmap's clothes.

Behaviour & accessibility contract

Columns are real lists with headings; order within a column is meaningful and stated. Items link to their explanation. Moving an item between buckets is a public event (see the changelog entry), not a quiet edit — roadmap history is part of the roadmap.

Platform anchor

FTC forward-looking claims discipline (a public roadmap is a representation); WCAG 2.2 · 1.3.1 info & relationships.

Live

Now

Next

Later

Select an item for its status note. Later items carry no dates — sequence is the promise, not the calendar.

// three lists, ordered on purpose. "Later" never carries a quarter. // bucket moves are changelog events, not quiet edits.

Product & Metrics

Confidence Timeline

Quarters on the x-axis, honesty on every bar: committed, planned, and exploring are three different promises — encoded by pattern and word, not just colour.

When to use

Roadmaps that genuinely need dates — enterprise commitments, platform deprecations, compliance deadlines. Each bar states its confidence class, and only "committed" reads as a promise.

When not to

Never paint "exploring" the same solidity as "committed" and let distance on the x-axis do the hedging — a Q4 bar drawn solid is read as a promise regardless of the legend. And never delete slipped bars: reschedule them visibly.

Behaviour & accessibility contract

Confidence is triple-encoded: fill pattern (solid / hatched / outline), a printed word on the bar, and position in the legend. Each bar is a list item to assistive tech ("Shared spaces — planned, Q4 2026"). Slips render as a strikethrough date plus the new one, never a silent move.

Platform anchor

FTC forward-looking representations; WCAG 2.2 · 1.4.1 use of color, 1.3.1.

Live
  • Offline mode — committed · Q3 2026 (contractual for enterprise)
  • Shared spaces — planned · Q4 2026 (date is intent, not contract)
  • Public API — exploring · 2027 (direction only; solid bars are earned)
// solid = committed · hatched = planned · outline = exploring // pattern + printed word + list — colour never carries confidence alone

Product & Metrics

Public Changelog

Shipped, dated, append-only. The changelog is the roadmap's receipts — the one page where the product can't exaggerate, because the feature is either there or it isn't.

When to use

Every product with a public roadmap: each shipped item gets a dated entry linking to the feature, written in outcome language ("you can now export…"), including honest entries for fixes and rollbacks.

When not to

Never edit history — a changelog whose old entries quietly improve is marketing wearing an archive's clothes. And no "improvements and bug fixes" filler: if it's worth an entry, it's worth a sentence.

Behaviour & accessibility contract

Entries are an ordered list with real <time datetime> stamps, newest first. Rollbacks and regressions get entries too — the record includes the walk-backs. Anchors per entry make every change citable.

Platform anchor

Append-only record discipline; WCAG 2.2 · 1.3.1; consumer-protection truthfulness for shipped-feature claims.

Live
  1. Data export shipped — you can now download every record as CSV from Settings. (GDPR Art. 20 in the wild.)
  2. Rolled back: smart sort — it reordered lists people had memorised. Reverted while we rethink it; your order is yours again.
  3. Offline mode in beta — 10% of users, opt-in. Known gap: attachments don't sync yet.

Append-only, rollbacks included. A changelog that only ever ships wins is a brochure.

<li><time datetime="2026-07-08">Jul 8</time> Rolled back: …</li> // history is never edited — corrections are new entries. // rollbacks get entries too; receipts include the returns.

Product & Metrics

Feature Request Board

Upvotes with an honest ending: under review, planned, or declined with a reason. Declining honestly beats ghosting — a board where nothing is ever refused is a suggestion box with a hole in the bottom.

When to use

Products that genuinely let user demand shape priority. Every request has a status; "declined" states why; "planned" links to the roadmap bucket it landed in.

When not to

Never as engagement theatre — if votes don't influence anything, the board is a dark pattern harvesting hope. And votes are one input, not a referendum: the loudest cohort is not the whole market, and the board says so.

Behaviour & accessibility contract

Vote buttons are real toggles with aria-pressed and a live count; one vote per account, reversible. Status is a word + glyph, filterable. The "how we decide" note is part of the component, not a help-centre footnote.

Platform anchor

FTC dark-pattern guidance (no fake participation); WCAG 2.2 · 4.1.2 name/role/value, 4.1.3.

Live
Calendar integration● under review
Shared spaces✓ planned · Next
Blockchain sync— declined: adds custody risk we won't take on

Votes are an input, not a referendum — the decision note ships with every status change.

<button aria-pressed="true">▲ 215</button> // reversible, one per account, announced. // declined = a reason, in public. Ghosting is the dark pattern.

Product & Metrics

Staged Rollout Status

Beta at 10%, expanding to 100% — with who's in the cohort, what's not done yet, and a visible way out. A rollout indicator is informed consent for unfinished software.

When to use

Feature flags and progressive releases that real users can see. The status names the stage, the percentage, the known gaps, and how to opt out (or in) while the flag is live.

When not to

Never use "beta" as a liability shield for a finished-and-charged feature, and never roll a cohort forward silently after telling users they could opt out — the exit must survive the expansion.

Behaviour & accessibility contract

Stage, percentage and known-gaps render as text, not just a progress bar (the bar is aria-hidden). Opt-out is a real control adjacent to the status, and the choice persists. Expansion steps are announced and dated — see the changelog entry pattern.

Platform anchor

App Store Review 2.2 (beta labelling); WCAG 2.2 · 1.1.1, 4.1.2; FTC — "beta" is a disclosure, not a disclaimer.

Live
Offline mode● beta · 10% of users

Stage 1 of 3 · 10% cohort (opt-in) · known gap: attachments don't sync. Next expansion is dated and announced, never silent.

// stage + % + known gaps as TEXT; the bar is decoration. // the opt-out survives every expansion — consent doesn't expire // because the cohort grew.

Start · Skills Kit

Skills that speak this system.

One hundred and nineteen downloadable Markdown skills for the people — and agents — who build with this catalogue, from screen composition to Dify workflow planning and generative-media pipelines. Unlike the institutional sibling's forty-skill general set, every one of these is CDS-native: it reads the machine contracts, cites the registers, and enforces the hard rules. Load one into any assistant and the assistant works the way this system works. Both kits — all 160 files — are browsable in one place at the AI Skills Library.

How these fit. A skill here is a working method bound to this system's contracts — each references components.json, tokens.json and AGENTS.md, so the method and the machine layer can't disagree. Plain Markdown: paste into any assistant, load as a Claude skill, or serve to a local model — vendor-neutral by construction. Where a skill touches law (the dark-pattern audit, AI surfaces), it says plainly that it finds design-level issues and routes legal questions to counsel. The institutional sibling's 40-skill set covers the general domains; these hundred and nineteen are the specialists — for this catalogue, for the AI tools you already run, and for the craft of design itself.

flagship · new

Organizational Memory Architect

Decision archaeology for a whole company: tickets, epics, chat, version history and release records distilled into a cited, role-scoped institutional memory — documented vs. inferred labelled, veterans confirm, InfoSec gate first, maintained by delta. Knowledge that survives attrition.

organizational-memory-architect.md
designer · dev

Screen Composer

Brief → screen, from the catalogue: selection, bundles, hard rules, token slice, human sign-off — the seven-step read path as a working method.

cds-screen-composer.md
designer · dev

Component Author

Add a component the way the existing 190 were built — register before markup, tokens only, delegated events, reduced-motion contract, real anchor.

cds-component-author.md
designer

Design Review

Crit and PR review where feedback carries a citation — every finding maps to an entry URL or rule id; blocks distinguished from opinions.

cds-design-review.md
design · product

Dark-Pattern Audit

Walk the money, the exit, the consent, and the pressure — findings a regulator would recognise, each paired with the honest alternative from the catalogue.

cds-dark-pattern-audit.md
designer · dev

Accessibility Verification

The consumer-specific WCAG 2.2 pass: targets, reduced motion, live regions, keyboard walks — verified against each component's own contract cell.

cds-a11y-verification.md
designer

UX Writing

The words carry the same contract as the components: the error formula, outcome-named buttons, symmetric consent language, zero guilt anywhere.

cds-ux-writing.md
designer · dev

Motion Choreography

One spring, three durations, self-terminating loops — including the Text Animations domain's binding usage limits and the reduced-motion rendering mode.

cds-motion-choreography.md
developer

Token & Theming

Rebrand and extend without breaking the API — values change, names don't; themes move in lock-step; AA is part of the token, verified per theme.

cds-token-theming.md
developer

Framework Port

React, SwiftUI, Compose — the deliverable is contract parity, not pixel worship: the register travels, tokens map, accessibility survives the translation.

cds-framework-port.md
designer · dev

AI Surface Kit

Sequence the 22-entry AI domain into a coherent feature: disclosure → grants → streaming → grounding → gates → traces → honest memory. Human signs.

cds-ai-surface-kit.md
designer · dev

Product Design Kit

Analyse any product surface into a tokenized Design Kit document — YAML tokens, {token.ref} component specs, falsifiable don'ts, and a mandatory Known Gaps section.

cds-product-design-kit.md
dev · product

Dify Workflow Planner

Plan LLM apps on dify.ai before touching a node — contract paragraph, three-lane graphs, model routing by node, RAG as a supply chain, human gates on the irreversible.

dify-workflow-planner.md
designer · dev

ComfyUI Pipeline Architect

Node graphs as production pipelines — five planned stages, pinned seeds and versions, VRAM budgets, workflow JSON in version control, licenses checked before commercial use.

comfyui-pipeline-architect.md
designer

Generative Image Direction

Direct generation like a photographer with a brief — stable prompt structure, style systems for brand consistency, and binding honesty rules: synthetic never masquerades as evidence.

generative-image-direction.md
designer

Generative Video Direction

Storyboard first, one shot one job, consistency locks, cut to hide the seams — and never present synthetic footage as recorded reality. Disclosure per destination platform.

generative-video-direction.md
designer · dev

AI Asset Integration

Goods-inwards for generated media — provenance intake, license verdicts, C2PA that survives the CDN, versioning like code, and the CDS disclosure surfaces that present it honestly.

ai-asset-integration.md
developer · QA

n8n Test Automation

Turn n8n into a standing test harness — scheduled smoke tests, schema-asserting HTTP checks, loud specific alerts, and the workflow JSON in version control.

n8n-test-automation.md
developer · ops

Ollama Local Deploy & VRAM Advisor

Stand up Ollama in Docker, then pick the model your GPU can actually run — a VRAM-first sizing method (params × quant + KV) with a recommendation per memory tier.

ollama-local-deployment.md
developer

Hermes Agent Orchestration

Run a Hermes-family open-weight model as an agent’s reasoning core — typed tool schemas, validated structured calls, an allowlist, and a human on every irreversible action.

hermes-agent-orchestration.md
developer · ops

OpenClaw Web Automation

Operate a computer-use agent safely — allowlisted targets, no credentials handed over, page content treated as data not instructions, human gates on every irreversible click.

openclaw-web-automation.md
writer · everyone

Monica Assistant Workflow

Get trustworthy work out of an all-in-one assistant — bring real context, edit the draft, verify every fact, keep sensitive data out of the box.

monica-assistant-workflow.md
developer

OpenCode Terminal Agent

Drive a terminal coding agent well — one bounded task, plan before edit, read every diff, tests as the acceptance gate, least-privilege on a branch.

opencode-terminal-agent.md
developer

Cline VS Code Agent

Use Cline’s Plan-then-Act as a real checkpoint, approve edits deliberately, and wire MCP at a live contract so it builds to a system it can be refused by.

cline-vscode-agent.md
ML · ops

Hugging Face Model Ops

Ship open models like an engineer — read the card before the weights, verify the licence before commercial use, pin the revision, prefer safetensors, match serving to load.

huggingface-model-ops.md
developer · platform

AWS Cloud Architect

Build and operate AWS with an agent — IaC with a reviewed plan, least-privilege IAM, private-by-default storage, cost budgets with alarms, Well-Architected, and a human on every destroy.

aws-cloud-architect.md
developer · mobile

Firebase Backend Ops

Maintain a Firebase backend safely — security rules first and Emulator-tested, App Check on real clients, idempotent Functions, read/write cost alarms, human-gated production rule changes.

firebase-backend-ops.md
developer · platform

GitHub Repo Orchestration

Run a repo like an org — PR-first, CI as the merge gate, protected main, CODEOWNERS review, fine-grained tokens, secret scanning, SHA-pinned Actions, environment-gated deploys.

github-repo-orchestration.md
developer · QA

Google Jules QA Planner

Delegate to an async coding agent and get reviewable PRs — spec the task, approve the plan, parallelize independent work, tests as the gate, and turn every edge case into a committed test.

google-jules-qa-planner.md
product · PM

Jira Project Orchestration

Run delivery in Jira as a disciplined PM — stories with testable acceptance criteria, capacity-honest sprints, JQL that surfaces risk, human-owned status transitions, tickets linked to evidence.

jira-project-orchestration.md
ops · team

Slack Ops Automation

Wire Slack as the ops nervous system without noise — specific actionable alerts, severity routing, self-assembling incident channels, in-channel approvals gated on a human click.

slack-ops-automation.md
developer

VS Code Agent Workspace

Turn VS Code into an agent cockpit — runnable tasks/debug, MCP wired to a real contract, every diff reviewed, Workspace Trust gating untrusted folders, destructive commands human-gated.

vscode-agent-workspace.md
developer · platform

Cloudflare Edge Platform

Ship and defend at the edge — Workers/Pages via previews, storage matched to access, surgical cache purges, tested WAF, Zero-Trust internal access, and DNS changes gated because they go global in seconds.

cloudflare-edge-platform.md
everyone · research

Perplexity Research

Use a citation-first answer engine as a research accelerator — follow citations to primary sources, separate grounded from synthesized, verify anything you ship, keep sensitive queries out.

perplexity-research.md
research · everyone

NotebookLM Source Curation

For an industry or domain, curate the sources worth grounding on — highest-authority URLs, peer-reviewed papers, and official first-party datasets — each scored for authority and date.

notebooklm-source-curation.md
developer · ML

NVIDIA CUDA GPU Optimization

Read the real NVIDIA hardware and size GPU rendering + VRAM budgets to fit — precision, batch, headroom — so the workload runs fully on the GPU instead of OOM-ing or spilling to CPU.

nvidia-cuda-gpu-optimization.md
designer

Graphic Design Award Research

Find award-winning graphic design from the last five years (The One Show, D&AD, TDC), verify each winner against the official archive, and dissect the philosophy and the design logic that won.

graphic-design-award-research.md
designer · dev

Awwwards Site Analysis

Analyse Awwwards SOTD/SOTM winners — design difficulty and build difficulty rated separately (★1–5), inferred tech stack, jury criteria, and 3–5 competitor sites anchored to your project.

awwwards-site-analysis.md
designer · dev

FWA Award Analysis

Mine The FWA for cutting-edge interactive work — design and build difficulty on ★1–5, inferred stack, what is genuinely innovative, and 3–5 peer sites anchored to what you are building.

fwa-award-analysis.md
design-system lead

Design System Analysis

An objective read of a design system — no hype, no hate, only facts. Ed Chen’s lens: separate fact from judgment, reconstruct the build logic and context, anchored to what you are trying to achieve.

design-system-analysis.md
growth · data

SEO & GEO Self-Audit

Turn your own site’s traffic into an honest SEO + GEO plan: fold out your own dev/QA visits, split bots from humans, read the real timezone and referrer signals, then strengthen search ranking and AI-answer discoverability.

seo-geo-self-audit.md
designer · dev

Material Design Adoption

Decide whether Material 3 fits before adopting it, then deploy it coherently — tokens, components, elevation, motion — so the whole system is used as one, not half-applied and drifting.

material-design-adoption.md
designer · dev

Ant Design Adoption

Decide whether Ant Design fits a data-dense product, then deploy it on-system: seed/map/alias tokens, ConfigProvider theming, components before custom, adherence checks against drift.

ant-design-adoption.md
designer · dev

Apple HIG Adherence

Keep an Apple-platform product on the HIG rails: enforce the non-negotiables, earn a premium feel through restraint and native conventions, and protect performance and App Store review readiness.

apple-hig-adherence.md
data · growth

Google Analytics (GA4)

Set up GA4 to answer real decisions, not collect vanity metrics: a measurement plan, consent-aware collection, bot and self-traffic filtering, and honest reading of attribution and sampling.

google-analytics.md
product · PM

Trello Project Flow

Turn a Trello board into a delivery system: lists that model the real workflow, cards with owners and a definition of done, honest WIP, and Butler automation that never fakes progress.

trello-project-flow.md
product · docs

Confluence Knowledge Base

Build a Confluence space people find and trust: architecture that matches how the team searches, templates that make good docs the default, one source of truth per topic, and a rhythm that fights rot.

confluence-knowledge-base.md
backend · data

MySQL Backend

Design and run MySQL that stays correct and safe: schema from access patterns, indexes for real queries, parameterised access, least privilege, and backup-first, human-gated destructive operations.

mysql-backend.md
data · platform

Databricks Lakehouse

Build governed, cost-controlled pipelines on Databricks: medallion + Delta, idempotent jobs with real failure handling, Unity Catalog governance and PII protection, and human-gated irreversible operations.

databricks-lakehouse.md
bot · dev

Telegram Bot Development

Take a Telegram bot from idea to listed product: Bot API surface, webhook vs long-polling, clean state and data, locale/language adaptation, rate-limit and error handling, and an architecture built for expansion and stability.

telegram-bot-development.md
bot · dev

Discord Bot Development

Build a complete Discord bot end to end: slash-command and component design, least-privilege intents and permissions, safe moderation, rate-limit and gateway handling, and a deployment architected for growth.

discord-bot-development.md
product · thinking

Notion Thinking Assistant

Use Notion as a structured thinking partner: externalise the reasoning into a shape a person and an agent both work in — the agent structures, drafts, and flags gaps while the human keeps the decision and the sign-off.

notion-thinking-assistant.md
fintech · security

Digital Wallet Architecture

Reason through a wallet before it touches real value: threat model, key management, encryption, signing integrity, and the honest limits of what software guarantees. A design method with security humility — not a guarantee; a professional audit is required before real funds.

digital-wallets-architecture.md
ops · data

Datadog Observability

Get the full value from Datadog: correlated metrics/logs/traces, SLOs and burn-rate alerts, dashboards that answer questions, and managed operation — with cost governance and PII protection as hard rules and destructive changes human-gated.

datadog-observability.md
strategy · ops

Operations & Value-Driver Analysis

Find where enterprise value is created and lost: a hypothesis-driven, MECE breakdown of the operating model into value drivers and cost-to-serve, converging on the two or three moves that matter.

mckinsey-operations-strategy.md
diagnostic · exec

Enterprise Health Score

Sync every dimension — financial, spend (software/people/office/legal/admin/product/marketing), people (attrition, satisfaction), market (acceptance, share), product (lifecycle, iteration, repeat-customer) — into one transparent, weighted health score.

deloitte-enterprise-health-score.md
finance · review

P&L & EBITDA Review

Read a P&L and build a defensible EBITDA bridge: reconcile first, normalise one-offs honestly, keep GAAP separate from adjusted, and flag what a real audit would question. Unaudited — routes to a CPA.

pwc-pnl-ebitda-review.md
risk · ERM

Multi-Lens Enterprise Risk

See risk from every angle — strategic, financial, operational, market, regulatory, technology, people, product — scored by likelihood and impact, mapped to owners and mitigations, with interdependencies named.

kpmg-enterprise-risk-analysis.md
finance · forecast

Revenue Forecast & FP&A

Build a driver-based revenue forecast a board will trust: grounded in unit economics, tested with base/upside/downside scenarios and sensitivities, honest about assumptions. A range, not false precision.

ey-revenue-forecast-fpna.md
credit · finance

Financial Health & Credit Risk

Judge financial resilience through the cycle — leverage, coverage, liquidity, business risk — into a transparent internal health grade. An internal analytical opinion, never an actual credit rating.

moodys-financial-health-rating.md
market · strategy

Market Position & Share

Where do you actually stand: defined market, triangulated sizing, share and its trajectory, competitive position, and market acceptance — sourced and labelled, not an inflated TAM.

sp-market-position-analysis.md
spend · finance

Enterprise Spend Analysis

Full visibility into where the money goes across every cost bucket, then the savings: duplicate tools, unused licenses, auto-renewals, maverick spend — a prioritised, owned savings plan that cuts waste, not growth.

coupa-enterprise-spend-analysis.md
risk · AI

AI Displacement Risk Analysis

Judge how exposed a role, product, or business model is to AI — task-level exposure, the defensible core, commoditisation not just replacement, a realistic horizon — without hype in either direction.

ai-displacement-risk-analysis.md
product · risk

Product Logic Adversarial Review

Argue against your own product bet before you commit: steelman the opposing case, red-team the plan, audit the data the claim rests on, run a pre-mortem — so weak logic surfaces in a review room, not the market.

product-logic-adversarial-review.md
org health · exec

Organizational Health (OHI & 7S)

Diagnose the org, not the financials: alignment, execution, and renewal — the health that predicts whether performance lasts — read through the OHI and 7S lens, from real organizational input, not a top-down guess.

mckinsey-ohi-7s.md
customer · org

Customer & Founder’s-Mentality Health

Read health from both ends: customer loyalty (NPS and the reasons behind it) and front-line vitality (insurgency, front-line obsession, owner mindset) — catching “big-company disease” before the numbers do.

bain-nps-founders-mentality.md
strategy · cost

Capabilities Strategy & Fit for Growth

Test whether spend matches the capabilities the strategy actually needs, then reallocate from the non-differentiating to the vital few — prune to the core, fund what wins. Reallocation, not blunt cost-cutting.

pwc-fit-for-growth.md
ops · tech

Tech & Operations Health (Process Mining)

Map the operation as executed, not as drawn: find the bottlenecks, rework loops, and cash leaks hiding in ERP, supply-chain, and cross-border flows — a fact-based read of where the operation is stuck and what it costs.

deloitte-tech-ops-process-mining.md
change · resilience

Transformation & Resilience Health

Judge readiness to absorb shock — geopolitical, technology, regulatory, market — and the capacity to actually execute the change it demands. Most transformations die on execution; this finds the gaps first.

ey-transformation-resilience.md
controls · compliance

Risk & Compliance Controls Health

Check whether the defenses actually hold: control maturity across compliance, cyber, financial, and operational domains through the three-lines-of-defense model — the gap between the policy on paper and the control in practice.

kpmg-compliance-controls-health.md
design · maturity

Design Maturity Assessment

Place a design practice on a five-level ladder — from design-as-decoration up to design-as-business-strategy — and name what caps it. Are your designers skinning finished decisions, or in the room where they’re made?

invision-design-maturity.md
UX · maturity

UX Maturity Assessment

Audit how mature a UX practice really is: the research process (real and acted on?), the design system (maintained and governed?), and quality control — placed on a staged UX-maturity scale with the ceiling named.

nng-ux-maturity.md
agency · ops

Digital Agency Operations Health

Find why a busy agency isn’t profitable: read the time-tracking data to catch scope creep (endless unpaid revisions), rank the money-losing clients, and fix the pricing and scope structure underneath.

promethean-agency-operations.md
agency · leverage

Agency Operational Leverage Health

Check whether the studio’s machine makes money: billable utilization against the healthy ~60–70% range, the non-billable overhead ratio, realized rate, and margin per biller — sustainable leverage, not burnout.

ami-agency-leverage.md
agency · benchmark

Digital Studio Peer Benchmarking

Benchmark a studio against genuine peers on the numbers that matter — compensation, pricing and rates, and margins — to see honestly whether it’s healthy or quietly falling behind the market.

bureau-digital-benchmarking.md
product · PM

Product Conflict Analysis & PRD

Review the history — templates, past PRDs, R&D records — compare a new requirement against the existing architecture to surface logic conflicts, API breaking changes, and schema risk, then write a rigorous edge-case PRD and Jira breakdown.

pm-product-conflict-prd.md
data · growth

Backend Data Growth & GEO/SEO

Read backend analytics (funnel, drop-off, retention), find unconsidered functional gaps, then strengthen both traditional SEO and 2026 generative-engine optimization (GEO) — every number from your own data.

backend-data-geo-seo.md
content · marketing

Blog Copy & AI-Tone Humanizer

Write and optimize blog content on current SEO/GEO structure, then strip the machine tone — over-narration, empty filler, AI cadence — for a natural, authoritative human brand voice, without fabricating claims.

blog-copy-humanizer.md
developer · engineering

GitHub OSS Discovery & License Audit

Scan trending and relevant repos, match the team’s real needs to existing OSS to avoid reinventing the wheel — with a mandatory license (MIT / Apache-2.0 vs copyleft GPLv3) and maintenance / security check.

github-oss-discovery-audit.md
strategy · product

Competitive Analysis & Market Strategy

A multi-dimensional competitor matrix — UI/UX, DAU/MAU, download and revenue estimates, business model — an objective gap analysis, and a 6–18 month strategy read. Figures labelled as public / inferred estimates, never a rival’s actuals.

competitive-analysis-market-strategy.md
growth · expansion

Global Expansion & GTM Strategy

Target market, regulatory limits, SWOT, TAM/SAM/SOM, ARPU, plus a GDPR/CCPA and cross-border data check and a landing playbook — estimates with stated assumptions; legal routed to local counsel.

global-gtm-strategy.md
ops · finance

R&D Resource & Cost Accounting

Two modes (5–10 team / 11–50 scale-up): headcount and ramp-up cost, engineering-hour scheduling, tech-debt ratio, training, and Cloud/DevOps FinOps (API, GPU, SaaS, build-vs-buy). Models, not audited accounting.

rnd-resource-cost-accounting.md
CRM · wealth

High-Net-Worth Client Relationship

A private-banking client-tiering and fiduciary lens for a high-stickiness experience and a consented Data Gap Strategy — progressive behavioral tagging that respects privacy. Not affiliated with UBS; not financial advice.

hnw-client-relationship.md
data · product

Analytics Event Taxonomy

From a PRD’s goals, design an event-tracking spec (name, trigger, params) and the funnel + retention logic, with a consistent naming convention, no PII in params, and consent-aware collection.

analytics-event-taxonomy.md
UX · product

User Journey & Friction Mapping

Draw the journey map, annotate the emotion curve and friction at each stage, and give conversion-lifting breakthroughs — grounded in research where available, labelled as inference where not.

user-journey-friction-mapping.md
product · PM

Agile Epic & BDD Tickets

Decompose a PRD into Epic → Story → Sub-tasks, write Given-When-Then acceptance criteria, and estimate story points — testable, owned, and team-calibrated, not imposed hours.

agile-bdd-ticket-spec.md
design · trust

Ethical Dark-Pattern & Trust Audit

Audit a flow for misleading visuals, hidden terms, and trust-damaging dark patterns; give high-transparency fixes aligned to a private-banking service standard and GDPR / FTC — legal determinations routed to counsel.

ethical-dark-pattern-audit.md
design · front-end

Accessibility (WCAG 2.1 AA) & i18n Guard

Check contrast, screen-reader labels, and full keyboard operation; review RTL and long-word multi-language overflow — citing the specific WCAG success criteria. Not a substitute for a formal audit or real AT-user testing.

accessibility-i18n-guard.md
strategy · pricing

Game Theory & Counter-Strategy

Simulate competitor retaliation — price war, copying, PR — within 1–3 months of a launch or pricing change, and pre-embed a moat and defense. Scenarios, not predictions; no price coordination.

game-theory-counter-strategy.md
comms · founder

Crisis PR & Sentiment War Room

Pre-rehearse negative sentiment and PR crises for a sensitive change or incident, and draft a response statement + support FAQ balancing compliance, sincerity, and remediation — a draft for legal review, not a cleared statement.

crisis-pr-sentiment-war-room.md
strategy · exec

Consultant-Grade Strategy Blueprint

MECE + 7S to produce an enterprise strategy, org-change plan, and a decision blueprint. Not affiliated with McKinsey; the frameworks are used as a reference lens; every figure labelled estimate or measured.

mckinsey-strategy-blueprint.md
design · front-end

Design Tokens & Component States

Decompose a design into design tokens and fill in every component state — Hover, Disabled, Focus, Loading — with Tailwind, verified against the project config; state never conveyed by colour alone (WCAG 1.4.1).

design-tokens-component-states.md
growth · experimentation

Autonomous A/B Testing & Personalization

Analyze real-time behavior, switch UI/CTA variants, track statistical significance, and roll out the winner — pre-registered and significance-gated, with a human sign-off on irreversible or high-risk changes.

autonomous-ab-testing-personalization.md
HR · hiring

Employer Branding & Talent Engineering

Track competitor product-team talent flow from public signals, then engineer the funnel: attractive, honest job descriptions and real System-Design / scenario interviews that an AI question-bank can’t game. Public signals only; fair, job-related screening.

employer-branding-talent-engineering.md
HR · people ops

Org Health & Retention Guard

Benchmark comp against the market and watch consented, team-level health signals — pulse surveys, a burnout index — to catch attrition risk early and act humanely. Aggregate and consented, never covert individual surveillance.

org-health-retention-guard.md
developer · audit

Long-Context Legacy Codebase Audit

Feed a legacy project — 100K–300K tokens of source, architecture, API specs, migrations — into a local long-context model to surface zombie code, breaking-upgrade risk, and design debt, then propose low-blast-radius refactor PRs with tests. Local, so code never leaves the machine; nothing auto-applies.

legacy-codebase-audit-long-context.md
HR · finance

Cross-Border Workforce Cost

Cost a team across two scales (5–10 / 11–50) and across geographies — salary, benefits, hardware, SaaS, recruiting, ramp-up — then model regional / remote pay premiums and the ROI of a hire before you make it. Estimates from public data; comp routed to finance and HR.

cross-border-workforce-cost.md
HR · people ops

Cultural Friction & Retention Guard

Read org health on the dimensions employees rate (balance, trust, growth, pay), surface the cross-team friction (Dev↔PM, PM↔Design) that process and KPI conflicts create, and turn early consented signals into humane retention action. Aggregate and consented, never covert surveillance.

cultural-friction-retention-guard.md
data · engineering

Python Data Science

Turn a messy CSV, API dump, or permitted scrape into a reproducible Python pipeline — clean, join with match rates reported, automate the weekly pull. Raw stays raw; every transform is code, so the number in the deck re-runs. Personal or copyrighted data routes to legal first.

python-data-science.md
stats · analysis

Statistical Modeling & Multivariate Statistics

Pick the right model — regression, PCA/clustering, hypothesis tests — check its assumptions, and report effect size with a confidence interval instead of a bare p-value. “Not significant” is a real finding; correlation is labelled correlation; causal claims need a design.

statistical-modeling.md
ML · AI

Machine Learning & AI for Business

Decide whether ML or generative AI actually beats a transparent baseline — then frame it, evaluate on a decision-tied metric sliced by segment, and keep a human on the irreversible call. Often the answer is “a simple rule wins,” and the skill says so.

ml-for-business.md
NLP · text

NLP & Sentiment Analysis

Turn reviews, social posts, and tickets into themes, aspect-level sentiment, and early-warning signals — every finding anchored to real quotes, accuracy checked against a labelled sample. Quotes over scores; aggregate and consented, never individual profiling.

nlp-sentiment-analysis.md
marketing · analytics

Marketing Analytics Science

The quantitative core of growth — CLV, multi-touch attribution, churn prediction, and value-based dynamic pricing — modelled with stated assumptions and backtested against realised cohorts. No last-click gospel, no surveillance pricing; auto-spend and price changes stay human.

marketing-analytics-science.md
psychology · growth

Behavioral Economics & Consumer Insight

Decode how consumers really decide — defaults, framing, loss aversion, social proof — as testable hypotheses, then validate the effect with data. Applied to help people choose well, never to manipulate: manufactured urgency and hidden costs are refused.

behavioral-economics-consumer.md
brand · strategy

Brand Positioning & Strategy

Build a defensible, provable position — for someone, against something — and the brand equity that earns a price premium, with an architecture that holds as you extend. Grounded in win/loss and competitor evidence, not adjectives; superlatives route to legal.

brand-positioning-strategy.md
research · insight

Market Research & Intelligence

Design research that yields decisions — unbiased surveys and sound experiments, plus interviews and observation for depth — then triangulate qual and quant into one trustworthy read. Bias control before collection; every sample and its limits named; consented, never covert.

market-research-intelligence.md
law · contracts

Commercial & Technology Contracts

Draft, review, and negotiate MSAs, SaaS terms, licensing, DPAs, SLAs, and NDAs — spotting the risk-shifting clauses (indemnity, liability caps, IP ownership, auto-renewal, data rights) and building a fallback ladder. Not legal advice; anything signed goes to counsel in the governing jurisdiction.

commercial-tech-contracts.md
law · IP

IP Strategy & Licensing

Decide what to protect and how — patent vs trade secret vs trademark vs copyright — map a portfolio to the roadmap, and structure technology licensing and cross-border deals. Frames the strategy; patentability, FTO, and filings belong to qualified IP counsel.

ip-strategy-licensing.md
law · finance

Secured Transactions & Creditors’ Rights

Structure and perfect security interests under UCC Article 9, and navigate insolvency mechanics — liquidation vs reorganization, the automatic stay, priority waterfalls. Recoveries labelled estimates; every filing and priority dispute routed to insolvency counsel.

secured-transactions-bankruptcy.md
law · competition

Antitrust & Competition Law

Surface competition-law risk before it becomes an enforcement action — per se hazards (price-fixing, market allocation, bid-rigging), merger-review exposure, and abuse-of-dominance concerns. Per se conduct is refused, never structured around; the clearance is counsel’s.

antitrust-competition-law.md
compliance · ESG

Corporate Compliance & ESG Program

Stand up a risk-based compliance program that actually runs — policies, controls, training, monitoring, whistleblower channels — plus ESG governance with substantiated disclosure. Greenwashing and unsupported claims are flagged; the legal compliance call stays with counsel.

corporate-compliance-esg.md
governance · M&A

Corporate Governance & M&A Defense

Navigate board fiduciary duties and the standards courts apply, shareholder activism, and hostile-takeover mechanics and defenses. Strategy framed within duties — not entrenchment; the defensive measures and transaction steps are executed by corporate counsel.

corporate-governance-manda.md
DevOps · infra

DevOps & CI/CD

Build delivery pipelines that ship safely — CI gated on tests, Docker images pinned by digest, right-sized Kubernetes, canary/blue-green rollout with one-action rollback and secrets hygiene. K8s is justified, not defaulted; the irreversible prod step stays human-gated.

devops-cicd.md
QA · verification

Formal Methods & Quality Assurance

Raise quality with the rigorous end of the toolbox — static analysis, property-based and contract testing, and lightweight formal specification where correctness really matters. Rigor follows risk; “verified” always names its model and assumptions, never “cannot fail.”

formal-methods-qa.md
requirements · scope

Requirements Engineering & System Scoping

Turn a fuzzy business ask into precise, testable requirements and a bounded scope — with explicit non-goals, assumptions, and acceptance criteria. If you can’t write the acceptance check, it’s still an idea; the “out of scope” list is real and specific.

requirements-engineering.md
architecture · cloud

Distributed Systems Architecture

Design cloud-native systems that hold up — making the consistency/partition/failure trade-offs explicit (CAP/PACELC), choosing serverless vs containers vs cluster by real load, idempotent by default, infra as code. Distribution is questioned before it’s adopted.

distributed-systems-architecture.md
AI · code

AI for Code & Code Intelligence

Use LLMs and coding agents where they genuinely help — refactoring, debugging, test generation, code comprehension — inside a discipline that keeps a human accountable: every diff reviewed, tests as the gate, the merge human-owned. The model produces; the engineer governs.

ai-for-code.md
AI · agents

Local Tool-Use Router

Give a local open-weight model real agent execution — a ReAct loop plus an API interceptor that lets it trigger Python, database queries, and webhooks, with schema validation, an allowlist, and a human gate on the irreversible. The model proposes the call; the harness disposes.

local-tool-use-router.md
AI · serving

Dynamic LoRA Router

Serve one base model as many specialists — an intent classifier hot-swaps task-specific LoRA adapters (PRD, code refactor, HR) at inference, with cached adapters, a confidence threshold, and a base-model fallback. Specialized depth per task, measured switch latency — not a claim of frontier general intelligence.

dynamic-lora-router.md

Start · Agent Library

The other artifact: sixty-two master agents.

The skills above are methods — how to do a piece of work. A second body of files does the opposite: each one describes a mind in enough structural detail that a model can argue from inside it rather than about it. Same artifact class as everything else here — plain Markdown, no runtime, no dependencies, no account. Written across three traditions so the format had to survive translation, not just repetition.

Why this sits next to a design system. A design system and an agent specification are the same discipline pointed at different material: decide the structure once, make every instance conform, and let the conformance be checkable rather than asserted. The counts on this page and on the library page are generated from the files themselves, not typed in. Read them together and the claim is about method, not volume. Browse the whole set — sources, structure and per-agent downloads — at the Agent Library →

Start · AI-Ready Export

Compile your Design.md pack.

One file that makes any AI tool fluent in this system: the seven-step agent protocol, the blockable hard rules, the bundled-by-contract pairs, the component index for the domains you pick, the matching skill methods, the dual-theme token set, and setup for your tool. Compiled client-side from the same live contracts agents fetch — 190 components across 17 domains and 119 skill methods. No model involved, nothing invented.

Target tool
Domains
Skill methods

Pick a category or take them all — 119 methods across 6 categories. Each ships in the pack with its download URL, grouped by category.

Pick a target and domains, then compile — the pack renders here.

Honest scope. The pack is assembled deterministically from components.json and tokens.json — the same files an MCP server would serve; if this page is opened offline the compiler falls back to reading the catalogue's own navigation and says so inside the pack. The "AI-ready Design.md package" idea has been popularised by tools like Alpy Studio — this implementation is original, and generated from a live, versioned system rather than a form.

Core

Data Table

The workhorse nobody designs and everybody ships badly. A table earns its complexity only when the data is genuinely tabular — rows you compare, columns you sort. Anything else is a list wearing a grid.

When to use

Homogeneous records the user needs to scan, sort, and compare across fields — transactions, orders, members. Right-align numbers, keep the header visible while the body scrolls, and make the sort state unmistakable.

When not to

Never for a single record's details — that is a description list, not a table. Never on a phone without a plan: a 9-column table on a 375px screen is a horizontal-scroll trap. Never encode the sorted column by colour alone.

Behaviour & accessibility contract

Real <table> semantics; sortable headers are <button>s inside <th> with aria-sort that updates, and the direction is shown with a glyph, not just a hue. Row selection has a header “select all” whose indeterminate state is real. Numbers use tabular figures so columns line up.

Platform anchor

WCAG 2.2 · 1.3.1 info and relationships, 1.4.1 use of colour, 1.4.10 reflow; WAI-ARIA sortable table pattern; HIG Tables; Material 2 data tables — Material 3 has never documented the component, which is itself the reason this entry specifies one.

Live — click a header to sort

Sort state lives in aria-sort and shows a ↑/↓ glyph — never colour alone.

<th aria-sort="ascending"><button>Spend ↑</button></th> // "select all" carries a real indeterminate state allBox.indeterminate = some && !every; // numbers: font-variant-numeric: tabular-nums

Core

Dropdown Menu

A menu is a list of actions, and it is constantly confused with a select, which is a list of values. Getting that wrong is the single most common accessibility bug in consumer software.

When to use

A set of commands triggered from one control — row actions, an overflow “…”, account options. Each item does something; the menu closes after.

When not to

Never to choose a form value — that is a <select>, and using a menu robs the user of native form behaviour. Never hide a primary, frequent action inside an overflow. Never a menu with one item — that is just a button.

Behaviour & accessibility contract

Trigger is aria-haspopup="menu" with aria-expanded; the menu is role="menu" with roving focus — arrow keys move, Enter activates, Esc closes and returns focus to the trigger. A destructive item is visually and semantically distinct, never the first focus.

Platform anchor

WAI-ARIA Menu & Menu Button pattern; WCAG 2.2 · 2.1.1 keyboard, 2.4.3 focus order; HIG Menus; Material 3 Menus.

Live — open, then arrow-key through it

Arrow keys move focus, Enter runs the action, Esc closes and returns focus to the trigger.

<button aria-haspopup="menu" aria-expanded="false">Actions</button> <div role="menu">…<div role="menuitem">…</div></div> // ↑↓ roving focus · Enter activates · Esc closes and restores focus

Core

Command Palette

The power-user shortcut that has quietly become a mainstream expectation: press ⌘K, type, jump anywhere. It rewards the keyboard without punishing the mouse.

When to use

Apps with more destinations and actions than a nav bar can hold. It is an accelerator layered over the UI, never a replacement for it — everything reachable here is also reachable by pointing.

When not to

Never as the only path to a feature — a command a user cannot discover by looking is a feature most users do not have. Never on a small, shallow app where it is ceremony. Never hide the shortcut and hope people guess it.

Behaviour & accessibility contract

The input is a combobox owning a listbox; aria-activedescendant tracks the highlighted result so a screen reader hears it without moving DOM focus. Focus is trapped while open and restored on close. Esc closes; the match count is announced.

Platform anchor

WAI-ARIA Combobox pattern (list-with-inline-autocomplete); WCAG 2.2 · 2.1.2 no keyboard trap (a deliberate, escapable one), 4.1.3 status messages; HIG Keyboard shortcuts.

Live — open it, then type

Everything here is also reachable by pointing — the palette is an accelerator, not the only door.

<input role="combobox" aria-controls="list" aria-activedescendant="opt-3"> <ul role="listbox"><li role="option" id="opt-3" aria-selected="true">…</ul> // highlight moves via aria-activedescendant — DOM focus stays in the input

Core

Tooltip

A tooltip explains; it never contains. The moment something important only exists on hover, it has vanished for every touch user and every keyboard that isn't pointing at it.

When to use

A short, supplementary label for an icon-only control, or a plain-language gloss on a term. It clarifies an element that already has an accessible name — it does not supply the only one.

When not to

Never for essential instructions, interactive content, or anything a touch user must read — there is no hover on a phone. Never as the only label for a control. Never on a delay so long the user has moved on, or so short it flickers.

Behaviour & accessibility contract

Shows on hover and keyboard focus; dismissible with Esc while the pointer stays (WCAG 1.4.13); linked by aria-describedby so it is announced. It never traps the pointer and never obscures the element it describes.

Platform anchor

WCAG 2.2 · 1.4.13 content on hover or focus, 4.1.2 name/role/value; WAI-ARIA Tooltip pattern; Material 3 Tooltips (plain).

Live — hover or Tab to it
Archive — moves it out of your inbox, keeps it searchable Pin to the top of the list

Each button already has an aria-label; the tooltip adds detail, it isn't the only name.

<button aria-label="Archive" aria-describedby="tip">⧉</button> <span role="tooltip" id="tip">Archive — keeps it searchable</span> // shows on hover AND focus · Esc dismisses · never the only label

Core

Copy to Clipboard

The smallest interaction with the highest trust stakes: it either did the thing or it didn't, and the user cannot see the clipboard. So the one rule is — never fail silently.

When to use

Any value the user needs elsewhere — a code, an API key, a share link, a wallet address. Put the control next to the value, confirm the copy visibly, and revert after a moment.

When not to

Never as the only way to get the value — the text must still be selectable. Never claim success you didn't verify: the Clipboard API can reject (permissions, insecure context), and a green tick over a failed copy is worse than an honest error.

Behaviour & accessibility contract

Success and failure are announced via role="status", not shown by colour alone. The label changes text (“Copied”), not just an icon. Focus stays on the button so a keyboard user hears the result. It awaits the promise and reports what actually happened.

Platform anchor

WCAG 2.2 · 4.1.3 status messages, 1.4.1 use of colour; Async Clipboard API (secure-context, permission-gated); HIG feedback conventions.

Live — copy it for real
sk_live_5f3a9c2e8b41

The value stays selectable, and the button tells you what actually happened — not just a hopeful tick.

try { await navigator.clipboard.writeText(v); announce('Copied'); } catch { announce('Copy failed — select it manually'); } // never a silent green tick

Core

Rating Input

Stars are a radio group in a costume. Treat them as decoration and you lock out every keyboard and screen-reader user from an action the whole product is asking them to take.

When to use

Collecting a discrete score — a review, a satisfaction rating. Show the current value in words as well as fill, and let the user change or clear it before submitting.

When not to

Never for display-only averages — that is static output, not an input, and it must not be tabbable or look clickable. Never force a rating to proceed. Never rely on hover to reveal the value on touch, where hover doesn't exist.

Behaviour & accessibility contract

A radiogroup of real radios (visually stars); arrow keys move between values, and each option has a text label (“3 stars”). The selected value is announced. Fill is never the only signal — the chosen number is stated.

Platform anchor

WAI-ARIA Radio Group pattern; WCAG 2.2 · 1.4.1 use of colour, 2.5.3 label in name; HIG Ratings and reviews.

Live — click, or arrow-key

No rating yet

<div role="radiogroup" aria-label="Your rating"> <input type="radio" name="r" value="3" aria-label="3 stars"> … // ← → moves value · the number is spoken, not just the fill

Core

Tag Input

Turning free text into removable tokens — skills, recipients, filters. The failure mode is a box that eats your entries into a black hole: added, but no way to see or remove them one by one.

When to use

Collecting a set of short, discrete values where each should be individually visible and removable. Enter or comma commits a token; Backspace on an empty field removes the last.

When not to

Never for a single value — that is a plain input. Never for a fixed, known list — that is a multi-select. Never make a committed tag impossible to remove, or removable only by clearing everything.

Behaviour & accessibility contract

Each tag is focusable with its own remove control that has a discernible name (“Remove design”, not just “×”). Adding and removing are announced via a live region. The text field keeps focus so entry flows; duplicates are rejected quietly, not silently swallowed.

Platform anchor

WCAG 2.2 · 4.1.3 status messages, 2.4.4 link/label purpose, 2.1.1 keyboard; WAI-ARIA grouping and live-region practice.

Live — type + Enter, or Backspace to remove
design research

Enter or comma adds · Backspace on an empty field removes the last.

<span>design<button aria-label="Remove design">×</button></span> // Enter/comma commits · Backspace-on-empty removes last · adds are announced if (tags.includes(v)) { announce(v + ' is already added'); return; }

Core

Relative Time

“2 hours ago” is friendlier than a timestamp and lies more easily. It is only honest if the exact time is one hover away and the reader's own timezone is respected.

When to use

Recent, human-scale events in feeds, activity, and chat — where “just now” and “5 min ago” read better than a full date. Always back it with the absolute time.

When not to

Never for anything the user must act on by a deadline — “in 3 days” hides the actual date a bill is due. Never for records or receipts, where the precise, localized timestamp is the point. Never render in the server's timezone and call it the user's.

Behaviour & accessibility contract

A <time> element carries the machine-readable datetime; the exact, timezone-correct value is available on hover and in title, so it is never lost. Text updates as time passes without stealing focus or spamming a live region.

Platform anchor

HTML <time datetime>; Intl.RelativeTimeFormat for locale; WCAG 2.2 · 1.3.1 info and relationships; GDPR-adjacent honesty — a due date is not a vibe.

Live — hover a row for the exact time
New comment on your post
Priya liked your photo
New follower

Relative on the surface, exact underneath — the real timestamp is in datetime and on hover, never thrown away.

<time datetime="2026-07-14T09:30:00Z" title="14 Jul 2026, 09:30">2h ago</time> // Intl.RelativeTimeFormat for the label · absolute value never discarded

Spatial & Motion

Motion Role Tokens

Most systems size their motion: small things move fast, big things move slowly. That is a measurement, not a meaning. These tokens are typed by what the movement is for, so a component picks a role and never picks a millisecond.

When to use

As the layer above a duration scale, not a replacement for it. --dur-1/2/3 still answer “how big is this element”. These answer “what is this movement doing to the user” — and that is the question that decides whether 380 ms feels crisp or 380 ms feels broken.

When not to

Never add a role because a screen felt slightly off; five is already near the ceiling of what a team can hold in their heads, and a sixth role is usually a component that picked the wrong one. Never let a component hard-code a duration “just this once” — that is how a system loses the ability to retune itself in one commit.

Behaviour & accessibility contract

Every role collapses to 1 ms under prefers-reduced-motion: reduce, at the token, so no component can forget. Motion is never the only carrier of a state change: each role has a non-animated counterpart in text, position or an attribute.

Platform anchor

WCAG 2.2 §2.3.3 Animation from Interactions (motion triggered by interaction can be disabled); §2.2.2 Pause, Stop, Hide. Material 3 motion scheme (duration and easing as paired tokens); Apple HIG — Motion (“use motion to explain, not to decorate”).

Five roles — pick one and watch the same object move

Pick a role to run it

Ambient is twice Navigation on purpose. A backdrop that arrives in 180 ms reads as “a layer appeared”; the same backdrop over 720 ms reads as “the room went quiet”. Same opacity, opposite meaning — and the only variable is time.

:root { --motion-nav: 380ms; --motion-nav-ease: cubic-bezier(.65,0,.35,1); --motion-ambient: 720ms; --motion-ambient-ease: cubic-bezier(.16,1,.3,1); --motion-reveal: 220ms; --motion-reveal-ease: cubic-bezier(.2,0,0,1); --motion-commit: 120ms; --motion-commit-ease: cubic-bezier(.4,0,1,1); --motion-settle: 460ms; --motion-settle-ease: cubic-bezier(.2,.9,.2,1); } @media (prefers-reduced-motion: reduce) { :root { --motion-nav:1ms; --motion-ambient:1ms; --motion-reveal:1ms; --motion-commit:1ms; --motion-settle:1ms; } } /* components name the role, never the number */ .panel { transition: opacity var(--motion-ambient) var(--motion-ambient-ease); }

Spatial & Motion

Focus Zoom

Opening an item as a camera move rather than a layer. The item you chose becomes the subject; everything else stays exactly where it was, quieter. Nothing is covered, so nothing has to be remembered.

When to use

Any surface where the user placed the items themselves — boards, canvases, galleries, maps, spatial note tools. The user built the arrangement; a modal throws it away and asks them to rebuild it from memory every time they open something.

When not to

Never for destructive confirmation, consent, payment or anything that must not be dismissed by accident — those need a real focus trap and an explicit choice, which is exactly what this pattern removes. Never on a list the system ordered rather than the user: there is no spatial memory to preserve, so the zoom is just a slow navigation.

Behaviour & accessibility contract

Escape and a second activation both return; the return lands the item back at its original coordinates, not at a re-flowed position. Attenuated peers are set aria-hidden and removed from the tab order — visually present is not the same as programmatically present, and handing a screen reader a room the sighted user has already left is worse than a modal. Cards are real controls: reachable by Tab, operable by Enter and Space.

Platform anchor

WCAG 2.2 §2.4.3 Focus Order; §3.2.3 Consistent Navigation; §2.4.11 Focus Not Obscured (Minimum). Material 3 container transform (the origin persists through the transition); Apple HIG — Navigation, on preserving a sense of place.

Click a card — then find the others
Brief
Research
Refs
Open questions
Draft
Ship

Click any card to make it the subject

The peers do not fade to nothing. They fade to a floor, and they hold their coordinates — so returning is recognition rather than re-orientation. A modal is cheaper to build and costs the user their place every single time.

/* the subject travels to centre and scales; the canvas never scrolls */ const dx = canvasRect.width/2 - (card.left + card.width/2); card.style.transform = `translate(${dx}px, ${dy}px) scale(2.4)`; canvas.dataset.zoomed = ''; /* peers: visually attenuated AND removed from the a11y tree */ peers.forEach(p => { p.setAttribute('aria-hidden','true'); p.tabIndex = -1; }); /* .csp-canvas[data-zoomed] .csp-card:not([data-focus]) { opacity: var(--csp-floor) } */

Spatial & Motion

Attenuated Context

The alternative to a scrim. Instead of covering what the user was doing, turn its volume down — and stop short of zero, because the last third of the fade is what they are using to keep their place.

When to use

For transient, reversible, non-committal surfaces: a formatting menu, an inspector, a quick-add, a preview. Anything the user will open dozens of times an hour and where re-finding their position each time is the real cost.

When not to

Never for consent, payment, deletion or any decision with a legal or destructive consequence. Those need a genuine modal with a focus trap, because “the background is still faintly operable-looking” is the exact ambiguity you must not create when the user is about to be charged. Never attenuate below the floor and then leave the content focusable — invisible-but-tabbable is a trap.

Behaviour & accessibility contract

Attenuated content is inert and aria-hidden for as long as it is attenuated: it is not readable, so it is not offered. The floor exists to preserve place, never to preserve reading — do not defend attenuated text on contrast grounds, defend it on the grounds that nobody is being asked to read it. Escape and an outside click both restore, and restoration returns focus to the control that opened the panel.

Platform anchor

WCAG 2.2 §4.1.2 Name, Role, Value (via the HTML inert attribute); §2.4.11 Focus Not Obscured (Minimum); §1.4.3 Contrast (Minimum) applies to content that is offered for reading, which attenuated content explicitly is not. Material 3 scrim guidance; Apple HIG — Modality.

Compare a scrim with an attenuation — then take the floor to zero

Notes on density

A dense screen is not a crowded screen. Density is a measure of how much of what is on the surface the reader can act on; crowding is a measure of how much of it they must first ignore.

The distinction matters because the fix is opposite in each case. A crowded screen is fixed by removal. A screen that is merely dense is usually made worse by removal, because the thing removed was the thing that made the rest interpretable.

Which is why “simplify it” is not a brief. It names a direction without naming the quantity being reduced.

Formatting — the surface you opened
--csp-floor: 0.34

Drag the floor to zero while the panel is open. The document does not become “more focused” — it becomes a different screen, and closing the panel now costs a re-orientation. The floor is not a styling preference; it is the thing being designed.

.doc { transition: opacity var(--motion-ambient) var(--motion-ambient-ease); } [data-mode="attenuate"] .doc { opacity: var(--csp-floor); /* 0.34 — never 0 */ } // attenuated is not readable, so it is not offered docEl.toggleAttribute('inert', open); docEl.setAttribute('aria-hidden', String(open));

Spatial & Motion

Anchored Control

A control that opens from the object it edits, and keeps its handle visible while it is open. The user never has to hold “which block am I changing” in their head, because the answer is drawn on the screen.

When to use

Block editors, canvas objects, table cells, layers, timeline clips — anywhere a menu acts on one specific object among several that look alike. The more uniform the objects, the more the anchor is doing.

When not to

Not for global commands: a command palette acts on the document, not on an object, and anchoring it to wherever the caret happened to be is a false claim about scope. Not when the anchor would push the menu off a small viewport — below the breakpoint, promote it to a sheet with the object’s name in the title, which is the same guarantee by other means.

Behaviour & accessibility contract

Implements the ARIA menu-button pattern: the handle carries aria-expanded, focus moves to the first item on open, Escape closes and returns focus to the handle. The handle stays rendered and visibly active for the whole open state — the connection is a persistent object, not an animation that already finished. The menu never covers the object it edits.

Platform anchor

WCAG 2.2 §1.4.13 Content on Hover or Focus (dismissable, hoverable, persistent); §2.4.11 Focus Not Obscured (Minimum); §2.4.3 Focus Order. ARIA Authoring Practices — Menu Button pattern; Material 3 menu anchoring.

Hover a paragraph for its handle — then compare with a cursor-anchored menu

A brief history of Scandinavian design.

Born in the Nordic countries, the movement held that beautiful, functional objects should be available to everyone rather than to a wealthy few.

Its clean lines and natural materials continue to define modern interiors around the world.

Switch to cursor anchoring and open two menus from different places on the same block. Nothing broke — and that is the point. The relationship is still true, it is simply no longer drawn, so the user now has to carry it.

<div class="block"> <button class="handle" aria-expanded="false" aria-haspopup="true">⋮⋮</button> <p>…</p> <div class="menu" role="menu">…</div> <!-- positioned from the handle, not the pointer --> </div> /* the handle stays lit for the whole open state */ .handle[aria-expanded="true"] { opacity: 1; background: var(--surface3); }

Spatial & Motion

Draw to Create

Creation as a direct gesture: drag out the space the thing will occupy and it exists, already focused, already the size you asked for. The neighbours yield while you are still dragging, so the outcome is visible while the decision is still reversible.

When to use

Canvases, boards, timelines, calendars, floor plans — anywhere the object’s size and position are part of what the user is deciding. A dialog asking for width and height after the fact is asking the user to describe something they could simply have drawn.

When not to

Never as the only way to create. A drag is a compound gesture: it excludes switch access, most screen-reader interaction, users with tremor, and anyone on a trackpad in a moving vehicle. Never for objects whose size is meaningless — if the system will normalise it anyway, the drag was theatre.

Behaviour & accessibility contract

A single-pointer, non-dragging alternative is mandatory and is shipped alongside, not hidden in a menu: the button creates at a sensible default size in a free position. Live dimensions are announced in text, not only drawn. Below a minimum size the gesture is treated as a click and creates nothing, so a stray drag never litters the canvas.

Platform anchor

WCAG 2.2 §2.5.7 Dragging Movements (AA, new in 2.2 — any dragging action needs a single-pointer alternative); §2.5.1 Pointer Gestures; §2.5.5 Target Size (Enhanced, AAA) with §2.5.8 Target Size (Minimum, AA) as the floor. Apple HIG — Direct manipulation; Material 3 drag-and-drop accessibility guidance.

Drag anywhere on the canvas — or use the button, which is the same feature
Team update

Drag to draw a note — the neighbour moves while you draw, not after

the keyboard path, shipped in the open

The button is not a fallback bolted on for compliance. It is placed next to the canvas at the same visual weight, because a “secondary” accessible path is a path the team will eventually forget to maintain.

// live dimensions during the drag; neighbours yield in the same frame dim.textContent = `${Math.round(W)} × ${Math.round(H)}`; neighbours.forEach(n => n.style.transform = overlaps(n) ? `translateX(${push}px)` : ''); // below the minimum, a drag is a click and creates nothing if (W > 34 && H > 26) create(L, T, W, H); // WCAG 2.2 §2.5.7 — the same feature, without the drag addBtn.onclick = () => create(defaultX, defaultY, 120, 76);

Spatial & Motion

Elevation on Commit

Shadow as a state signal rather than a decoration. An object in the user’s hand is lifted; the moment it is placed it settles back down. Elevation stops being a style token and starts answering a question: is this thing mine right now, or is it the system’s?

When to use

Drag-and-drop, reordering, resizing, any transfer of custody between the user and the layout engine. The lift is the promise that the move is not yet committed; the settle is the receipt that it is.

When not to

Never as the only indicator of the drag state — shadow is a hue-and-luminance cue and disappears at high contrast, on e-ink, and for many low-vision users. Never apply a constant elevation to every card and call it hierarchy; if everything is lifted, nothing is held.

Behaviour & accessibility contract

Elevation always ships with a non-visual partner: the destination slot is drawn, the state is exposed by a live-region announcement — not aria-grabbed, which was deprecated in ARIA 1.1, removed in 1.2, and is implemented by nothing current, and a keyboard reorder path exists. The lift is instantaneous (transition removed while the pointer is down, so the object never lags the finger); only the settle is animated, on the Settle role.

Platform anchor

WCAG 2.2 §1.4.1 Use of Color (elevation must not be the sole carrier); §2.5.7 Dragging Movements; §1.4.11 Non-text Contrast for the destination slot. Material 3 elevation levels; Apple HIG — Materials and the meaning of depth.

Drag the card — then turn elevation off and drag it again
Drag me

The shadow is the difference between holding and having placed

With constant elevation the card still moves, still lands, still works. What is missing is the moment of custody — you can no longer feel the difference between dragging and having dropped, so you check the screen instead of trusting your hand.

.card { transition: box-shadow var(--motion-settle) var(--motion-settle-ease), transform var(--motion-settle) var(--motion-settle-ease); } /* held: no transition at all, or the object lags the finger */ .card[data-lift] { transition: none; box-shadow: var(--shadow-3); transform: scale(1.04); } /* placed: the settle role gives it a little mass on the way down */ // shadow never travels alone — the slot is drawn, the state is announced slot.dataset.on = ''; live.textContent = 'Holding “Drag me”. Use arrow keys to place.';

Spatial & Motion

Shortcut-Teaching Menu

A menu that prints its own keyboard path on every row, at rest, and notices when you have taken the slow route three times. The pointer is never punished — it is simply not the only thing the interface is willing to talk about.

When to use

Any surface a user will touch dozens of times a day: block menus, command palettes, editor toolbars, triage queues. The value is compounding, so it belongs where frequency is high and the population is long-lived.

When not to

Never in a one-off flow — checkout, onboarding, a settings page visited twice a year. Teaching a shortcut for something nobody repeats is noise dressed as generosity. And never advertise a key the build does not actually honour: one dead shortcut teaches the user to stop reading the column.

Behaviour & accessibility contract

Shortcuts are shown at rest, not on hover, so they are legible to someone who never hovers. The nudge appears inline, once, and never blocks, never re-appears in the session, and never gates the pointer path. Single-character shortcuts are avoided or made remappable, because they collide with speech input and typing.

Platform anchor

WCAG 2.2 §2.1.4 Character Key Shortcuts (remappable, or active only on focus); §3.3.5 Help; §2.1.1 Keyboard. ARIA Authoring Practices — Menu and Menubar keyboard interaction.

Run one command three times with the pointer — then try its key
no runs yet

The nudge is not a tooltip and not a modal. It is a line of text that appears once, inside the surface the user already opened, and never asks to be dismissed. If the user ignores it forever, nothing degrades — which is the only version of this pattern that is not a tax.

<button role="menuitem" data-cmd="Task" data-shortkey="t"> <span>Task</span><span class="k">⌘T</span> <!-- printed at rest, not on hover --> </button> // the key is real, and using it clears the counter rather than scoring it if ((e.metaKey || e.ctrlKey) && menu.querySelector(`[data-shortkey="${e.key}"]`)) run(…); // surfaced once, inline, non-blocking, never repeated if (pointerUses >= 3 && !shown) show(`${label} also answers to its shortcut.`);

Spatial & Motion

Stack Gather

Collapsing a selection into one object without pretending the members stopped existing. The pile is legible as a pile — which, for a set of same-shaped cards, takes a deliberate rotation of between 0.8° and 3°, and nothing else.

When to use

Grouping on a canvas, collapsing a multi-select, tidying a board, archiving in place. Any time the user wants fewer things on screen but has not decided to remove any of them.

When not to

Never as a substitute for a real container with a name — a pile the user cannot label is a pile they will not find again. Never let the gather be lossy or reorder the members silently: fanning back out must return the original arrangement, or the gesture is a one-way door disguised as a tidy-up.

Behaviour & accessibility contract

The rotation jitter is seeded, not random — two people opening the same board must see the same stack, because decorative randomness is still state. The magnitude carries a floor as well as a ceiling: an unbounded ±3° range eventually deals one card 0.01°, and that card disappears into the pile it was supposed to be counted in. The count is rendered as text, never inferred from the visual depth. Under reduced motion the rotation is dropped entirely and the stack stays readable through offset alone, so the pattern degrades rather than disappearing.

Platform anchor

WCAG 2.2 §1.3.1 Info and Relationships (the group is a real relationship, exposed as one); §1.4.1 Use of Color; §2.3.3 Animation from Interactions. Apple HIG — Grouping; Material 3 selection and container patterns.

Gather, then fan back out — the arrangement survives
Kickoff
Notes
Refs
Review
Handoff

Five cards, one pile — and five is written down, not counted from the edges

seed 20260729 — identical on every load

Set the rotation to zero and the pile becomes a single rectangle: five cards that render as one. The jitter is not decoration bolted on afterwards — it is the only thing carrying the cardinality, which is why it is specified with a floor rather than improvised with a random range.

// seeded, so the same board is the same board for everyone const rnd = mulberry32(20260729); cards.forEach((c, i) => { // the magnitude needs a FLOOR as well as a ceiling: a plain ±3° range // eventually deals some card 0.01°, and that card vanishes into the pile const r0 = rnd() * 2 - 1; const rot = reduced ? 0 : Math.sign(r0) * (0.8 + Math.abs(r0) * 2.2); // 0.8–3.0° c.style.zIndex = 10 + i; c.style.transform = `translate(${dx}px,${dy}px) rotate(${rot.toFixed(2)}deg)`; }); badge.textContent = cards.length; // the count is text, never inferred from depth

Mobile

Safe Area & Insets

The screen is not the rectangle you were given. A notch, a Dynamic Island, a home indicator and a rounded corner all take bites out of it, and every one of them is a place where a control is visible but cannot be pressed.

When to use

Any full-bleed surface: edge-to-edge media, sticky headers, bottom bars, sheets, and anything drawn behind translucent chrome. Background art extends into the inset; anything interactive or readable stops at it.

When not to

Never hard-code the numbers. A device you have not seen yet will have different ones, and the same device reports different insets in landscape, in split view, and while a call is in progress. Read them, do not type them.

Behaviour & accessibility contract

Interactive targets clear the home-indicator band entirely so a tap is never eaten by the system gesture. Insets are re-read on rotation and on multitasking changes, not measured once at launch. Text that reflows into an inset is a 1.4.10 failure, not a cosmetic one.

Platform anchor

HIG Layout (safe areas, Dynamic Island, home indicator); CSS Environment Variables Level 1 · env(safe-area-inset-*); Material 3 window insets; WCAG 2.2 · 1.4.10 reflow.

Live — turn the insets off and watch the button move under the home indicator
Inbox
Delivery confirmed
Weekly summary
Two new replies

Insets respected — Compose clears the home indicator by 22px.

/* read the inset, never type it — and add your own padding on top */ .screen { padding-top: env(safe-area-inset-top); padding-bottom: calc(env(safe-area-inset-bottom) + 8px); } /* background art may cross the line; targets may not */ .hero-image { margin-top: calc(-1 * env(safe-area-inset-top)); }

Mobile

Keyboard Avoidance

The software keyboard takes half the screen and gives no warning. The contract is that the field you are typing in, the thing you are typing about, and the button that submits it are all still visible when it arrives.

When to use

Every screen with a text field: composers, checkout, search, comment threads, forms. The keyboard is not an edge case on mobile — it is the second half of every input interaction.

When not to

Do not solve it by shrinking the page into an unreadable strip, and do not animate the lift on a timer. Follow the real inset event; a guessed 260px is wrong the moment a hardware keyboard, a floating keyboard, or a language with a candidate bar appears.

Behaviour & accessibility contract

WCAG 2.2 · 2.4.11: the focused field is never fully hidden by the keyboard or by the sticky bar riding above it. Scroll position is adjusted, not the layout torn apart. Dismissal restores the previous scroll offset so the reader does not lose their place.

Platform anchor

HIG Onscreen keyboards; Android WindowInsetsCompat · IME insets and adjustResize; VisualViewport API; WCAG 2.2 · 2.4.11 focus not obscured (minimum).

Live — raise the keyboard and watch what stays on screen
Can you send the invoice?
Sent it this morning.
Nothing here yet.
Checking now — one minute.
Message

Keyboard down — IME inset 0px.

/* follow the real inset; never hard-code a keyboard height */ .composer { padding-bottom: env(keyboard-inset-height, 0px); } // or, where env() is not available yet visualViewport.addEventListener('resize', () => { const inset = innerHeight - visualViewport.height; root.style.setProperty('--kb', inset + 'px'); focused.scrollIntoView({ block: 'nearest' }); // 2.4.11 });

Mobile

Edge-Swipe Back

The system owns the left edge. Everything you put there — a carousel, a slider, a swipeable row — is competing with the gesture people use to leave, and the gesture wins.

When to use

On every pushed screen. The gesture should be interactive and reversible: the previous screen follows the finger, and letting go below the threshold puts the current screen back without navigating.

When not to

Never place a horizontally draggable control inside the edge zone, and never disable the gesture to protect unsaved work — that is what a save prompt is for. Disabling back is how an app earns a force-quit.

Behaviour & accessibility contract

WCAG 2.2 · 2.5.7: a visible Back control does everything the gesture does, because the gesture excludes switch access, keyboards, and anyone who cannot drag. The destination is announced on arrival, and a cancelled swipe announces nothing.

Platform anchor

HIG Gestures (interactive back gesture, system edge protection); Android predictive back; WCAG 2.2 · 2.5.7 dragging movements, 2.1.1 keyboard.

Live — drag the current screen right from its left edge, or press Back
Library
Field notes
Interviews
Field notes
Drag me from the left edge. Release past 40% to commit; release short and I spring back.

Detail screen. Edge zone is 28px — nothing draggable lives there.

/* keep your own horizontal drags out of the system edge */ .carousel { margin-left: 28px; } /* or start the track past the edge zone */ // the gesture is interactive, and it is never the only way back onDrag(dx => card.style.transform = `translateX(${Math.max(0, dx)}px)`); onRelease(dx => dx > width * 0.4 ? pop() : springBack()); backButton.onclick = pop; // 2.5.7 · same result, no dragging

Mobile

Thumb Reach

A phone held in one hand has a comfortable arc, a stretch, and a corner most people re-grip to reach. Where you put the primary action decides whether the interface is used walking, or only sitting down.

When to use

Whenever a screen has one obvious next step. Put it in the bottom band, full width or near it, and leave the top for identity, title, and things that are read rather than pressed.

When not to

Not for destructive actions, which belong away from the resting thumb and behind a confirm or an undo. And not as an excuse to stack four bottom bars: the reachable band is scarce, so it holds one decision.

Behaviour & accessibility contract

WCAG 2.2 · 2.5.8: 24×24 CSS px minimum, and this system holds 44×44 for anything used in motion. Reach is a comfort argument, never a correctness one — every bottom action also exists somewhere a keyboard, a switch, or a screen reader can find it.

Platform anchor

HIG Layout (bottom-anchored controls, Reachability); Material 3 bottom app bar and FAB placement; WCAG 2.2 · 2.5.8 target size (minimum).

Live — move the primary action and watch which zone it lands in
Order summary
2 items · delivery Tuesday
Pay with saved card

Primary action sits in the easy zone — 44px tall, reachable without re-gripping.

/* one decision in the reachable band, and it clears the indicator */ .primary-bar { position: sticky; bottom: 0; padding-bottom: calc(env(safe-area-inset-bottom) + 8px); } .primary-bar button { min-height: 44px; } /* 2.5.8 floor is 24; walking needs 44 */

Mobile

Dynamic Type

Someone has already chosen their text size, system-wide, before they ever opened your app. Honouring it is not an accessibility feature — ignoring it is a bug that hides a third of your users' content.

When to use

Everywhere text appears. Scale from the system setting, size containers in relative units, and let rows become stacks when the text no longer fits beside the thing it labels.

When not to

Never cap the scale to protect a layout, and never truncate a label to one line at the largest sizes. Fixed-height rows, single-line buttons and icon-plus-label pairs that refuse to wrap are where this breaks.

Behaviour & accessibility contract

WCAG 2.2 · 1.4.4: readable and operable to 200% without loss of content or function; 1.4.10 reflow at 320 CSS px; 1.4.12 survives user text spacing. Accessibility sizes reflow rows to stacks rather than clipping, and the bold-text setting is honoured alongside the size.

Platform anchor

HIG Typography (Dynamic Type, larger accessibility sizes); Material 3 font scaling and non-linear scale; WCAG 2.2 · 1.4.4 resize text, 1.4.10 reflow, 1.4.12 text spacing.

Live — drag to the accessibility sizes and watch the row become a stack
Delivery window changed
Tuesday, 09:00–12:00

Default size (1.00×) — row layout, nothing truncated.

/* scale from the system setting, and let the layout change shape */ body { font: -apple-system-body; } /* iOS: tracks Dynamic Type */ .row { display: flex; gap: 10px; } @container (min-width: 0) { /* or a scale query */ .row:has(> .label:only-child) { flex-direction: column; } } /* buttons grow with the text, never clip it */ .cta { min-height: 2.8em; height: auto; white-space: normal; }

Mobile

Lifecycle & State Restoration

A mobile app is interrupted constantly and killed silently. The measure of quality is what the user finds when they come back: the same screen, the same scroll position, the same half-written sentence.

When to use

Any flow longer than one screen, and every text field with more than a few words in it. Persist on backgrounding, not on a timer, and restore to the exact screen rather than to the root of the app.

When not to

Do not restore anything sensitive without re-authentication, and do not restore a modal that was mid-payment as if nothing happened. Restore the place, re-verify the permission.

Behaviour & accessibility contract

WCAG 2.2 · 3.3.7: information the user already entered is never asked for twice. 2.2.6 warns before any inactivity timeout that would discard data. Restoration is announced only if the screen changed under them; silent, correct restoration needs no announcement.

Platform anchor

UIScene state restoration and app lifecycle (HIG); Android lifecycle, SavedStateHandle and process death; Page Visibility API; WCAG 2.2 · 3.3.7 redundant entry, 2.2.6 timeouts.

Live — background the app, then cold-start it, with restoration on and off
New report · step 2 of 3
The lift on level 3 has been out since Monday and the notice on the door has no date on it.
state: active · draft held
cold start…

Active. The draft exists in memory and on disk.

// persist when the system tells you it might kill you — not every keystroke document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') save(draft); // last safe moment }); // restore to the screen, then re-verify anything sensitive const restored = load(); if (restored) { goTo(restored.route); if (restored.sensitive) reauth(); }

Mobile

Low Power & Data Saver

Battery saver, data saver and reduced motion are all the same instruction in different words: this person has told the system to spend less on their behalf. An app that ignores it is spending someone else's money.

When to use

Anything continuous: autoplay, polling, background sync, live location, decorative animation, prefetch. Degrade the expensive layer and keep the function — the content still arrives, it just stops arriving unasked.

When not to

Never degrade correctness. A saver mode that drops a message, skips a security check, or silently stops an upload is not thrift, it is a defect wearing a battery icon.

Behaviour & accessibility contract

prefers-reduced-motion collapses movement to opacity; prefers-reduced-data drops poster-weight images and autoplay. Every degradation is visible and reversible in one tap, and the user is told what changed rather than left to notice.

Platform anchor

iOS Low Power Mode and HIG Motion (reduce motion); Android battery saver and Data Saver; Media Queries Level 5 · prefers-reduced-motion, prefers-reduced-data; Network Information API saveData.

Live — turn the saver on and read exactly what was given up
Live scores
autoplaying highlight
Second half · 62'
Next update in 5s

Full mode — autoplay on, polling every 5s, live dot animating.

/* the platform already asked on the user's behalf — just listen */ @media (prefers-reduced-motion: reduce) { .pulse { animation: none } } @media (prefers-reduced-data: reduce) { .hero { background-image: none } } // degrade the cost, never the content const thrifty = navigator.connection?.saveData || lowPowerMode; poll.interval = thrifty ? 60_000 : 5_000; video.autoplay = !thrifty; // still one tap away

Mobile

Orientation & Foldables

The window can change size mid-sentence — a rotation, a fold opening, a second app sliding in beside you. The layout is allowed to change shape; the user's place in it is not.

When to use

Design to width classes, not to device names. Compact stacks, medium splits, expanded shows list and detail together — and a fold or a rotation simply moves you between them.

When not to

Lock orientation only where the content genuinely has one — a camera viewfinder, a game, a signature pad. Locking a reading or form screen to portrait is a 1.3.4 failure and a hardship for anyone whose device is mounted.

Behaviour & accessibility contract

WCAG 2.2 · 1.3.4: content works in both orientations unless one is essential. Scroll position, selection, focus and in-flight input survive the change; a re-layout that restarts a form is data loss with an animation on it.

Platform anchor

HIG Layout (multitasking, split view, foldables); Material 3 window size classes and adaptive layouts; Device Posture API; WCAG 2.2 · 1.3.4 orientation, 1.4.10 reflow.

Live — change the window and check the selection survives
Field notes

Selection is carried across the layout change, not rebuilt from the top.

Compact — one pane, detail pushes. Selected: Field notes.

/* size classes, not device names */ .shell { display: grid; grid-template-columns: 1fr; } @media (min-width: 600px) { .shell { grid-template-columns: 260px 1fr; } } // survive the change: keep selection and scroll in state, not in the DOM onResize(() => render({ selectedId, scrollTop })); // never reset to root

Mobile

Live Activity

A task that is already running, shown where the phone is already being looked at: the lock screen. It earns that place by being finite, by ending, and by never needing the app to be opened.

When to use

Bounded, user-initiated, genuinely live: a delivery, a ride, a timer, a match, an upload. There is a start the user asked for, a middle that changes, and an end that arrives on its own.

When not to

Never for marketing, re-engagement, or anything open-ended. An activity that has no end state is a permanent advertisement on someone's lock screen, and it will be revoked — by the user, then by the platform.

Behaviour & accessibility contract

WCAG 2.2 · 4.1.3: each update is a status message, not a focus steal. The compact and expanded presentations carry the same facts; the expanded one adds detail, never new meaning. It ends in a terminal state and dismisses itself.

Platform anchor

HIG Live Activities (Dynamic Island, lock screen) and widgets; Android ongoing notifications and foreground services; WCAG 2.2 · 4.1.3 status messages, 2.2.4 interruptions.

Live — start it, advance it, and watch it end by itself
9:41
Tuesday 4 August
No live activity
Courier: Nadia · van 4 · two stops before yours. Same facts as the compact view, more of them.

Idle. Nothing is running, so nothing is on the lock screen.

// started by the user, ended by the system, never open-ended const act = await LiveActivity.start({ id, compact, expanded }); for await (const update of stops) act.update(update); // 4.1.3 status, not focus act.end({ state: 'delivered', dismissAfter: 60_000 }); // it removes itself