v1.0.0
Portfolio

Consumer Design System · v1.0.0 · 2026

Built for products that billions of people touch.

A business-to-consumer design system: 90 component categories across ten domains — commerce, social, media, onboarding, growth, trust. 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.

90Categories
10Domains
2Themes
AAWCAG 2.2 baseline
0Dark patterns
Institutional sibling

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 not affiliated with or endorsed by any platform. 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.

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

One spring curve everywhere. Motion explains where things came from and where they went — and 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 machine-readable files 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. Logos identify third-party tools for context; no affiliation or endorsement is implied.

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 — eds-mcp serves 65 contracts through 23 tools. These contracts share the same shape, so a cds-mcp server is a thin adapter over the same two files.

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

What a tool call sees

Request

        
Response

        

Honest scope. The two JSON files are real and downloadable today. The tool calls above are an illustrative console showing how these contracts back an MCP server — the pattern is proven in production by the author's eds-mcp (23 tools, 65 contracts) for the institutional system; a consumer-system server would serve these files the same way. Payload shapes here are simplified for reading.

Start · Changelog

Changelog

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

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 keep ≥ 24px of clear space between them even when visuals are smaller (WCAG 2.5.8); 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 */ }

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.

The working set
search
home
save
share
comment
bag
alerts
play
profile
settings

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.1 perceivable redundancy principle.

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

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 (keyboard alternative required), 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 7(2) — unbundled, unticked consent; 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.

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 3 banners; WCAG 2.2 · 4.1.3 status messages, 1.4.1 use of color.

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

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 6 purpose limitation.

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; FTC Negative Option Rule (clear disclosure before billing).

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 junk-fee rulemaking (all-in pricing); 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 display rules (last four only); card-network brand guidelines; WCAG 2.2 · 3.3.8 (no card-number memory tests).

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.5.7 dragging/press alternatives; 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

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

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

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.

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.

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.4 (review suppression); App Store Guideline 1.1.7 & SKStoreReviewController limits; 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.

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.

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

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

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

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

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.

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

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

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

FTC Negative Option Rule (click-to-cancel); App Store 3.1.2 / Play subscription cancellation rules; EU Consumer Rights Directive.

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>