CARGO
back to the workshop

// reference · cargo/06

UI Pattern Library

A library of the UI patterns people building for the web get wrong. Every entry does the hard part: when to use it, when not to, what to reach for instead, shown with a real interactive example you can feel.

18 / 18

// overlays

Modal dialog

A window layered over the page that blocks interaction with everything behind it until it is dismissed.

when to use

A focused decision or short task that genuinely must be resolved before the user continues: confirming a destructive action, a short required form, an interruption the user must acknowledge before anything else makes sense.

when not to use

Transient messages ("Saved", "Copied"), error or validation feedback, loading states, or anything complex enough to deserve its own page. A modal for a success toast traps the user in a dialog to tell them everything worked.

use instead

A toast for transient confirmation (it informs without blocking), an inline error for validation (it sits next to the field that is wrong), a dedicated page for any flow with more than a couple of steps.

live example
Delete projectdestructive · irreversible
Save settingstransient confirmation

// content

Pagination vs infinite scroll

Two ways to reveal a long list: fixed pages with explicit controls, or one stream that keeps loading as you scroll.

when to use

Pagination when users need to find, return to, reference, or bookmark a specific position: search results, data tables, anything where "the third result" or "page 4" is a real thing a user needs to get back to. Infinite scroll when the content is a casual feed built for open-ended browsing and no single item needs to be found again.

when not to use

Infinite scroll when users need the footer (it keeps running away), when they need to find an item again (position is not addressable), or when losing your place mid-list is costly. Pagination when the content is a lean-back feed where page boundaries are pure friction.

use instead

This entry is the decision itself: choose by whether position has to be addressable. If a user will ever say "it was about halfway down" or needs the footer, paginate. If they will only ever scroll and skim, infinite is fine.

live example

position: ?page=1

#1Result 1 — "responsive grid layout"example.com/article-1 · 2 min read
#2Result 2 — "responsive grid layout"example.com/article-2 · 3 min read
#3Result 3 — "responsive grid layout"example.com/article-3 · 4 min read
#4Result 4 — "responsive grid layout"example.com/article-4 · 5 min read
#5Result 5 — "responsive grid layout"example.com/article-5 · 6 min read
#6Result 6 — "responsive grid layout"example.com/article-6 · 7 min read

// content

Optimistic vs pessimistic UI

When a user acts, does the interface update immediately and reconcile if the server later fails (optimistic), or wait for the server to confirm before showing any change (pessimistic)?

when to use

Optimistic for frequent, low-stakes actions that almost always succeed and are cheap to reverse: liking, toggling, reordering, marking read. Pessimistic for high-stakes or failure-prone actions: payments, irreversible changes, anything where a false "success" followed by a silent revert would mislead the user about the real state.

when not to use

Optimistic when a wrong-then-reverted state is dangerous or confusing (a payment that "went through" then quietly did not). Pessimistic for a like button, where a spinner on every tap makes a fluid interaction feel broken.

use instead

This entry is the tradeoff itself: perceived speed against honesty about failure. Pick optimistic when the lie is brief and harmless; pick pessimistic when being wrong, even for a second, costs the user trust.

live example
Dana Okoro@danabuilds · 2h

Shipped the new grid system today. Small thing, but it finally feels right.

    event log
  • tap the like button…

// overlays

Toast / notification

A small, transient message that appears briefly and dismisses itself, without blocking the interface.

when to use

Confirming a low-stakes action succeeded ("Saved", "Copied", "Link sent"), or brief non-critical status the user does not have to act on. It informs in passing and gets out of the way.

when not to use

Anything the user must act on. A toast dismisses itself before a slower reader finishes it, so an "Undo" or a decision living only in a toast is a thing the user can simply miss. Not for errors that need a choice, and not for critical information.

use instead

A persistent inline banner or a modal for anything that requires action (it stays until resolved), and an inline error for validation (it sits with the field and does not time out).

live example
Share linklow-stakes · nothing to act on
Delete itemaction-required · undo matters

// overlays

Tooltip

A small text label that appears on hover or focus, giving a brief hint about an element.

when to use

A short, supplementary, text-only hint: naming an icon-only button, clarifying a control. Non-essential information the UI is fine without.

when not to use

Essential information (tooltips are hidden by default and unreliable on touch and keyboard), anything interactive (a tooltip cannot hold a button or link, it closes the moment you move toward it), or anything more than a few words.

use instead

A popover for interactive or richer content (it is click-triggered and stays open so its controls are reachable), and visible inline text for anything essential.

live example
tooltip done right — a hint for an icon-only control
hover, focus, or tap the icon
now put an action inside it — tooltip cannot hold a button, popover can

// disclosure

Accordion

A set of stacked sections, each expandable, usually with one or few open at a time.

when to use

Genuinely optional or secondary content, or a long page of independent sections the user wants one at a time (FAQs, advanced settings). The collapse earns its keep only when most sections are noise to most users.

when not to use

Content users need to compare side by side (collapsing it forces them to expand, memorise, collapse, expand, comparing from memory), content short enough that the expand click is pure friction, or primary content that should simply be visible.

use instead

Tabs when the sections are peers the user switches between (still one at a time, but framed as equals), and just showing the content when it is short, primary, or has to be compared. An accordion that hides what the task needs on screen together is fighting the user.

live example

Task: which plans include SSO?

plans you can compare at once: 0 of 3. An accordion hides what the task needs side by side. To answer, you expand one, remember it, collapse, expand the next. The pattern is fighting the task.

// input

Inline validation

Form validation that gives feedback as the user fills a field, rather than only on submit.

when to use

Fields where early feedback genuinely helps and can be checked mid-flow: a username's availability, a password meeting visible rules, a confirm-password match. Fired at the right moment, it saves a round trip.

when not to use

Validating on every keystroke (an email marked invalid while the user is still three characters into typing it is hostile, it is wrong about an unfinished field), or fields whose correctness cannot be known until submit.

use instead

The same inline validation, fired on blur rather than on keystroke (quiet while typing, helpful once the user leaves the field), and submit-time validation for anything that cannot be meaningfully judged mid-entry. The skill is not whether, it is when.

live example
nags you mid-entry — hostile

// content

Skeleton vs spinner

Two ways to show a loading state: a skeleton placeholder that mimics the layout that is coming, or a spinner indicating indeterminate activity.

when to use

A skeleton when the content has a known structure that will populate (a feed, a dashboard, a profile, a list) so the wait reads as "almost here". A spinner for short or structureless waits, or an action in progress, where there is no layout to preview.

when not to use

A skeleton for a very short wait (it flashes in and out and reads as a glitch) or for content whose layout is unknown (a skeleton that does not match is worse than none). A spinner for a long structured load, where it is just a blank wait.

use instead

This entry is the choice itself: match the loader to the wait. Known layout and a real wait, skeleton; short or shapeless, spinner. See the Loading States tool for the loaders themselves.

live example
Dana Okoro@danabuilds · Product designer

Rebuilt the settings screen around one decision per view.

The grid system finally feels right after three tries.

Shipped on a Friday. It is fine. The tests are green.

Reload with a known layout: the skeleton reads as "almost here", the spinner as a blank wait. Try the 200ms action in skeleton mode to feel the flash. see the Loading States tool for the loaders themselves →

// overlays

Popover

A small overlay anchored to an element, holding richer or interactive content, shown on click.

when to use

A short, optional, interactive cluster anchored to a control: a small menu of actions, a compact filter, a brief form. Click-triggered and persistent so its controls are actually reachable.

when not to use

Essential always-needed content (it is hidden until triggered), large or complex content that overflows the bubble and breaks the anchor, or a plain non-interactive hint.

use instead

A tooltip for a non-interactive hint (simpler, hover or focus, no controls), a panel or a page for large or complex content, and inline content for anything essential. The popover is not the container for everything.

live example

A short, optional, interactive cluster anchored to its trigger: this is exactly what a popover is for. (A plain text hint would be a tooltip, not this.)

// disclosure

Dropdown menu

A control that reveals a list of options or actions on click, collapsed by default.

when to use

Many options (a country list), or secondary and overflow actions, or when space is genuinely tight and the options do not need to be compared. Collapsing the list is the whole point, and it earns the click.

when not to use

A small set of mutually exclusive options (2 to 5) the user should see and compare at once, a primary choice central to the task, or anywhere hiding the options behind a click is pure friction with nothing gained.

use instead

A segmented control or radio group for a small, visible, comparable set, and plain visible buttons for a primary action. The same dropdown that is wrong for three options is right for two hundred — the test is whether collapsing buys anything.

live example
Billing period — 3 options, central to the task

Three mutually exclusive options that matter to the task, hidden behind a click. You cannot weigh them without opening the menu. A dropdown adds friction and hides the comparison.

Country — ~200 options, secondary

Two hundred options no one compares side by side. Collapsing them behind a click is exactly the dropdown's job — the same pattern that was wrong above is right here.

// input

Segmented control vs dropdown

Two ways to present one set of mutually exclusive options: all visible as a segmented control, or collapsed into a dropdown.

when to use

A segmented control when the options are few (2 to 5), short-labelled, and benefit from being seen and compared at once. A dropdown when they are many, long, or secondary, where always showing them only costs space.

when not to use

A segmented control once the set grows past a handful or the labels get long (it becomes an unscannable wall that eats the layout), and a dropdown for the two or three options central to a decision (it hides what should be compared).

use instead

This entry is the decision itself, and it pairs with the dropdown entry from the other side: choose by option count and label length. Few and short, show them; many or long, collapse them. The threshold is a handful.

live example
options in this setting:
segmented control
dropdown

At three short options the segmented control wins outright: every option visible, comparable, one tap, no hidden state. The dropdown here only adds a click.

// input

Multi-step form / wizard

A long form broken into sequential steps with progress, rather than one long page.

when to use

A genuinely long or complex form where steps reduce overwhelm, where inputs have a natural order or branch, or where visible progress reassures the user through a heavy task.

when not to use

A short form, where steps add clicks and hide how little is actually left, forms where users want to see everything or jump around, and above all padding a four-field form into a three-step wizard for the look of it.

use instead

A single well-grouped form for anything short, and a single page with clear sections when users need the overview. Steps are a cost that only a genuinely long or branching form earns back.

live example
clicks to submit: 0

Four fields, one screen, one click, the end in sight. Steps only earn their keep on a genuinely long or branching form (think 20+ fields or conditional sections) where one page would overwhelm.

// content

Confirmation dialog vs undo

Two ways to protect against mistakes: ask the user to confirm before an action, or let it happen with an easy undo.

when to use

Undo for most reversible actions: it is frictionless and trusts the user. A confirmation dialog for the genuinely destructive and irreversible, where one deliberate moment of friction is worth it.

when not to use

A confirmation on routine reversible actions. Constant "are you sure" dialogs train the user to click through them without reading, so the dialog stops protecting anything precisely when you needed it to.

use instead

This entry is the choice: friction-now versus reversible-later. Default to undo and reserve the confirmation for the irreversible, so that when a dialog does appear it still carries weight.

live example
confirmed 0×
Q3 report.pdf
budget.xlsx
notes.md
logo.svg
draft.txt

A confirmation on every routine delete. Friction now, every time. Delete a few in a row and watch what the habit does to it.

// disclosure

Progressive disclosure / "show more"

Revealing secondary content or controls only when the user asks, keeping the default view simple.

when to use

Advanced or optional settings most users never touch, long content where a preview plus an honest "show more" respects the reader, and genuinely secondary detail that would only clutter the common path.

when not to use

Hiding content users routinely need (the reveal click becomes constant friction), hiding so much that the default view is uninformative, or using disclosure to disguise an overloaded interface instead of actually simplifying it.

use instead

Just show the content when most users need it, and give genuinely large secondary content its own page or section. Disclosure earns its place only when most users would skip what it hides.

live example

task: change your email address

The fields people actually touch are visible; a rarely-needed cluster waits behind an honest "show advanced". The default view is simple but complete.

// disclosure

Tabs

A row of labels that switch between peer panels in the same space, one visible at a time.

when to use

A few peer sections of the same object that users view one at a time (a product's description, specs and reviews; a settings page), where no two sections need to be seen together.

when not to use

Content users must compare across sections (tabs hide all but one, forcing memory, this is the accordion problem from the tab side), a sequence that is really a wizard, too many tabs (the content wants another structure), or primary content that should just be visible.

use instead

Show the content together when sections must be compared or are short, a multi-step form for a sequence, and sub-navigation or separate pages for many distinct areas.

live example

One product, sections you read one at a time

A 13-inch ultralight laptop. Aluminium unibody, 1.1kg, built for travel.

Description, Specs and Reviews are peers of one object and nobody needs two at once. One at a time in shared space is exactly what tabs are for.

// input

Search-as-you-type

A search field that filters or suggests results live as the user types, rather than on submit.

when to use

A bounded, fast, local dataset where instant filtering genuinely helps: filtering a visible list, a command palette, anything where feedback is immediate and predictable.

when not to use

Slow or expensive queries where every keystroke fires a request (wasteful and janky), large or remote datasets where live results lag or mislead, or when the user is better off composing a full query before searching.

use instead

A submit or search button for expensive or remote queries, and debounced search-as-you-type as the middle ground when live feedback is wanted but each query has a real cost.

live example
requests fired: 0idle
Dashboard
Settings
Billing
Members
API keys
Webhooks
Audit log
Integrations
Notifications
Security
Domains
Usage

A bounded local list, filtered live. Zero network, instant feedback — search-as-you-type at its best.

// content

Empty state

The designed state of a screen or section when it has no content yet: a new account, a cleared list, no results.

when to use

It is not optional. Any view that can be empty has an empty state whether you design it or not. The only question is whether it is a designed moment or an accident.

when not to use

A literal blank space (the user thinks it is broken or unfinished), an empty state that only states the emptiness ("No items") with no path forward, or a discouraging dead end where a first run could have been an onboarding moment.

use instead

An empty state that does a job: orient the user, explain why it is empty, and offer the next step. Treat the first-run empty state as onboarding, a doorway, not a dead end.

live example
Projects 0 total

A dead end that looks broken. Nothing tells the user this is empty by design rather than failing or still loading.

// content

Drag-and-drop

Letting users move or reorder items by dragging them directly.

when to use

Direct manipulation that genuinely fits the task: reordering a short list, moving a card between columns, where dragging is intuitive and the items are few enough to drag comfortably.

when not to use

As the only way to perform the action. Drag is invisible (nothing signals an item is draggable), hard or impossible on touch and keyboard, and clumsy for long lists. Drag-only locks out anyone who cannot or does not discover the drag.

use instead

Keep the drag as an enhancement, but make the same action reachable another way: up and down controls, a "move to" menu, a position input. The genuine tension is drag-as-only-affordance versus drag-plus-a-visible-fallback, and the fallback is not optional.

live example
Inbox
Drafts
Sent
Archive

Drag is the only way. No grip, no buttons, an ordinary cursor: nothing signals these move, and there is no touch or keyboard path. The interaction is invisible and, for many users, impossible.