Browse pages
Form and Field Patterns
This page shows how each field kind takes its value to create or change records such as expenses, trips, and payouts, and how a form reports a refused submit or unsaved work: money, record pickers, dates, text, single choices, switches, uploads, periods, slugs, capacity, colors, repeated groups, tags, addresses, and message templates. Each answer is drawn as options with a named default to build, and the product owner chooses among the options when the team builds or revises a form. One decided shape per field keeps entry fast and errors fixable, so staff type once and repair only what the message names.
Stack: any
Layer: frontend-ui
Concepts#
How to Read a Pattern#
Each pattern answers one recurring question about a field kind, such as how a money field names its currency. It states the question, the default option a project builds, how the default behaves on a narrow screen, and why the default won. Each option then shows one wireframe drawing with its own rationale, cost, and optional reference. A project records its choice in its pattern catalog and builds the chosen option with its own components. The UI patterns page states the catalog contract and the wireframe language.
The Running Example#
The drawings on this page use the Claimwick expense Ava files for the Rotterdam client visit: the EUR 45.00 hotel night, its booking fee, the Amsterdam sales cost centre, and the trips, payouts, and approval policies around them. On this page, you means the staff member entering values. The Standards section states the two rules every option keeps: every field carries a visible label naming its value, and a form keeps every entered value when the API refuses a submit.
Standards#
Name Every Field with a Visible Label#
ruleruleguidancelitespec/frontend/patterns/forms-and-fields#name-every-field-with-a-visible-labelEvery field MUST carry a visible label that names its value.
Why
A placeholder disappears on the first keystroke, so a filled field with no label no longer says what it holds. Screen readers announce the label, and sighted staff returning to a filled form read it to check each value before saving. Without a visible label, both groups guess what each value means.
Checka reviewer answers
Reviewed forWhich built input lacks a shown label naming its value? Open every form on built screens and read each field's label.
Marked guidance: a reviewer answers this question.
Keep Entered Values After a Refused Submit#
ruleruleguidancelitespec/frontend/patterns/forms-and-fields#keep-entered-values-after-a-refused-submitA form MUST keep every entered value when the API refuses a submit.
Why
Clearing the form after a refusal forces staff to retype every value to fix one field, which wastes time and adds new typos. Keeping all values lets the reader repair only the field the message names and submit again.
Checka reviewer answers
Reviewed forWhich built form drops typed values after a refused submit? Open the refused submits on built forms and check every field.
Marked guidance: a reviewer answers this question.
Patterns#
Money Field#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#money-fieldQuestion The back office holds amounts, fees, reversals, and payout figures, and staff type every one of them; the software chooses none. The field is where that rule meets a keyboard. The decision is how a money field takes its value: text input shape, currency naming, symbol prefix, error wording, and totals restated beside it.
Default text-input-currency-in-the-label-symbol-inside
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The HMRC prefix with the currency in the label stays the pick. The prefix renders in light and dark themes. It names the currency to a screen reader while hiding the symbol from it. It refuses nothing you can type.
The payout total restatement is the runner-up. It belongs on every amount the approver sees. It stops the fee from surprising either side at payout. Never ship the stepper. You cannot reach a continuous amount by stepping.
Reviewed for Which built amount input breaks from the wireframe its catalog entry chose? Open the built amount, currency, and total fields beside the catalog entry recording the chosen option and its reason.
text-input-currency-in-the-label-symbol-insideText Input, Currency in the Label, Symbol InsideDefaultpage
breadcrumb "Home" "Expenses" "New expense"
header
heading "Lines"
text "What each night costs, and what the payout adds on top."
region "Hotel night"
field "Nightly amount, in euros" number = "45.00"
text muted "45,00 or 45.00. Up to two decimal places."
field "Booking fee added, in euros" number = "5.00"
text muted "Added at payout. The budget pays this one."
Rationale The HMRC pattern places an aria-hidden prefix inside the field. It names the currency in the label. A screen reader hears the currency even though it does not read the symbol. This is not a number input. GDS rejected the number type outright.
Cost You can type the symbol into the field, so validation has to accept it rather than refuse it. Allow at most two decimal places, per the HMRC pattern.
Reference HMRC
currency-as-a-separate-selectCurrency as a Separate Selectpage
breadcrumb "Home" "Expenses" "New expense"
header
heading "Lines"
text "What each night costs."
region "Hotel night"
row
field "Nightly amount" number = "45.00"
field "Currency" select = "EUR"
text muted "45,00 or 45.00. Up to two decimal places."
row
field "Booking fee added" number = "5.00"
field "Currency" select = "EUR"
Rationale The amount and the currency are two inputs, so a company claiming in several currencies changes one select rather than retyping an amount.
Cost No design system in this set publishes a currency select beside an amount, and the company pays out in one currency. A select for a closed set of one is a control that cannot do anything.
amount-with-a-unit-suffix-for-a-countAmount with a Unit Suffix, for a Countpage
breadcrumb "Home" "Expenses" "New expense"
header
heading "Lines"
text "What each night costs."
region "Hotel night"
row
field "Nightly amount" number = "45.00"
text "per night"
row
field "Booking fee added" number = "5.00"
text "per expense"
Rationale The GOV.UK suffix pattern uses a word rather than a symbol. Per night, per expense, and per kilometre read better than a symbol where the unit is not money.
Cost The suffix takes horizontal room in a narrow field, and it cannot carry a currency. Money and a unit would need two suffixes, which no pattern here supports.
Reference GOV.UK
money-with-an-error-that-says-what-to-doMoney with an Error That Says What to Dopage
breadcrumb "Home" "Expenses" "New expense"
header
heading "Lines"
text "What each night costs."
region "Hotel night"
field "Nightly amount, in euros" number = "45.000"
message critical "Use at most two decimal places, like 45.00"
field "Booking fee added, in euros" number = "45.00"
text muted "Whole euros are fine here."
Rationale The same field appears in its refused state. The message follows the GOV.UK template. It reads as a specific instruction rather than a rejection.
Cost You cannot see the three states at once, so the text states which one the wireframe shows. Each wireframe here is one state.
Reference GOV.UK
amount-with-a-stepperAmount with a Stepperpage
breadcrumb "Home" "Expenses" "New expense"
header
heading "Lines"
text "What each night costs."
region "Hotel night"
row
action quiet "One euro less"
field "Nightly amount, in euros" number = "45.00"
action quiet "One euro more"
text muted "Steps of one euro."
Rationale A plus and a minus sit on either side of the value. They cover the case where one amount is the common one. Small differences are the exception.
Cost NN/g and Carbon both warn against a stepper for an amount. You cannot reach a continuous value by stepping. You cannot express EUR 1.50. This is the wrong control for this field.
money-as-a-read-only-fact-with-an-edit-commandMoney as a Read-Only Fact with an Edit Commandpage
breadcrumb "Home" "Expenses" "EXP-1042"
header
heading "EXP-1042"
status "Paid" positive
action primary "Message spender"
region "What was claimed"
fact "Hotel night, 2 nights" "EUR 90.00"
fact "Booking fee per expense" "EUR 5.00"
fact "Paid to the spender" "EUR 125.00"
action "Change a priced line"
Rationale Once a line has been priced, the page shows it as a figure with one command to change it. You never retype the value, so a mistyped amount cannot become a second record.
Cost Editing moves to a second surface for a field you touch often. A line already priced is a versioned fact rather than a field.
price-with-the-customer-facing-total-restatedAmount with the Payout Total Restatedpage
breadcrumb "Home" "Expenses" "New expense"
header
heading "Lines"
text "What each night costs, and what the payout adds on top."
region "Hotel night"
field "Nightly amount, in euros" number = "45.00"
text muted "45,00 or 45.00. Up to two decimal places."
field "Booking fee added, in euros" number = "5.00"
fact "Payout sends" "EUR 50.00"
Rationale The money reads more clearly. You type what the night costs and read what the payout sends on the same screen before saving. The EUR 50.00 total figure stops the fee from surprising either side at payout.
Cost The restated total must update as you type, which a static wireframe cannot show. It costs one line under every amount field, even where the fee is zero.
gross-price-with-the-net-and-tax-restatedGross Amount with the Net and Tax Restatedpage
breadcrumb "Home" "Expenses" "New expense"
header
heading "Lines"
text "What each night costs, and what the payout adds on top."
region "Hotel night"
field "Nightly amount, in euros" number = "45.00"
fact "Of which VAT at 21 percent" "EUR 7.81"
fact "Net counts against the budget" "EUR 37.19"
region "Booking fee"
field "Booking fee added per expense, in euros" number = "5.00"
fact "Of which VAT at 21 percent" "EUR 0.87"
Rationale Stripe Dashboard restates every gross amount as net plus tax under the field. You claim the gross amount. You read what counts against the budget.
Cost The restated lines update as you type. The rates come from configuration keyed by date. An amount from January restates with January rates. This costs two lines per field even where you never look twice.
Reference Stripe
preset-price-chips-for-the-common-price-pointsPreset Amount Chips for the Common Amountspage
breadcrumb "Home" "Expenses" "New expense"
header
heading "Lines"
text "What each night costs."
region "Hotel night"
row
action "25.00"
action "35.00"
action primary "45.00"
action "60.00"
action quiet "Custom"
field "Nightly amount, in euros" number = "45.00"
fact "Payout sends with the booking fee" "EUR 50.00"
Rationale Square and Shopify POS put the common amounts one tap away. Most lines land on a round amount, and typing 45.00 ninety times a year is ninety chances to type 54.00. The field stays for the odd amount.
Cost The presets are company data that must be learned or configured, and a stale set pushes you toward last year's amounts. Five chips plus a field is also wider than a field alone on a phone.
Choosing a Record#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#choosing-a-recordQuestion You choose a reference to another record from a picker showing names. Nobody types an identifier. The failure to avoid is a select reading "Choose a cost centre" beside a field holding "CC-0C893968A2" as if it were a value to type.
Default searchable-picker-showing-names
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The searchable picker over names stays the pick: nobody types an identifier, and search beats scrolling for every list longer than a screen. The picker with one distinguishing fact per row is the runner-up for records that share a name. Never ship a bare identifier field: a code such as CC-0C893968A2 is not a value you can type or check.
Reviewed for Which built record picker breaks from the wireframe its catalog entry chose? Open the built searchable pickers and recent lists beside the catalog entry recording the chosen option and its reason.
searchable-picker-showing-namesSearchable Picker Showing NamesDefaultpage
breadcrumb "Home" "Expenses" "New expense"
header
heading "New expense"
text "Start with a name."
region "Which budget pays"
field "Cost centre" search = "amster"
list 3
item: text "Amsterdam sales" · text muted "EUR 10,000"
text muted "A cost centre carries its budget and its spend."
field "Name" text
Rationale It searches over names and shows nothing else. Time zone research from NN/g is the reason it searches. In that research 44 percent of first searches were by city. Search beats scrolling every time.
Cost A picker hides how many records exist. If you know the cost centre is "Zaandam", you still have to search rather than scan. It needs an empty-result state that offers creating one.
picker-with-the-record-s-facts-beside-its-namePicker with the Record's Facts Beside Its Namepage
breadcrumb "Home" "Settings" "Approval policies"
header
heading "Add an approval policy"
text "An approval policy is what finance picks and what the payout follows."
region "Copy from"
field "Copy from an existing policy" search
list 3
item: text "Standard" · text muted "9,412 decided, active"
field "Name" text = "Travel"
Rationale Each option carries the one fact that tells two similar records apart. Budget suits a cost centre and date suits a trip. State suits an approval policy. You tell two cost centres both called Rotterdam apart by what they hold.
Cost The list grows wider, and the extra column is only useful for records that share a name. For a record set where names are already unique it is noise.
a-select-of-namesA Select of Namespage
breadcrumb "Home" "Expenses" "New expense"
header
heading "New expense"
text "Start with a name."
region "Which budget pays"
field "Cost centre" select = "Choose a cost centre"
field "Name" text
Rationale It is a plain select listing names. It has fewer moving parts than a picker, and it works with the keyboard with no extra setup.
Cost The GOV.UK guidance says to reduce the option count before reaching for a select. Research finds selects hard to use. The guidance forbids pre-selecting an option. A 40-cost-centre select is the case that guidance is about.
Reference GOV.UK
a-picker-whose-options-are-typedA Picker Whose Options Are Typedpage
breadcrumb "Home" "Expenses" "New expense"
header
heading "New expense"
text "Start with a name."
region "Which budget pays"
field "Cost centre" text = "rotter"
list 3
item: text "Rotterdam sales"
text muted "Choose from the list. Nothing is picked until you do."
field "Name" text
Rationale You type part of a name and the list narrows, but you must choose from the list. APG warns that list autocomplete with automatic selection silently adopts the first suggestion, which stores the wrong record.
Cost The field must not commit on blur or on Enter alone, or a mistyped prefix charges the wrong cost centre. That is an implementation rule the wireframe cannot show.
Reference APG
a-read-only-reference-showing-what-is-linkedA Read-Only Reference Showing What Is Linkedpage
breadcrumb "Home" "Settings" "Approval policies"
header
heading "Standard"
status "Active" positive
action "Retire policy"
region "What this policy belongs to"
fact "Trip" "Rotterdam client visit"
fact "Cost centre" "Amsterdam sales"
fact "Applies to" "All trips"
action "Change what is linked"
Rationale On a record rather than a form, the page shows the relation as a fact. It carries a command to change it. You see what is linked without opening a picker.
Cost Changing a relation takes two steps, and on a page with several relations it takes several such facts. It belongs on a record page, not on a create form.
a-picker-that-also-createsA Picker That Also Createspage
breadcrumb "Home" "Expenses" "New expense"
header
heading "New expense"
text "Start with a name."
region "Which budget pays"
field "Cost centre" text = "Zaandam"
message critical "There is no cost centre called Zaandam."
action primary "Create a cost centre called Zaandam"
action "Choose from 8"
field "Name" text
Rationale The list ends with the command to make the missing record. If you need a new cost centre, you are never sent away. You make one and come back to the form.
Cost The field opens a second surface, and a dialog must never open another dialog. Creating a cost centre has to be a page, which means losing the contents of the form.
recent-records-above-the-searchRecent Records Above the Searchpage
breadcrumb "Home" "Expenses" "New expense"
header
heading "New expense"
text "Start with a name."
region "Which budget pays"
field "Cost centre" search
text "Recent"
list 2
item: text "Amsterdam sales" · text muted "EUR 10,000"
text "All cost centres"
list 2
item: text "Utrecht sales" · text muted "EUR 1,100"
Rationale Linear opens every picker on the records you touched last. The cost centre for the next expense is almost always the cost centre of the last one. Recent first turns the common case into one tap. It leaves search for the rest.
Cost Recent is per person, so two people see two lists and a screenshot never matches twice. It also needs an empty state for the first expense, where nothing is recent yet.
Reference Linear
results-grouped-by-state-with-countsResults Grouped by State with Countspage
breadcrumb "Home" "Settings" "Approval policies"
header
heading "Add an approval policy"
text "An approval policy is what finance picks and what the payout follows."
region "Copy from"
field "Copy from an existing policy" search
text "Active (2)"
list 2
item: text "Standard" · text muted "9,412 decided"
text "Retired (1)"
list 1
item: text "Travel" · text muted "0 decided"
Rationale GitHub command palette groups results under a heading with a count, so a list of thirty reads as three lists of ten. Grouping policies by state keeps a retired policy from being copied by mistake.
Cost The groups are one more opinion about the records. A group the picker invents has to stay in sync with the states the records hold. This costs a heading row per group on a phone.
Reference GitHub
Date and Time Field#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#date-and-time-fieldQuestion Staff enter dates and times for trips, payouts, and trip plans. The decision is which control takes the value: three text inputs, one field with a calendar, date and time side by side, relative entry, or presets. The native browser date input is refused: it draws the browser chrome instead of the application control, and GDS rejected it for implementation problems across browsers.
Default one-text-field-with-a-calendar-that-opens-beside-it
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale One typed field with a calendar beside it stays the pick. There is always a way to type. The calendar serves you when you would rather click.
The three GDS inputs are the runner-up where accessibility review demands them. They cost the slowest entry in the form. Never ship the native date input. It draws the browser chrome instead of the application control. It renders differently everywhere.
Reviewed for Which built calendar input breaks from the wireframe its catalog entry chose? Open the built date, duration, and range fields beside the catalog entry recording the chosen option and its reason.
three-text-inputs-with-a-format-hintThree Text Inputs with a Format Hintpage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
text "Start with a name."
region "When it runs"
row
field "Day" number = "14"
field "Month" number = "3"
field "Year" number = "2026"
text muted "For example, 14 3 2026"
row
field "Start hour" number = "08"
field "Start minute" number = "00"
text muted "In the Europe/Amsterdam time zone."
Rationale The GOV.UK answer to a date uses three fields. It uses numeric inputmode and autocomplete tokens. The hint reads "For example, 31 3 1980". Every browser renders it the same.
Cost Three fields hold one value, and if you enter 94 dates a year you type 282 numbers. This is the most accessible answer and the slowest one.
Reference GOV.UK
one-text-field-with-a-calendar-that-opens-beside-itOne Text Field with a Calendar That Opens Beside ItDefaultpage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
text "Start with a name."
region "When it runs"
field "Trip starts" date = "14/03/2026 08:00"
text "March 2026"
grid 31
item: text "14"
text muted "Times are in the Europe/Amsterdam time zone."
field "Trip ends" text = "14/03/2026 12:00"
Rationale You type into one field, with a calendar beside it if you would rather click. The APG combobox date picker pattern is exactly this. The Carbon rule is that there must always be a way to type.
Cost A calendar is a grid of 42 cells to build, translate, and test in two languages. It is the most code in the form package for one field.
Reference APG
date-and-time-on-one-row-the-time-zone-named-onceDate and Time on One Row, the Time Zone Named Oncepage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
text "Start with a name. All times are Europe/Amsterdam."
region "When it runs"
row
field "Start date" date = "14/03/2026"
field "Start time" text = "08:00"
row
field "End date" date = "14/03/2026"
field "End time" text = "12:00"
message critical "The end must be after the start."
Rationale Date and time sit side by side as two fields, with the time zone stated once above them rather than repeated under each. That means fewer repetitions of the same fact.
Cost On a narrow screen two fields plus the row wraps to three lines. You have to read a time zone that is stated once before the second field. It does not sit next to that field.
relative-dates-for-the-near-future-absolute-for-the-farRelative Dates for the Near Future, Absolute for the Farpage
breadcrumb "Home" "Trips" "Rotterdam client visit"
header
heading "Add to the trip plan"
text "Entries are sorted by their start time."
region "The entry"
field "What" text = "Meet the client"
field "When" select = "Relative to departure"
field "How long before" number = "4"
text "Departure is at 08:00 on Sat 14 Mar 2026, so this is 04:00."
Rationale A trip plan is written in relative language: "departure at 08:00", "check-in 4 hours before". One field takes a duration and the page calculates the clock time from the trip date.
Cost The page derives an absolute time and shows it, so you never hold it in your head. A relative field cannot express the anchor itself: departure at 08:00 must come from the trip date.
a-duration-field-for-a-length-of-timeA Duration Field for a Length of Timepage
breadcrumb "Home" "Settings" "Policies"
header
heading "Receipt grace period"
text "How long a spender has to file a receipt before the expense is refused."
region "The hold"
row
field "Wait for the receipt" number = "5"
field "Unit" select = "days"
text muted "Past 5 days, more receipts never get filed."
field "Remind the spender before it ends" select = "1 day before"
Rationale For a grace period, a timeout, or a hold, you know five days, not a date. One field takes the number and a unit sits beside it. The NN/g rule says the step and the unit stay explicit.
Cost Two fields do the work where one number would do, and they cannot express "2h30". A single field parsing both needs error wording for every way a duration can be wrong.
the-date-refused-with-the-date-that-was-wrongThe Date Refused, with the Date That Was Wrongpage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
text "Start with a name."
region "When it runs"
field "Trip starts" date = "14/03/2026 26:00"
message critical "Trip starts must be a time between 00:00 and 23:59. You entered 26:00."
field "Trip ends" date = "14/03/2026 08:00"
message critical "The trip must end after it starts. You entered 08:00."
Rationale The same field appears refused, with the GOV.UK error wording. It names the field and says what to do. It says what was entered and what is wrong. You do not have to remember what you typed.
Cost The message repeats the value, which makes it longer than the field. The message fits above the input, not inside it.
Reference GOV.UK
preset-chips-for-the-usual-dispatch-timesPreset Chips for the Usual Departure Timespage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
text "Start with a name. All times are Europe/Amsterdam."
region "When it runs"
row
action "Fri 08:00"
action primary "Sat 08:00"
action quiet "Custom"
field "Trip starts" date = "14/03/2026 08:00"
field "Trip ends" text = "14/03/2026 12:00"
Rationale Calendly puts the usual lengths above a custom field because most meetings are thirty minutes. Departure times cluster the same way around 08:00. Two presets plus Custom covers the week without typing.
Cost The presets are company data that goes stale when the season changes. A preset that names a date needs a new value every week. A stale chip offers last month mornings.
Reference Calendly
start-and-end-as-one-range-with-a-length-readoutStart and End as One Range with a Length Readoutpage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
text "Start with a name. All times are Europe/Amsterdam."
region "When it runs"
row
field "Trip start" date = "14/03/2026 22:00"
field "Trip end" date = "15/03/2026 06:00"
fact "Length of the trip" "8 hours"
Rationale Airbnb and Linear ask for a range as one control with the length read back. A start without an end is not a value the page can use. The readout catches the overnight mistake before the submit does.
Cost Two values in one field need one error message that names both, and a screen reader meets a composite where it expected a field. Wire the labels with care.
Reference Airbnb
Text Field and Its States#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#text-field-and-its-statesQuestion Every text field draws several states: empty, filled, focused, refused, disabled, and read-only. The label stays visible above the input in all of them. A placeholder is never the label, because it vanishes on the first keystroke.
Default label-above-help-under-six-states-on-one-page
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The six-state reference page stays the pick, because it is the only option that pins every state one field must draw. Inline validation that confirms a good value is the runner-up for create forms, where it saves a refused submit. Never ship the placeholder as the label: it vanishes on first keystroke, and its color misses contrast.
Reviewed for Which built text input breaks from the wireframe its catalog entry chose? Open the built labels, counts, and growing areas beside the catalog entry recording the chosen option and its reason.
label-above-help-under-six-states-on-one-pageLabel Above, Help Under, Six States on One PageDefaultpage
breadcrumb "Home" "Settings" "Field states"
header
heading "Field states"
text "Every state one text field has to draw."
region "The states"
field "Empty" text
field "Filled" text = "Rotterdam client visit"
field "With help" text = "Rotterdam client visit"
text muted "Spenders see this on the expense and in the payout."
field "Refused" text = "Spring offsite"
message critical "A trip is already scheduled as Spring offsite."
field "Read only" text = "EXP-1042-01"
text muted "Issued by the system when the line was made."
Rationale The component reference page for the field: default, filled, focused, error, disabled, and readonly. Showing them together is how you check one control rather than six screenshots.
Cost Six instances of one field is a style guide, not a screen. It never appears in the product. It exists so the states cannot drift apart.
label-above-placeholder-as-a-hintLabel Above, Placeholder as a Hintpage
breadcrumb "Home" "Settings" "Approval policies"
header
heading "Add an approval policy"
region "The policy"
field "Name" text = "Travel"
text muted "Spenders see this on the expense and in the payout."
field "What it includes" textarea
field "Internal note" textarea
text muted "Not shown to spenders or to approvers."
Rationale The label names the field and the placeholder shows an example. Both are present. That is the GOV.UK recommendation once a hint is genuinely needed.
Cost A form field carries two sets of words, so a form with five fields carries ten lines before the first value. The hint has to earn its place every time.
Reference GOV.UK
placeholder-as-the-labelPlaceholder as the Labelpage
breadcrumb "Home" "Expenses"
header
heading "Expenses"
text "Every paid expense, newest first."
toolbar
table 3 "Expense" "Spender" "Paid"
item: text "EXP-1042" · text "Ava" · text "EUR 125.00"
Rationale The search field has no label. The only description of what the search covers is a placeholder reading "Expense number, name or email".
Cost It fails the GOV.UK guidance on three counts at once. It vanishes on first keystroke. Not every screen reader reads it. Its default color often misses 1.4.3 contrast.
Reference GOV.UK
character-count-under-a-long-answer-fieldCharacter Count Under a Long-Answer Fieldpage
breadcrumb "Home" "Settings" "Policies"
header
heading "Approval policy"
text "Spenders read this before they file."
action primary "Save"
region "The policy"
field "Policy" textarea = "Expenses above EUR 150 need a receipt."
text muted "148 characters of 1,500"
Rationale Where a field has a hard limit, the remaining count sits under it. GOV.UK says count characters only where its own research shows it helps.
Cost GOV.UK permits live counting as a narrow exception to its no-live-validation rule, because it judges a field before it is finished. On a policy text the count is worth it. On a name it is not.
Reference GOV.UK
a-textarea-with-the-customer-s-words-preservedA Textarea with the Spender's Words Preservedpage
breadcrumb "Home" "Settings" "Policies"
header
heading "Approval policy"
text "Spenders read this before they file."
action primary "Save"
region "Preview for spenders"
text "Expenses above EUR 150 need a receipt before approval."
text "Without one, finance refuses the expense instead."
text muted "This is how it appears to a spender, with the line breaks kept."
Rationale The page shows long answers with the line breaks you entered. A policy pasted from a document keeps its paragraphs instead of reflowing into one block.
Cost Uncontrolled line breaks in stored text are a rendering hazard on a spender page. The width there differs from the width of the editor. Normalize it somewhere.
a-field-that-grows-as-the-answer-doesA Field That Grows as the Answer Doespage
breadcrumb "Home" "Expenses" "EXP-1042"
header
heading "Record a dispute"
text "What happened on the trip, in your words."
region "The dispute"
field "What happened" textarea
field "Where it happened" select = "Rotterdam office"
field "Who decided" text
footer
action "Cancel"
action primary "Record it"
Rationale The textarea expands with its content up to a limit, so a dispute note is not a 74-pixel window with a scrollbar inside it.
Cost A growing field pushes everything below it down as you type, which moves the button you are about to press. Most implementations stop growing at about six lines.
inline-confirmation-of-a-good-valueInline Confirmation of a Good Valuepage
breadcrumb "Home" "Settings" "Approval policies"
header
heading "Add an approval policy"
text "Checked as you type, so a good value is confirmed before the submit."
region "The policy"
field "Name" text = "Standard"
message positive "Standard is free to use."
field "What it includes" textarea
footer
action "Cancel"
action primary "Add the policy"
Rationale Stripe Checkout and Vercel confirm a valid value where you typed it. A tick and a quiet line mark it, instead of waiting for the submit. Confirming Standard early saves you a round trip for the name you wanted.
Cost GOV.UK permits live validation only as a narrow exception, because it judges a field before it is finished. The check must wait for a pause in typing, or it nags.
Reference Stripe
floating-labels-that-shrink-when-filledFloating Labels That Shrink When Filledpage
breadcrumb "Home" "Settings" "Approval policies"
header
heading "Add an approval policy"
text "Two fields drawn the Material way, for comparison."
region "The policy"
field "Name" text
text muted "Empty: the label sits where the value will go."
field "Name" text = "Standard"
text muted "Filled: the label shrinks above the value and stays visible."
footer
action "Cancel"
action primary "Add the policy"
Rationale Material Design answers the placeholder problem by floating the label above the value once the field fills, so the label never vanishes. It keeps the compact look the placeholder promised without breaking it.
Cost The empty state still reads as a placeholder, with its small text and low contrast. A floated label at 10.5 px is smaller than any label the back office otherwise draws. The GOV.UK guidance keeps the label above and full size for that reason.
Reference Material Design
Choosing One of a Few Options#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#choosing-one-of-a-few-optionsQuestion You choose from a closed set, such as a naming rule or a plan. The GOV.UK rules say a select is a last resort and radios beat a select. Nothing is pre-selected in a question, though settings allow it. The decision is which control carries the choice: radio list, segmented control, cards, select, checkbox list, listbox, or plain buttons.
Default a-radio-list-with-a-description-per-option
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The radio list with a description per option stays the pick. Each choice carries the sentence that lets you choose between names that sound alike. The segmented control is the runner-up for two or three short values with no descriptions.
Never ship the native multi-select. GOV.UK rules it out for its usability and assistive record. A checkbox list replaces it.
Reviewed for Which built single-choice control breaks from the wireframe its catalog entry chose? Open the built radios, segments, and plan cards beside the catalog entry recording the chosen option and its reason.
a-radio-list-with-a-description-per-optionA Radio List with a Description per OptionDefaultpage
breadcrumb "Home" "Expenses" "Receipts"
header
heading "Receipt naming"
text "Finance reads the name on each file."
action primary "Save"
region "Naming rule"
field "No name" radio
text "A receipt is filed without a name on it."
field "The spender name" radio
text "The spender name is on the file."
field "Every line named" radio
text "The spender names each line before filing."
Rationale Each option carries a sentence saying what it means for the spender. That sentence is the only way you can choose between "The spender name" and "Every line named".
Cost Three options at 22 pixels each is a tall block, and it does not scale to twelve options, which a permission picker does need.
a-segmented-control-for-two-or-threeA Segmented Control for Two or Threepage
breadcrumb "Home" "Expenses" "New expense"
header
heading "New expense"
text "Start with a name, or start from a template."
region "Start from"
row
action primary "A blank expense"
action "A template"
field "Name" text
field "Cost centre" select = "Choose a cost centre"
Rationale The choice is between two or three values that are all visible at once. A segmented control shows every option. It hides none of them behind a menu.
Cost Labels have to fit, so "A blank expense" and "A template" are both shortened. The control grows with its longest label.
plan-cards-for-a-priced-choicePlan Cards for a Priced Choicepage
breadcrumb "Home" "Settings" "Plan"
header
heading "What the plan costs"
text "The company is on Business. Changing plan takes effect from the next month."
region "The plans"
card "Starter"
figure "EUR 0 a month"
text "Up to 100 expenses a month. No approval policies of your own."
card "Business"
figure "EUR 49 a month"
text "Everything, including your own approval policies."
card "Enterprise"
figure "EUR 249 a month"
text "Ten cost centres at once and four approvers each."
footer
action "Cancel"
action primary "Change to this plan"
Rationale For a choice between priced options, each card names the option, its price, and its period. The one the company holds says so in words. It does not rely only on a ring.
Cost Sections are the only boxes and never nest, so three plan cards inside a section break that rule three times.
cards-with-a-radio-inside-eachCards with a Radio Inside Eachpage
breadcrumb "Home" "Settings" "Plan"
header
heading "What the plan costs"
text "The company is on Business."
region "The plans"
card "Starter"
field "Starter" radio
figure "EUR 0 a month"
text "Up to 100 expenses a month."
card "Business"
field "Business" radio
figure "EUR 49 a month"
text "Everything, including your own approval policies."
card "Enterprise"
field "Enterprise" radio
figure "EUR 249 a month"
text "Ten cost centres at once and four approvers each."
footer
action "Cancel"
action primary "Change plan"
Rationale The plan cards carry the choice in a real radio. The tab order and the arrow keys work as a form control. They do not work as a set of buttons.
Cost One card holds two nested controls. The whole card has to forward the click to the radio, and the radio still has to be reachable by Tab.
a-select-in-a-settings-page-where-nothing-is-pre-selectedA Select, in a Settings Page Where Nothing Is Pre-Selectedpage
breadcrumb "Home" "Settings" "General"
header
heading "General"
text "What Harbor and Pine is and where it works."
action primary "Save"
region "Locale"
field "Time zone" select = "Choose a time zone"
text muted "Every date and time on every page is written in this zone."
field "Default currency" select = "Choose a currency"
text muted "The currency amounts are entered in. Every amount uses it."
Rationale GOV.UK allows a pre-selection in settings and nowhere else. Where the value has no default and the options are many, a select with a real empty first option is the least-bad shape.
Cost The empty option is a placeholder, and placeholders stay out of components. A select whose first choice is a blank that must be replaced is two states, not one.
Reference GOV.UK
a-multi-select-as-a-list-of-checkboxesA Multi-Select as a List of Checkboxespage
breadcrumb "Home" "Settings" "Team"
header
heading "New role"
text "What this role may do."
action primary "Create the role"
region "The role"
field "Name" text = "Auditor"
text "Expenses"
field "Look at any expense" checkbox
field "Reverse a payout" checkbox
field "Approve an expense" checkbox
field "Record a refusal" checkbox
text muted "7 actions across 2 areas."
Rationale GOV.UK states it directly: do not use select multiple, because of its history of poor usability and assistive support. Use checkboxes in a list, or a dedicated picker where the options are many.
Cost Twenty checkboxes is a wall. Above about eight, the picker needs a search and a summary of what is chosen, which is a component rather than a field.
Reference GOV.UK
a-listbox-with-a-checkmark-and-descriptionsA Listbox with a Checkmark and Descriptionspage
breadcrumb "Home" "Expenses" "Receipts"
header
heading "Receipt naming"
text "Finance reads the name on each file."
region "Naming rule"
field "Who is named on the file" select = "No name"
text muted "Applies to receipts not yet filed."
popover
list 3
item: text "No name" · text muted "A receipt is filed without a name on it."
Rationale Apple HIG and Radix UI draw a choice with descriptions as a listbox. One button shows the current value. A checkmark marks it in the list. It includes full keyboard support. It carries the same sentences as the radio list in the height of one field.
Cost The options hide behind a button, so you compare them only by opening the list. A radio list shows all three at once. This one shows one and promises two more.
Reference Apple HIG
Switch or a Checkbox#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#switch-or-a-checkboxQuestion The payout editor holds a toggle for the cost-centre line, and settings pages hold many more switches and checkboxes. This pattern decides which control each setting deserves: switch or checkbox. It also decides what the label must say, since the label carries the difference between the two.
Default a-switch-with-the-label-and-its-consequence
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The switch with its consequence stated stays the pick. The label says what is on. The line under it says what turning it off does.
The instant-apply switch with an undo toast is the runner-up for low-risk settings you flip often. Never ship a switch whose label names the control instead of the outcome. You need no documentation to learn what on means.
Reviewed for Which built toggle breaks from the wireframe its catalog entry chose? Open the built switches, checkboxes, and instant applies beside the catalog entry recording the chosen option and its reason.
a-switch-with-the-label-and-its-consequenceA Switch with the Label and Its ConsequenceDefaultpage
breadcrumb "Home" "Settings" "Payouts"
header
heading "Payouts"
text "What spenders see when the payout lands."
region "Remittance"
field "Show the cost centre line" toggle
text "The line on the remittance that names the budget."
field "Let spenders file without an account" toggle
text "They can still sign in afterwards to reach their own expenses."
field "Add a booking fee" toggle
text "The budget pays this one. EUR 5.00 per expense."
Rationale The label says what is on and the line under it says what turning it off does, so the choice is reversible and stated.
Cost A switch has no pressed state on first paint, so you cannot tell what is currently on without looking twice. It carries no visible text.
a-checkbox-where-the-off-state-is-the-defaultA Checkbox Where the off State Is the Defaultpage
breadcrumb "Home" "Settings" "Email"
header
heading "Email"
text "What Harbor and Pine sends, and what it promises."
action primary "Save"
region "Messages"
field "Email a spender when their payout is sent" checkbox
text "Carries the payout reference. This is the message a spender looks for."
field "Email a spender the day before the payout" checkbox
text "Sent at 18:00 in the office time zone."
field "Put the company name on every message" checkbox
text "Harbor and Pine, with its address and telephone number, as the law requires."
Rationale Use a checkbox rather than a switch, because the default is off and the action is to tick something. The GOV.UK wording phrases it as what ticking does. It does not phrase it as what the box is.
Cost A checkbox and a switch look nearly identical here, and readers conflate them. The phrasing of the label has to carry the difference.
Reference GOV.UK
a-checkbox-with-the-consequence-under-it-no-state-carriedA Checkbox with the Consequence Under It, No State Carriedpage
breadcrumb "Home" "Settings" "Payouts"
header
heading "Payouts"
text "How money reaches the spenders."
action primary "Save"
region "The connected account"
fact "The payout account" "NL91 ABNA 0417 1643"
divider
field "Pay twice a month" checkbox
text "On the 4th and the 19th, with the money from three days before."
field "Pay once a month" checkbox
text "On the 4th, with everything from the previous month."
Rationale Every setting is a checkbox with a consequence line. One control for the whole page, so nothing about the shape depends on whether the value happens to be on.
Cost You cannot see at a glance which are on without reading the boxes. On a settings page with eight such settings that matters.
two-options-where-the-difference-is-the-moneyTwo Options Where the Difference Is the Moneypage
breadcrumb "Home" "Payouts" "Schedule"
header
heading "How often the payouts run"
text "The payouts run from NL91 ABNA 0417 1643."
region "Frequency"
card "Twice a month"
field "Twice a month" radio
figure "EUR 28,510 on average"
text "On the 4th and the 19th, three days behind."
card "Once a month"
field "Once a month" radio
figure "EUR 57,020 on average"
text "On the 4th, everything from the previous month."
footer
action "Cancel"
action primary "Change to monthly"
Rationale Payout frequency is a choice between two money outcomes. It gets radio cards with the amount and the date on each. No switch hides the difference.
Cost Cards for a two-way choice are heavy, and the amounts are derived figures that have to be computed per company to stay correct.
a-switch-that-is-refused-with-the-reasonA Switch That Is Refused, with the Reasonpage
breadcrumb "Home" "Settings" "Payouts"
header
heading "Payouts"
text "How money reaches the spenders."
region "The connected account"
action disabled "Switch to a different bank account"
message critical "The Owner role can do this. Your role is Approver."
region "Where it pays"
fact "The payout account" "NL91 ABNA 0417 1643"
Rationale The setting is one the role cannot change: switched off and refused, with the reason in visible text. This is the explained-action pattern applied to a control rather than a button.
Cost A disabled switch with no value is a dead pixel if you do not notice the reason. The reason has to sit directly under it, not at the foot of the section.
a-switch-with-the-number-it-would-changeA Switch with the Number It Would Changepage
breadcrumb "Home" "Settings" "Payouts"
header
heading "Booking fee"
text "What the payout adds on top of the lines. The budget pays all of it."
region "The fee"
field "Per expense, in euros" number = "5.00"
divider
field "Waive the fee for the Travel policy" toggle
text "Travel policy only. Last month that would have been EUR 310.00 across 62 expenses."
field "Show the fee as part of the amount" toggle
text "Claim EUR 50.00 instead of EUR 45.00."
Rationale Where flipping the setting changes an amount, the amount is on the same line. You read the consequence before flipping rather than after.
Cost A derived figure beside a control has to be current, which means a read that the switch itself depends on. It also puts a number on a page that was otherwise free of them.
instant-apply-with-an-undo-toastInstant Apply with an Undo Toastpage
breadcrumb "Home" "Settings" "Payouts"
header
heading "Payouts"
text "What spenders see when the payout lands. Changes apply at once."
region "Remittance"
field "Waive the fee for the Travel policy" toggle
text "Travel policy only. Last month that would have been EUR 310.00 across 62 expenses."
field "Let spenders file without an account" toggle
text "They can still sign in afterwards to reach their own expenses."
field "Show the cost centre line" toggle
text "The line on the remittance that names the budget."
toast positive "Booking fee waived for the Travel policy."
Rationale Linear and Google Material apply a low-risk switch at once. They offer Undo in a toast, so there is no Save to forget and no dialog to dismiss. The toast names what changed, which a silent apply never does.
Cost Every flip is an audited command the moment it happens, with no review step before it. It fits display settings. It does not fit money, access, or anything a toast cannot undo.
Reference Linear
a-three-way-choice-as-a-segmented-controlA Three-Way Choice as a Segmented Controlpage
breadcrumb "Home" "Settings" "Email"
header
heading "Email"
text "What Harbor and Pine sends, and what it promises."
region "Reversal mail"
row
action primary "Every reversal"
action "Only bank refusals"
action "Never"
field "Copy to" text = "Priya Shah"
text muted "A person on the team. Not the spender."
footer
action primary "Save"
Rationale Slack draws a three-way notification choice as a segmented control, because the values are short, ordered, and all worth seeing at once. No menu hides the middle option you want.
Cost Three labels have to fit the control, so the wording shortens until it almost stops meaning anything. A fourth option or a per-option description breaks the shape back into radios.
Reference Slack
Upload Field#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#upload-fieldQuestion Claimwick records take files: a receipt photo or a staff spreadsheet. A receipt needs a stated type and size, and a staff import needs stated columns and row-level refusals. The decision is how the upload field states its contract and reports progress and refusals.
Default a-drop-zone-that-states-what-it-accepts
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep the drop zone that states its types and size limit. Nobody learns the limit by being refused. The per-file progress tray is the runner-up for imports of several files, where one bar per file beats one bar for all. Never ship a CSV upload with no stated columns: a file refused for a format nobody stated is a refusal the application caused.
Reviewed for Which built file input breaks from the wireframe its catalog entry chose? Open the built drop zones, CSV reads, and progress rows beside the catalog entry recording the chosen option and its reason.
a-drop-zone-that-states-what-it-acceptsA Drop Zone That States What It AcceptsDefaultpage
breadcrumb "Home" "Expenses" "EXP-1042"
header
heading "Receipts"
text "The receipt photos finance reads."
region "The receipt"
field "Drop a receipt here or choose a file" file
text "A photo of at most 5 MB. JPEG, PNG or PDF."
field "Receipt description" text
field "Drop the second page here or choose a file" file
text "The back of the receipt, if the total is there."
field "Back-page description" text
Rationale The zone states the accepted types and the size limit in a sentence under it, so nobody discovers the limit by being refused.
Cost A drop zone invites a click anywhere, and if you drop a file on the page outside the zone you get nothing at all. No affordance says where the zone is.
the-file-already-chosen-with-what-it-will-doThe File Already Chosen, with What It Will Dopage
breadcrumb "Home" "Expenses" "EXP-1042"
header
heading "Receipts"
text "The receipt photos finance reads."
action primary "Save"
region "The receipt"
fact "Receipt" "receipt-hotel.jpg"
text "512 by 512. 84 KB. Chosen now."
action "Replace"
action quiet "Remove the receipt"
field "Receipt description" text = "Hotel bill, 2 nights"
message info "Saving replaces the receipt finance reads now. It reaches the approver within a minute."
Rationale After the choice, the row carries the file, its size, and the consequence of saving. The old receipt is replaced and the approver sees the new one within a minute.
Cost A file that has been uploaded but not saved is a resource that exists and is not referenced. It needs cleanup, or the storage fills with orphans.
a-csv-upload-that-states-its-columns-firstA CSV Upload That States Its Columns Firstpage
breadcrumb "Home" "Settings" "Data imports"
header
heading "Import staff"
text "Bring in a list already held. Nothing is sent anywhere until the result is checked."
region "The file"
text "The file needs four columns."
text "email, first_name, last_name, cost_centre. The header row names them. Order does not matter."
link "Download a template"
field "Drop a CSV here or choose a file" file
text "Up to 5 MB, 2,000 rows. UTF-8, comma separated."
footer
action "Cancel"
action primary "Check the file"
Rationale For an import, the field states what it needs before anything is uploaded: the four column names and a link to a template. The upload cannot be wrong about a format nobody told it.
Cost Stating four columns above the zone is a paragraph of prose, and prose belongs on the page beside the field, not inside a component.
the-upload-refused-naming-the-row-that-failedThe Upload Refused, Naming the Row That Failedpage
breadcrumb "Home" "Settings" "Data imports"
header
heading "Import staff"
region "The file"
message critical "1,988 of 2,000 rows can be imported. Row 412 has no email address."
table 2 "Row" "What is wrong" "What was in the file"
item: text "412" · text "No email address" · text muted "Ada Kowalski"
footer
action "Choose a different file"
action primary "Import the 1,988 good rows"
Rationale The file is refused for a reason you can act on: row 412 has no email. A refusal that names a row is worth more than one that says "invalid file".
Cost Naming one row out of 2,000 needs a read of the whole file before answering. The upload is not instant, and you wait without knowing why.
an-image-field-with-the-dimensions-statedAn Image Field with the Dimensions Statedpage
breadcrumb "Home" "Expenses" "EXP-1042"
header
heading "Receipts"
region "The receipt"
field "Drop the second page here or choose a file" file
text "At least 1200 pixels wide. At most 5 MB. JPEG, PNG or PDF."
message caution "That photo is 800 by 600. Small text may not read."
field "Keep it anyway" checkbox
text muted "Stored as is. Retake it if finance asks."
footer
action "Cancel"
action primary "Save receipt"
Rationale For a photo that must stay readable, the resolution is part of the requirement, not a nicety. The field says 1200 pixels wide reads best and warns below it.
Cost The stored image box follows the page layout, not the resolution of the image file. A field that warns on resolution re-decides that per field.
an-import-that-reports-what-it-madeAn Import That Reports What It Madepage
breadcrumb "Home" "Settings" "Data imports" "IMP-4F82C1A9"
header
heading "1,988 staff imported"
text "Finished 8 Oct 2026, 14:22. Nothing has been charged to anybody."
region "The import"
fact "Rows in the file" "2,000"
fact "Staff created" "1,988"
fact "Welcome mails queued" "2,361"
fact "Rows refused" "12"
fact "Amount charged to anybody" "EUR 0.00"
footer
action "Undo this import"
action primary "Open the list"
Rationale After an import, the page says what happened: how many rows became what, how many were refused, and why. You can undo it while nothing has been paid out.
Cost A real import is a command whose effect spans many records, which the architecture reserves for a named exception. The report has to be a real read, not a summary of the session.
one-progress-row-per-fileOne Progress Row per Filepage
breadcrumb "Home" "Settings" "Data imports"
header
heading "Import staff"
text "Bring in a list already held. Nothing is sent anywhere until the result is checked."
region "Files"
list 2
item: text "staff-2026-10.csv" · text muted "1,240 of 2,000 rows read" · action "Cancel"
text "receipt-hotel.jpg is ready."
field "Drop another file here or choose one" file
text "CSV up to 5 MB, or an image up to 2 MB."
footer
action "Cancel"
action primary "Check the files"
Rationale Dropbox and Google Drive report each file on its own row with its own bar and its own cancel, because two files never finish together. One bar for all would lie about both.
Cost A row per file is a list the page must manage: cancel one, remove one, and retry one. For a single receipt it is heavier than the one zone it replaces.
Reference Dropbox
drop-anywhere-on-the-sectionDrop Anywhere on the Sectionpage
breadcrumb "Home" "Expenses" "EXP-1042"
header
heading "Receipts"
text "The receipt photos finance reads."
region "Front page"
field "Drop the front page anywhere in this box" file
text "A photo of at most 5 MB. JPEG, PNG or PDF."
field "Receipt description" text = "Hotel bill, 2 nights"
region "The receipt"
field "Drop the second page here or choose a file" file
text "The back of the receipt, if the total is there."
Rationale GitHub and Notion take a drop anywhere on the editing surface. The target highlights while the file hovers, so you never aim at a small box.
Cost A page that accepts drops everywhere must also refuse them precisely: a CSV over the receipt box needs a refusal, not a silent nothing. Every surface needs its own accepted list.
Reference GitHub
Reporting a Refused Submit#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#reporting-a-refused-submitQuestion A submit refused by the API must report what is wrong and where. GOV.UK asks for a summary with focus and links, plus a message on each field, meeting WCAG 3.3.1 and 3.3.3. The decision is how the form carries that report.
Default a-summary-above-the-form-plus-a-message-on-each-field
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep the GOV.UK summary with focus, plus per-field messages. It meets WCAG 3.3.1 and 3.3.3, and the links carry you to each field. Inline validation on blur is the runner-up as a complement, never a replacement. It confirms good values early but cannot orient you facing three errors. Never ship the dialog summary: it hides the form you must fix.
Reviewed for Which built submit-error report breaks from the wireframe its catalog entry chose? Open the built summaries and per-field messages beside the catalog entry recording the chosen option and its reason.
a-summary-above-the-form-plus-a-message-on-each-fieldA Summary Above the Form, Plus a Message on Each FieldDefaultpage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
region "There is a problem"
link "Enter the name of the trip"
link "Trip starts must be before the trip ends"
region "The trip"
field "Name" text
message critical "Enter the name of the trip"
field "Cost centre" select = "Amsterdam sales"
field "Trip starts" text = "08:00"
message critical "Trip starts must be before the trip ends. It is set to 08:00."
footer
action "Cancel"
action primary "Create trip"
Rationale This is the GOV.UK pattern in full. The summary comes first and receives focus, with each entry linking to its field. The field repeats the same wording so it reads the same out of context.
Cost The wording exists in two places per error, so generate it once and use it twice. Hand-written copies drift, and the drift is invisible.
Reference GOV.UK
messages-on-the-fields-onlyMessages on the Fields Onlypage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
region "The trip"
field "Name" text
message critical "Enter the name of the trip"
field "Cost centre" select = "Choose a cost centre"
message critical "Choose which budget pays"
field "Trip starts" text = "08:00"
message critical "Trip starts must be before the trip ends"
footer
action "Cancel"
action primary "Create trip"
Rationale Each field carries its own message and nothing else. You see the error where you have to fix it.
Cost On a long form the first error may be below the fold. You submit, see nothing change, and conclude the button is broken. G139 exists to jump to the errors.
a-summary-in-a-dialog-above-the-formA Summary in a Dialog Above the Formpage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
region "The trip"
field "Name" text
message critical "Enter the name of the trip"
field "Cost centre" select = "Choose a cost centre"
message critical "Choose which budget pays"
dialog
heading "There is a problem"
text "3 things need fixing before this trip can be created."
text "Enter the name of the trip. Choose which budget pays."
action primary "Fix these"
Rationale The summary arrives as a dialog over the form, which cannot be scrolled past and cannot be missed, so you cannot submit again unaware.
Cost A dialog over a form breaks focus rules: a dialog never opens another layer, and a form is not a dialog. It also hides the form you have to fix.
the-refused-field-only-with-no-summary-at-allThe Refused Field Only, with No Summary at Allpage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
text "Start with a name."
region "The trip"
field "Name" text = "Spring offsite"
field "Trip starts" text = "32/13/2026"
message critical "Trip starts must be a time between 00:00 and 23:59."
field "Trip ends" text = "14/03/2026 12:00"
footer
action "Cancel"
action primary "Create trip"
Rationale The one field the command refused carries a message and everything else is untouched. It is the smallest response to a single refusal, which is what most refusals are.
Cost This common behavior meets WCAG 3.3.1 but not 3.3.3, because "Invalid date" names no field and says nothing about what to do.
the-refused-command-with-what-to-do-insteadThe Refused Command, with What to Do Insteadpage
breadcrumb "Home" "Expenses" "EXP-1042" "Payout"
header
heading "Run the payout"
text "The payout runs Sat 14 Mar 2026 at 08:00."
region "What would be paid"
message critical "That is more than the budget holds. Lower an amount first."
table 3 "Cost centre" "Budget" "Claimed"
item: text "Amsterdam sales" · text "EUR 10,000" · text "EUR 10,310"
footer
action "Change an amount"
action disabled "Run the payout"
Rationale When a refusal is about the command rather than a field, it goes above the form as an alert. The alert carries the code sentence. Where the application can help, it states why it was refused and what resolves it.
Cost The refusal is not attached to anything you can click, so it explains rather than navigates. It suits a business rule, not a typo.
validation-on-leaving-each-fieldValidation on Leaving Each Fieldpage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
text "Each field is checked when it is left."
region "The trip"
field "Name" text = "Spring offsite"
message positive "Spring offsite is free."
field "Cost centre" select = "Amsterdam sales"
field "Trip starts" text = "08:00"
message critical "Trip starts must be before the trip ends. It is set to 08:00."
footer
action "Cancel"
action primary "Create trip"
Rationale Shopify Polaris and Stripe check a field on blur and mark it good or wrong where it sits. You fix one field and move on. By the submit nothing is left to refuse.
Cost It is a complement to the summary, never a replacement: with three errors and no summary you still cannot see them all at once. A check on blur fires while the form is half-filled.
Reference Shopify Polaris
one-question-per-screen-with-a-progress-markerOne Question per Screen with a Progress Markerpage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
text "Question 2 of 4."
region "Budget"
text "Halfway through the four questions."
field "Which budget pays" select = "Choose a cost centre"
message critical "Choose which budget pays"
footer
action "Back"
action primary "Continue"
Rationale TurboTax and Typeform ask one question per screen with nowhere else for an error to hide. The refusal sits under the only field on the page. A guided flow cannot submit with a field unseen.
Cost Four screens for four fields is the slowest form among these options, and if you know all four answers you resent the pacing. It fits a first trip. It does not fit the fortieth.
Reference Typeform
Work That Is Not Saved#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#work-that-is-not-savedQuestion A form must show work that is not saved. Nothing may read as saved before it is, and leaving must not drop typed work without a warning. The decision is how the form marks unsaved work and guards the way out.
Default a-visible-unsaved-marker-beside-the-save
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep the visible unsaved marker beside Save. You never have to remember whether anything was typed. The sticky save bar is the runner-up for long settings pages where the form actions scroll out of view. Never ship silent autosave on money or access. Each save would be an audited command. An audit entry per keystroke batch is not a trail you can read.
Reviewed for Which built unsaved marker breaks from the wireframe its catalog entry chose? Open the built dirty flags, drafts, and save bars beside the catalog entry recording the chosen option and its reason.
a-visible-unsaved-marker-beside-the-saveA Visible Unsaved Marker Beside the SaveDefaultpage
breadcrumb "Home" "Settings" "Remittance"
header
heading "Remittance"
text "The accent color spenders see on the remittance."
region "Appearance"
field "Accent color" text = "#B45309"
text "A #RRGGBB value. Contrast with white is 5.1 to 1."
field "Typeface" select = "Inter"
message info "Two changes not saved. Spenders still see the old ones."
footer
action quiet "Discard both"
action primary "Save 2 changes"
Rationale The moment a field changes, the Save button carries the word Save changes and a note says what changed. You never have to remember whether you typed anything.
Cost A note naming one changed field states a derived fact. It has to come from the form rather than the route. A Save that changes its own label moves under the pointer.
leave-with-a-question-when-there-is-something-to-loseLeave-with-a-Question When There Is Something to Losepage
breadcrumb "Home" "Settings" "Remittance"
header
heading "Remittance"
region "Appearance"
field "Accent color" text = "#B45309"
text muted "Contrast with white is 5.1 to 1."
footer
action "Cancel"
action primary "Save"
dialog
heading "Leave without saving"
text "Two changes will be lost. The accent color and the typeface."
action "Stay here"
action primary "Leave and lose both"
Rationale The Cloudcape rule confirms only when real data is at risk. It stays consistent about it, so the same dialog appears for a Cancel, a breadcrumb, and a move away.
Cost Every page with a form needs the same guard. The browser prompt cannot be worded, so the custom one and the native one both appear on a browser Back.
Reference Cloudcape
autosave-with-a-saved-markerAutosave, with a Saved Markerpage
breadcrumb "Home" "Expenses" "EXP-1042" "Disputes"
header
heading "Record a dispute"
text "Saved as it is typed. Nothing is lost when this browser closes."
region "The dispute"
field "What happened" textarea = "At 08:41 Ben refused the hotel breakfast as personal."
text muted "Saved 4 seconds ago"
Rationale The form saves itself every few seconds and a quiet marker says "Saved 4 seconds ago". Nothing is ever lost and no dialog is ever needed.
Cost Autosave makes each save a command against money and access, which means an audit entry per keystroke batch. A past decision must stay reconstructable, and a trail of keystroke batches is not one you can read.
a-draft-saved-and-left-on-the-serverA Draft, Saved and Left on the Serverpage
breadcrumb "Home" "Settings" "Policies"
header
heading "Approval policy"
text "A draft from 6 Oct is waiting. It has not been published."
region "The policy"
message info "A draft from 6 Oct 2026, 17:40 waits. The published policy has not changed since 2 Sep."
field "Published policy" textarea = "Expenses above EUR 150 need a receipt."
footer
action "Carry on"
action primary "Publish"
Rationale The state of the form is a draft the server holds. Leaving the page leaves the draft, and returning to it offers "Carry on". GOV.UK asks for this where a long form may be completed over more than one sitting.
Cost A draft is a second version of the record with its own name and its own rules about when it expires. It is a new aggregate, not a field on the form.
Reference GOV.UK
saved-silently-with-the-command-in-the-page-headerSaved Silently, with the Command in the Page Headerpage
breadcrumb "Home" "Settings" "Standard"
header
heading "Standard"
status "Active" positive
action "Retire policy"
region "The name"
field "Name" text = "Standard"
field "What it includes" textarea = "Standard limits for every trip."
action primary "Save the name"
region "Expense naming"
field "Naming" select = "The spender name"
action primary "Save the naming"
Rationale For a settings page where each change is independent, each section saves itself on its own Save and the header carries nothing. If you change two areas you can save each.
Cost Two Saves on one screen is the case where you edit one form and save the other. The rule has to be one Save per page or one per section, never a mix.
a-read-only-form-because-the-state-forbids-itA Read-Only Form Because the State Forbids Itpage
breadcrumb "Home" "Payouts" "March run" "Review"
header
heading "Review"
text "The March run is paying. 9,412 expenses carry EUR 45.00."
region "Hotel night"
field "Nightly amount, in euros" text = "45.00"
text "The amount the paid expenses already carry. It cannot change."
field "Amount for the expenses still open, in euros" text = "50.00"
message caution "Paying the 588 differently is allowed. A spender who finds out can ask for the difference."
footer
action primary "Save the new amount"
Rationale Once the run has started, the amount is a versioned fact and the field is read-only rather than a field with a save. Changing it is a different command with its own consequences.
Cost A read-only field with no command leaves you stuck, unless the command to change it is stated. The explained-action pattern exists for exactly this case.
a-save-bar-pinned-to-the-top-while-dirtyA Save Bar Pinned to the Top While Dirtypage
breadcrumb "Home" "Settings" "Remittance"
header
heading "Remittance"
text "The accent color and typeface spenders see."
toolbar sticky
text "2 unsaved changes. Spenders still see the old ones."
action "Discard"
action primary "Save"
region "Appearance"
field "Accent color" text = "#B45309"
field "Typeface" select = "Inter"
Rationale Shopify Polaris pins a contextual save bar to the top of a dirty form. Save and Discard never scroll out of reach on a long settings page. The bar appears with the first keystroke and leaves with the save.
Cost A pinned bar covers content on a phone and follows you past sections it has nothing to do with. It also needs the same dirty tracking as the marker, so it adds no new information, only reach.
Reference Shopify Polaris
per-field-revert-to-the-saved-valuePer-Field Revert to the Saved Valuepage
breadcrumb "Home" "Settings" "Remittance"
header
heading "Remittance"
text "The accent color and typeface spenders see."
region "Appearance"
field "Accent color" text = "#B45309"
text muted "Was #1F2937."
action quiet "Revert the color"
field "Typeface" select = "Inter"
text muted "Was System."
action quiet "Revert the typeface"
footer
action quiet "Discard both"
action primary "Save 2 changes"
Rationale Webflow and Figma mark each changed field with a dot and offer Revert beside it. You keep one change and drop another without retyping. The old value sits under the field as the value you fall back to.
Cost A dot plus a revert button per field adds two elements on every row of a long form. If you want to keep everything you still need the page-level Save. It pays off only where partial saves are common.
Reference Figma
Picking a Period#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#picking-a-periodQuestion Payouts and reporting run on periods. Two typed dates cover every window but ask for both edges, while presets cover the common windows in one tap. The decision is how the period control combines typing and presets.
Default two-date-inputs-with-presets
Compact Below the md breakpoint the painted calendar shows one month. The second month of the window follows below it. The compact option is a-calendar-with-the-range-painted.
Rationale Keep two typed dates with presets. One tap covers the common windows, typing covers the rare ones, and both edges stay visible. The two-month painted calendar is the runner-up for reporting screens where the window itself is the question. Never ship presets alone with no typed fallback: no preset reaches the filing to payout window the payout check wants.
Reviewed for Which built range picker breaks from the wireframe its catalog entry chose? Open the built presets, month pickers, and painted calendars beside the catalog entry recording the chosen option and its reason.
two-date-inputs-with-presetsTwo Date Inputs with PresetsDefaultpage
breadcrumb "Home" "Payouts" "March run"
header
heading "March run"
text "What moved for Harbor and Pine."
tabs "Last 7 days" "Last 30 days" "Last 90 days"
region "Period"
field "From" date = "14/02/2026"
field "To" date = "14/03/2026"
text muted "The payout itself: Sat 14 Mar 2026."
footer
action primary "Show the run"
Rationale The presets fill both fields and you adjust either edge by typing. The common windows cost one tap and the rare ones stay possible. The control that fills the fields and the fields that hold them are one unit.
Cost Two controls describe one value. The preset shows Custom the moment either date is edited, or the row claims a window it no longer holds.
preset-segmented-control-7-30-90-daysPreset Segmented Control: 7, 30, 90 Dayspage
breadcrumb "Home" "Payouts" "March run"
header
heading "March run"
text "What moved for Harbor and Pine."
tabs "Last 7 days" "Last 30 days" "Last 90 days"
region "Period"
text "Counts back from Sat 14 Mar 2026: 14/02/2026 to 14/03/2026."
footer
action primary "Show the run"
Rationale One tap covers the three windows a payout check almost always wants, with no typing. The resolved dates are shown back under the control so the preset is a promise you can read.
Cost No preset reaches a trip-anchored window like the trip to the payout, so a custom range still needs a second control beside this one.
month-pickerMonth Pickerpage
breadcrumb "Home" "Payouts" "March run"
header
heading "March run"
text "What moved for Harbor and Pine."
region "Period"
field "Month" select = "March"
field "Year" number = "2026"
text muted "March 2026, the month of Sat 14 Mar 2026."
footer
action primary "Show the run"
Rationale Payout and tax reporting run by calendar month. Picking March 2026 in one control beats typing the first and the last day of the month by hand.
Cost You cannot express a window that crosses a month edge, like the fortnight around the payout. Whole months are the only shape this control draws.
open-start-rolling-windowOpen-Start Rolling Windowpage
breadcrumb "Home" "Payouts" "March run"
header
heading "March run"
text "What moved for Harbor and Pine."
region "Period"
field "Window" select = "Since the quarter opened"
field "Up to and including" date = "14/03/2026"
text muted "From the quarter opening to Sat 14 Mar 2026."
footer
action primary "Show the run"
Rationale Everything since the quarter opened needs an end date only. The start is the opening and never moves. One field replaces two where you cannot answer differently.
Cost An open start silently includes old test expenses, so the resolved start must be shown back under the field. Without it you cannot tell what the window holds.
refusal-when-the-end-precedes-the-startRefusal When the End Precedes the Startpage
breadcrumb "Home" "Payouts" "March run"
header
heading "March run"
text "What moved for Harbor and Pine."
region "Period"
message critical "The end is before the start. To must be on or after 14/03/2026."
field "From" date = "14/03/2026"
field "To" date = "07/03/2026"
message critical "To must be on or after 14/03/2026."
footer
action primary "Show the run"
Rationale The page refuses an end before the start where you typed it, in the GOV.UK template. The message names the field and says what to do. The summary above it says the same in one line.
Cost It is one more state to draw and to test. The refusal waits until both fields hold a date. It never fires while you still type the second one.
Reference GOV.UK
timezone-safe-inclusive-day-boundariesTimezone-Safe Inclusive Day Boundariespage
breadcrumb "Home" "Payouts" "March run"
header
heading "March run"
text "What moved for Harbor and Pine."
region "Period"
message info "Both days count in full. From 14/02/2026 to 14/03/2026, Europe/Amsterdam."
field "From" date = "14/02/2026"
text muted "Includes the whole day."
field "To" date = "14/03/2026"
text muted "Includes the whole day, to the end of Sat 14 Mar 2026."
footer
action primary "Show the run"
Rationale Each edge is a whole day in Europe/Amsterdam. An expense filed at 22:41 on the payout day falls inside the window that ends on that day. The boundary sentence is part of the control, not documentation beside it.
Cost The sentence must be read to work, and if you think in UTC you see different instants. Stating the zone once keeps it from being repeated under each field.
a-calendar-with-the-range-paintedA Calendar with the Range Paintedpage
breadcrumb "Home" "Payouts" "March run"
header
heading "March run"
text "What moved for Harbor and Pine."
tabs "Payout week" "Trip week" "Custom"
region "Period"
split 1:1
grid 28
item: text "20"
grid 31
item: text "7"
text "07/03/2026 to 14/03/2026, both days counted in full."
footer
action primary "Show the run"
Compact layout:
page
breadcrumb "Home" "Payouts" "March run"
header
heading "March run"
text "What moved for Harbor and Pine."
tabs "Payout week" "Trip week" "Custom"
region "Period"
grid 28
item: text "20"
grid 31
item: text "7"
text "07/03/2026 to 14/03/2026, both days counted in full."
footer
action primary "Show the run"
delivery-anchored-presetsPayout-Anchored Presetspage
breadcrumb "Home" "Payouts" "March run"
header
heading "March run"
text "What moved for Harbor and Pine."
tabs "Since filing" "Trip week" "Payout week"
region "Period"
text muted "Anchored to Tue 13 Jan 2026 and Sat 14 Mar 2026, not to today."
field "From" date = "07/03/2026"
field "To" date = "14/03/2026"
footer
action primary "Show the run"
Rationale Airbnb and Google Flights paint the chosen range across the calendar, so you see the window instead of reading two dates. The endpoints stay readable as dates under the control.
Cost A painted month shows one month, and the payout window usually spans two. Two months side by side show the window truthfully, and they do not fit a phone.
Reference Airbnb
slug-with-the-live-address-under-itSlug with the Live Address Under ItDefaultpage
breadcrumb "Home" "Trips" "Rotterdam client visit" "Address"
header
heading "Trip address"
text "Where spenders file against this trip."
region "The address"
field "Name" text = "Rotterdam client visit"
field "Slug" text = "rotterdam-client-visit"
text muted "Lowercase letters, numbers and dashes. Changing it breaks links already shared."
fact "Spenders reach" "claimwick.example/rotterdam-client-visit"
action quiet "Copy"
footer
action "Cancel"
action primary "Save address"
Rationale Mixpanel and Amplitude anchor ranges to the milestones the business runs on, because last-30-days drifts while the filing stays fixed. The preset resolves to typed dates you can still adjust.
Cost Anchors multiply: filing, approval, close, and payout runs. Three anchors fit a control. Six need a picker of their own, and the window stops being one tap.
Reference Mixpanel
Slug or URL Field with a Preview#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#slug-or-url-field-with-a-previewQuestion A trip takes a slug, the short part typed in, and spenders read the full address: they link it and print it. The decision is how the field takes the slug and previews the address before saving.
Default slug-with-the-live-address-under-it
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep the slug with its live address under it. You type the short part and read the full address before saving. The transliteration from the name is the runner-up for creation, where typing the same words twice is pure overhead. Never ship an address that cannot be read before it is saved.
Reviewed for Which built address input breaks from the wireframe its catalog entry chose? Open the built slugs, transliterations, and availability checks beside the catalog entry recording the chosen option and its reason.
the-address-as-a-fact-with-a-change-commandThe Address as a Fact with a Change Commandpage
breadcrumb "Home" "Trips" "Rotterdam client visit" "Address"
header
heading "Trip address"
text "Where spenders file against this trip."
region "The address"
fact "Address" "claimwick.example/rotterdam-client-visit"
fact "First shared" "13 Jan 2026"
fact "Used in" "214 expenses"
message caution "Changing the address breaks the links already shared. The old address keeps working for 30 days."
footer
action primary "Change the address"
Rationale Contentful and Shopify show the full address under the slug field and update it as you type. The short value is never saved unseen. The copy command beside it is how the address leaves the back office.
Cost The address must update as you type, which a static wireframe cannot show. It also states the host the platform gives, which you cannot change on this screen.
Reference Shopify
transliterated-from-the-name-with-an-edit-toggleTransliterated from the Name with an Edit Togglepage
breadcrumb "Home" "Trips" "New trip"
header
heading "New trip"
text "Start with a name."
region "The address"
field "Name" text = "Rotterdam client visit"
field "Generate the slug from the name" checkbox
field "Slug" text = "rotterdam-client-visit"
text muted "Spenders reach claimwick.example/rotterdam-client-visit."
footer
action "Cancel"
action primary "Create trip"
Rationale GitHub shows a repository address as a fact and puts Rename behind a command, because an address in use is depended upon. The 30-day redirect is what makes the change survivable.
Cost Changing a value takes two steps, and the redirect has to be a real behavior, not a sentence. A redirect nobody builds is a promise the page breaks.
Reference GitHub
availability-check-with-alternatives-offeredAvailability Check with Alternatives Offeredpage
breadcrumb "Home" "Trips" "Rotterdam client visit" "Address"
header
heading "Trip address"
text "Where spenders file against this trip."
region "The address"
field "Slug" text = "visit"
message caution "visit is taken. Another company holds it. One of these is free."
list 3
item: text "client-visit" · action "Use"
footer
action "Cancel"
action primary "Save address"
Rationale WordPress builds the permalink from the title and lets you edit it, because the name and the slug agree nine times in ten. Typing both by hand is the same words twice.
Cost The generated value must transliterate Dutch diacritics the way you expect, and a renamed trip must not silently re-slug. Generation runs once at creation, never again.
Reference WordPress
the-slug-refused-with-the-rule-that-refused-itThe Slug Refused with the Rule That Refused Itpage
breadcrumb "Home" "Trips" "Rotterdam client visit" "Address"
header
heading "Trip address"
text "Where spenders file against this trip."
region "There is a problem"
link "Use lowercase letters, numbers and dashes in the slug"
region "The address"
field "Slug" text = "Rotterdam Visit!"
message critical "Use lowercase letters, numbers and dashes. You entered capitals, spaces and exclamation."
footer
action "Cancel"
action primary "Save address"
Rationale Namecheap answers a taken domain with free alternatives, because a refusal without a next step sends you back to guessing. The alternatives keep the shape of what was wanted.
Cost The page must check the alternatives live, one by one, or the list offers addresses that fail the same way. Three checks per keystroke pause is a read the page has to budget for.
Reference Namecheap
custom-domain-with-its-dns-instructionsCustom Domain with Its DNS Instructionspage
breadcrumb "Home" "Settings" "Workspace" "Domain"
header
heading "Workspace domain"
text "File from your own address instead of ours."
region "The domain"
field "Domain" text = "expenses.harbor-pine.example"
fact "Type" "CNAME"
fact "Name" "expenses"
fact "Value" "workspaces.claimwick.example"
message info "Add this record where the domain lives, then verify. DNS changes take up to a day."
status "Not verified" caution
footer
action primary "Verify"
Rationale The GOV.UK error template applies to a slug. The summary comes first, then the field, with the message naming what was entered and what to do. The page states the rule once and enforces it the same way.
Cost The message repeats the value, which makes it longer than the field. It also has to name every offending character class, not the first one found.
Reference GOV.UK
a-number-with-the-warehouse-ceiling-restatedA Number with the Budget Ceiling RestatedDefaultpage
breadcrumb "Home" "Finance" "Budgets" "TRIP10" "Capacity"
header
heading "Capacity"
text "How much TRIP10 can spend."
region "Money on the code"
field "Ceiling, in euros" number = "9600"
text muted "EUR 9,412 claimed. EUR 188 still to claim."
text "The budget is 98 percent spent."
fact "Amsterdam sales holds" "EUR 10,000"
text muted "PARTNER holds EUR 400 of it."
footer
action "Cancel"
action primary "Save capacity"
Rationale Vercel and Webflow pair a domain field with the exact DNS record it needs, because the field alone is half the task. The page states the record as values to copy, not as prose to interpret.
Cost Verification is a read against DNS the back office does not control. The badge can only say what the last check found and when. A stale badge claims a state the world has left.
Reference Vercel
Capacity Field#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#capacity-fieldQuestion A budget code takes money from one cost-centre budget: Amsterdam sales holds EUR 10,000, and every code draws from the same budget. The decision is how the capacity field shows the ceiling, the split, and the refusal past it.
Default a-number-with-the-warehouse-ceiling-restated
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep the number with the budget ceiling restated. One field, the budget it draws from, and what is left sit on the same screen. The split across codes is the runner-up for the budget page, where the question is the whole budget rather than one code. Never ship a capacity with no ceiling stated: a number without its budget is a number that overspends.
Reviewed for Which built ceiling input breaks from the wireframe its catalog entry chose? Open the built splits, steppers, and overage messages beside the catalog entry recording the chosen option and its reason.
the-split-across-types-with-what-is-leftThe Split Across Types with What Is Leftpage
breadcrumb "Home" "Finance" "Budgets" "Capacity"
header
heading "Capacity"
text "How the EUR 10,000 is divided."
region "Every code"
list 3
item: text "TRIP10" · text muted "Active since 13 Jan 2026" · text "9,412 of 9,600" · action "Change"
region "The budget"
fact "Claimed across every code" "9,730"
fact "Left in the budget" "EUR 270"
text muted "Retired codes keep their claimed amounts but release nothing back."
Rationale The budget total sits beside every ceiling field, because a code allocation means nothing without the budget it draws from. The bar shows how spent the budget is at a glance.
Cost The restated ceiling must update when another code changes, or two screens state two budgets. It is a read of the whole cost centre behind one field.
a-stepper-for-a-small-countA Stepper for a Small Countpage
breadcrumb "Home" "Finance" "Budgets" "Advances"
header
heading "Advances"
text "Cash up front for trips, inside the TRIP10 ceiling."
region "Money"
row
action "One euro less"
field "Advance amount" number = "24"
action "One euro more"
text muted "Steps of one euro, up to 50. EUR 318 of EUR 400 is claimed."
footer
action "Cancel"
action primary "Save advance"
Rationale Spending draws as one row per code with claimed against cap, because the question is the budget, not the row. The leftover line is what you came to read.
Cost A row per code is a table wearing a list costume, and at twelve codes it needs the table back. Editing still jumps to one code at a time.
a-percentage-held-back-from-saleA Percentage Held Back in Reservepage
breadcrumb "Home" "Finance" "Budgets" "Capacity"
header
heading "Capacity"
text "How much money stays held for the year-end close."
region "The hold"
field "Held back" number = "5"
text muted "Percent of the budget. Released to the codes at the quarter start."
fact "Held back" "EUR 500"
fact "Left to spend" "EUR 9,500"
footer
action "Cancel"
action primary "Save hold"
Rationale Apple HIG reserves the stepper for small counts adjusted by one, which a 24-euro advance is. Tapping plus twice beats selecting 24 and typing 26.
Cost A stepper for 9,600 euros would be tapped 9,600 times, so it fits only counts under fifty. The field must still take a typed value for the jump from 4 to 40.
Reference Apple HIG
the-capacity-refused-with-the-overage-namedThe Capacity Refused with the Overage Namedpage
breadcrumb "Home" "Finance" "Budgets" "TRIP10" "Capacity"
header
heading "Capacity"
text "How much TRIP10 can spend."
region "Money on the code"
message critical "That is more than the budget holds. Lower an amount first."
field "Ceiling, in euros" number = "10000"
message critical "That is EUR 400 more than the budget holds. Lower it to 9,600."
fact "Amsterdam sales holds" "EUR 10,000"
footer
action "Cancel"
action primary "Save capacity"
Rationale Holds are a percentage of the budget kept for the close, restated as money so you read both. A percentage survives a budget change. A count does not.
Cost Two numbers describe one hold, and they round differently: 5% of 10,000 is 500 until the budget becomes 9,999. The money wins and the percentage follows.
sold-as-a-read-only-fact-beside-an-editable-capClaimed as a Read-Only Fact Beside an Editable Cappage
breadcrumb "Home" "Finance" "Budgets" "TRIP10" "Capacity"
header
heading "Capacity"
text "How much TRIP10 can spend."
region "Money on the code"
fact "Claimed" "9,412"
fact "Ceiling" "9,600"
fact "Left" "188"
message info "Claimed is a fact and cannot be edited. Lowering the cap below 9,412 is refused."
field "Ceiling, in euros" number = "9600"
text muted "Must stay at or above 9,412."
footer
action "Cancel"
action primary "Save capacity"
Rationale The refusal is worded on screen, with the field carrying the same message. GOV.UK names the field and says what to do, and the numbers supply the overage. You read the overage, not the rule alone.
Cost The message must recompute the overage for every value, and a value that breaks two rules needs two messages. One refusal per field keeps it readable.
Reference GOV.UK
hex-field-with-a-swatch-and-a-contrast-readoutHex Field with a Swatch and a Contrast ReadoutDefaultpage
breadcrumb "Home" "Settings" "Mail"
header
heading "Mail style"
text "The accent color approvers see on mail buttons."
region "Accent"
image "Accent #1F2937 swatch"
field "Accent color" text = "#1F2937"
text muted "A #RRGGBB value. Contrast with white is 14.7:1, so white button text is readable."
fact "White button text" "Readable, needs 4.5:1, this accent reaches 14.7:1"
status "Readable" positive
footer
action "Cancel"
action primary "Save accent"
Rationale Stripe draws an available balance as a fact beside the controls that move it: claimed is what happened, the cap is what you decide. The two must never share a field.
Cost Three figures plus a field for one decision, and the derived Left line must update as you type the cap. It is the most screen per field in this item.
Reference Stripe
Color Field for the Mail Accent#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#color-field-for-the-mail-accentQuestion The company brands its approval mails with one accent color: a #RRGGBB value dark enough for white button text to read on it. The field takes the value and proves the contrast before the page refuses it.
Default hex-field-with-a-swatch-and-a-contrast-readout
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The hex field with a swatch and a contrast readout is the pick. It takes any color, shows it, and states the ratio the validation enforces. The approver-button preview is the runner-up beside it, because a ratio is believed once it is seen. Never ship a free color with no contrast check: a pale accent ships white text nobody can read.
Reviewed for Which built accent picker breaks from the wireframe its catalog entry chose? Open the built swatches, wells, and contrast readouts beside the catalog entry recording the chosen option and its reason.
preset-swatches-plus-a-custom-valuePreset Swatches Plus a Custom Valuepage
breadcrumb "Home" "Settings" "Mail"
header
heading "Mail style"
text "The accent color approvers see on mail buttons."
region "Accent"
text "Presets, every preset passes 4.5:1 against white."
row
image "Ink preset, chosen"
image "Amber preset"
image "Pine preset"
image "Ocean preset"
image "Plum preset"
image "Rose preset"
field "Or a custom color" text = "#1F2937"
text muted "A #RRGGBB value. Checked against white before it saves."
footer
action "Cancel"
action primary "Save accent"
Rationale Shopify theme editor pairs a hex field with a swatch and the rule it passes. A color value means nothing until it is seen. The readout repeats the validation rule, so you learn it before the request rather than after it.
Cost The ratio must recompute as you type, and an unfinished value has no ratio to state. Six characters that are not yet a color need patience, not a refusal.
Reference Shopify
the-accent-previewed-on-a-customer-buttonThe Accent Previewed on an Approver Buttonpage
breadcrumb "Home" "Settings" "Mail"
header
heading "Mail style"
text "The accent color approvers see on mail buttons."
region "Accent"
image "Accent #1F2937 swatch"
field "Accent color" text = "#1F2937"
text muted "A #RRGGBB value. Contrast with white is 14.7:1."
card "Approver preview"
heading small "Approval mail"
text "EXP-1042 from Ava"
text "EUR 125.00, Rotterdam client visit"
text "2 lines, EUR 125.00"
action "Approve EUR 125.00"
text muted "Harbor and Pine, Canal Street 12, 1011 AB Amsterdam"
footer
action "Cancel"
action primary "Save accent"
Rationale Slack and Notion offer a row of presets that all satisfy the rule, with a custom field for you when you know better. One tap covers the common case and cannot produce a failing color.
Cost Six presets cannot hold a brand color, so the custom field carries the real work anyway, and a preset row that never changes starts to look like the whole choice.
Reference Slack
a-pale-color-refused-with-the-ratio-that-failedA Pale Color Refused with the Ratio That Failedpage
breadcrumb "Home" "Settings" "Mail"
header
heading "Mail style"
text "The accent color approvers see on mail buttons."
region "Accent"
image "Accent #F59E0B swatch"
field "Accent color" text = "#F59E0B"
message "White button text needs 4.5:1 against the accent. #F59E0B reaches 2.1:1. Choose a darker color." critical
footer
action "Cancel"
action primary "Save accent"
Rationale The browser native color well is the only picker that costs no code. One input holds the platform dialog and a hex you can still type. MDN documents it as the accessible default.
Cost The well draws the browser chrome instead of the back-office control. It renders differently everywhere, which is the same charge the native date input faces. It also offers colors that fail 4.5:1 without saying so.
Reference MDN
an-inherited-accent-with-an-override-commandAn Inherited Accent with an Override Commandpage
breadcrumb "Home" "Trips" "Rotterdam client visit" "Style"
header
heading "Trip style"
text "What approvers see for this trip."
region "Accent"
fact "Harbor and Pine accent" "#1F2937"
fact "This trip" "Inherits the company accent"
message "One accent across every trip keeps the brand steady. Override it only for a trip with its own branding." info
action "Override for this trip"
Rationale The validation rule appears worded on screen in the GOV.UK template. The message names the value, states both ratios, and says what to do. You learn the rule from the refusal, not from documentation.
Cost A refusal that only says darker leaves you guessing how much darker. It earns its place fully only beside presets that all pass.
Reference GOV.UK
stacked-cards-one-per-typeStacked Cards, One per TypeDefaultpage
breadcrumb "Home" "Finance" "Budgets" "Budget codes"
header
heading "Budget codes"
text "What a spender claims and what the budget pays."
region "Three codes"
list 3
item: text "TRIP10" · text muted "412 of 500 claimed" · status "Active" positive · text "EUR 500 cap" · action "Edit"
footer
action "Add a budget code"
action primary "Save order"
Rationale Figma variables inherit down the tree and override at one level. The source stays named, so a trip can differ without hiding where its value came from. The company default stays the decider.
Cost An inherited value with an override is two values and a rule about which wins. If you never open this screen you cannot tell which trips differ.
Reference Figma
Repeatable Group for Budget Codes#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#repeatable-group-for-budget-codesQuestion The trip budget holds TRIP10, MEAL20, and TAXI05: three groups with the same fields, each with its own name, ceiling, and allocation. The group repeats, and the budget counts every repetition.
Default stacked-cards-one-per-type
Compact At phone width the table keeps its columns and scrolls sideways. The code column stays first. The compact option is a-table-with-inline-editing.
Rationale The stacked cards are the pick: one card per code with its ceiling and allocation readable at once, and one command to add another. The dated budget steps are the runner-up for quarters, where time decides the ceiling rather than the spender choice. Never ship a group whose repetitions cannot be removed: a code added by mistake must leave without taking its claimed amounts with it.
Reviewed for Which built code group breaks from the wireframe its catalog entry chose? Open the built stacked cards and inline tables beside the catalog entry recording the chosen option and its reason.
a-table-with-inline-editingA Table with Inline Editingpage
breadcrumb "Home" "Finance" "Budgets" "Budget codes"
header
heading "Budget codes"
text "What a spender claims and what the budget pays."
region "Three codes"
table 3 "Code" "Ceiling" "Claims" "Claimed"
item: text "TRIP10" · text "EUR 500" · text "500" · text "412"
field "TRIP10 ceiling in euros" text = "500"
footer
action "Add a budget code"
action primary "Save all"
Compact layout:
page
breadcrumb "Home" "Finance" "Budgets" "Budget codes"
header
heading "Budget codes"
text "What a spender claims and what the budget pays."
region "Three codes"
table 3 "Code" "Ceiling" "Claims" "Claimed"
item: text "TRIP10" · text "EUR 500" · text "500" · text "412"
footer
action "Add a budget code"
action primary "Save all"
Rationale Shopify draws product variants as one row per variant with its price readable at once, because you compare repetitions before editing one. Claimed codes stay visible but lose their Remove.
Cost A row shows the ceiling but hides the dates, the cost-centre rule, and the description, so editing still opens each code. The list answers what, never why.
Reference Shopify
one-section-per-type-with-add-anotherOne Section per Type with Add Anotherpage
breadcrumb "Home" "Finance" "Budgets" "Budget codes"
header
heading "Budget codes"
text "What a spender claims and what the budget pays."
region "Code 1 of 2"
field "Name" text = "TRIP10"
field "Ceiling, in euros" text = "500"
field "Maximum uses" text = "500"
text muted "412 claimed."
action "Remove"
region "Code 2 of 2"
field "Name" text = "MEAL20"
field "Ceiling, in euros" text = "200"
field "Maximum uses" text = "400"
text muted "318 claimed."
action "Remove"
footer
action "Add another budget code"
action primary "Save codes"
dated-price-steps-for-the-launchDated Budget Steps for the Quarterpage
breadcrumb "Home" "Finance" "Budgets" "Budget codes"
header
heading "Budget codes"
text "Time decides the ceiling. The spender claims whatever step is open."
region "Two steps"
text muted "Steps never overlap. The next step opens the moment the current one closes."
list 2
item: text "Q1, EUR 500" · text muted "1 Feb to 14 Mar 2026, 08:00, 412 of 500 claimed" · status "Active" positive · text "Open"
footer
action "Add a step"
action primary "Save steps"
Rationale Airtable edits values where they sit, so changing three ceilings costs three fields and no navigation. One row in edit state at a time keeps the table readable while it is also a form.
Cost A table that is also a form validates cell by cell, and a refused cell must hold its row open. At phone width the columns scroll sideways, which hides the ceiling behind a swipe.
Reference Airtable
drag-to-reorder-with-the-customer-order-shownDrag to Reorder with the Spender Order Shownpage
breadcrumb "Home" "Finance" "Budgets" "Budget codes"
header
heading "Budget codes"
text "Spenders meet them in this order. Drag a row to move it."
region "Three codes"
text muted "The numbers are the spender order on the filing form."
list 3
item: icon "Reorder" · text "1, TRIP10" · text muted "EUR 500 cap, active" · action quiet "Move up"
footer
action primary "Save order"
Rationale GOV.UK add-another repeats the full group once per entry with its own remove. Each repetition deserves the same labels, help, and errors as the first. Nothing about code 2 is abbreviated.
Cost Three full groups make a long page, and the budget total has nowhere to sit. It fits creation, where each code is new. It drags for review, where each code is known.
Reference GOV.UK
the-group-refused-naming-the-type-that-breaks-the-ceilingThe Group Refused, Naming the Type That Breaks the Ceilingpage
breadcrumb "Home" "Finance" "Budgets" "Budget codes"
header
heading "Budget codes"
text "What a spender claims and what the budget pays."
message "That is more than the budget holds. Amsterdam sales holds EUR 10,000." critical
region "Three codes"
text muted "EUR 10,400 on the codes. The budget holds EUR 10,000."
list 2
item: text "TRIP10, EUR 500 cap" · text muted "EUR 10,000 on the code. Lower it by EUR 400." · text "EUR 10,000" · action "Change"
footer
action primary "Save codes"
Rationale A quarter steps its ceilings by date, because the budget resets each quarter. The dates are part of each step, so you answer the ceiling on any day by reading one row.
Cost Dated steps plus per-code allocations divide one budget in two ways. They can disagree. A step can close with money left, or run out with days left. One of the two has to win.
chips-with-type-to-add-and-suggestionsChips with Type-to-Add and SuggestionsDefaultpage
breadcrumb "Home" "Expenses" "EXP-1042" "Tags"
header
heading "Tags"
text "Short labels spenders and finance read."
region "The tags"
row
status "travel" neutral
status "meals" neutral
status "client" neutral
field "Tags" search = "tra"
text muted "Lowercase letters, numbers and dashes. New words are allowed."
footer
action "Cancel"
action primary "Save tags"
popover
text "Suggestions"
text "travel-meals, 12 expenses"
text "Use tra as a new tag"
Rationale Linear and Trello order by dragging with button equivalents beside each row, because drag alone strands keyboard users. The spender order is numbered, so the finance order and the filing order cannot drift apart silently.
Cost Order is one more field per code with its own conflicts: two people reordering at once, and a code added in the middle. It needs the same version check as every shared write.
Reference Linear
a-checkbox-list-in-a-popover-with-a-countA Checkbox List in a Popover with a Countpage
breadcrumb "Home" "Expenses" "EXP-1042" "Tags"
header
heading "Tags"
text "Short labels spenders and finance read."
region "The tags"
action "3 tags chosen"
text muted "Tick what applies. New tags are made on the tag list."
footer
action "Cancel"
action primary "Save tags"
popover
field "Search tags" search
field "travel" checkbox
field "meals" checkbox
field "client" checkbox
field "travel-meals" checkbox
link "Manage the tag list"
dialog
heading "Manage the tag list"
text "Renaming a tag renames it on every expense that carries it."
list 4
item: text "travel" · action "Rename"
action "Done"
Rationale The refusal sits above the group and names the offending row below it. GOV.UK puts the summary first and the message on the field. Here the field is a row. TRIP10 carries the overage because TRIP10 grew last.
Cost Blaming one row for a budget total is a guess the page makes about intent: you may sooner cut MEAL20 than TRIP10. The message must offer both, not decide.
Reference GOV.UK
Tag Input#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#tag-inputQuestion Expenses and trips carry short labels: travel, meals, client. Tags are typed freely but read everywhere, so the input must take new words and still keep old ones spelled one way.
Default chips-with-type-to-add-and-suggestions
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The chips with type-to-add and suggestions are the pick: new words are allowed and old ones are offered, so travel is spelled once. The checkbox list is the runner-up where the tag set is closed and counted. Never ship free text with no suggestions: it grows travel, Travel, and travel-meals side by side.
Reviewed for Which built label chip breaks from the wireframe its catalog entry chose? Open the built chips, popover lists, and inherited tags beside the catalog entry recording the chosen option and its reason.
free-text-split-on-commasFree Text Split on Commaspage
breadcrumb "Home" "Expenses" "EXP-1042" "Tags"
header
heading "Tags"
text "Short labels spenders and finance read."
region "The tags"
field "Tags" text = "travel, meals, client"
text muted "Separate tags with commas. Spaces at the edges are removed."
text "Read as"
row
status "travel" neutral
status "meals" neutral
status "client" neutral
footer
action "Cancel"
action primary "Save tags"
Rationale GitHub and Linear offer existing labels as you type. They accept a new word on Enter, so the old spelling wins without forbidding the new one. The count beside each suggestion says how shared it is.
Cost Suggestions must search across every expense on every keystroke pause, and a slow search answers after you have already pressed Enter. The new word wins by timing, not by choice.
Reference GitHub
color-coded-tags-for-audience-segmentsColor-Coded Tags for Spender Groupspage
breadcrumb "Home" "Spenders" "Groups"
header
heading "Groups"
text "Who gets the receipt reminder before the close."
region "The groups"
row
status "Regulars" info
status "Early filers" positive
status "At risk" caution
fact "Regulars" "1,204 spenders, filed twice or more"
fact "Early filers" "2,000 spenders, filed in the first week"
fact "At risk" "318 spenders, filed last quarter, nothing since"
action "Add"
footer
action "Cancel"
action primary "Save groups"
Rationale Shopify and Jira draw a closed tag set as checkboxes with a count on the button. Ticking is faster than typing when the words already exist. Managing the list is a separate task with its own dialog.
Cost A closed set cannot take a new word where it is needed, so you leave the form to make one. It fits a governed vocabulary. It fights a growing one.
Reference Shopify
a-tag-refused-with-the-ruleA Tag Refused with the Rulepage
breadcrumb "Home" "Expenses" "EXP-1042" "Tags"
header
heading "Tags"
text "Short labels spenders and finance read."
region "The tags"
row
status "travel" neutral
status "meals" neutral
field "Tags" text = "Travel Meals!"
message "Tags hold lowercase letters, numbers and dashes. You entered capitals, a space and !." critical
footer
action "Cancel"
action primary "Save tags"
Rationale Stack Overflow and WordPress take tags as one comma-separated line with the parsed chips read back below. Pasting a list from elsewhere works without retyping. The readout shows exactly how the line was split.
Cost No suggestions means no shared spelling: travel and Travel become two tags and nobody is told. The readout shows the split but cannot judge the words.
Reference WordPress
postcode-lookup-that-fills-the-streetPostcode Lookup That Fills the StreetDefaultpage
breadcrumb "Home" "Settings" "Organization"
header
heading "Organization"
text "What Harbor and Pine is and where it works."
region "The address"
row
field "Postcode" text = "1011 AB"
field "Number" text = "12"
action "Look up the street"
field "Street" text = "Canal Street"
field "City" text = "Amsterdam"
field "Country" select = "Netherlands"
text muted "Spenders read this address on their payouts and their mail."
footer
action "Cancel"
action primary "Save address"
Rationale The GOV.UK error template applies to the tag being added. The good chips stay and the bad word carries the message. The rule is stated with the value that broke it.
Cost The message judges one word while the field holds a list, so you must tell which word failed. The chips above carry that by staying put.
Reference GOV.UK
Address Field#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#address-fieldQuestion Harbor and Pine answers for its expenses with a name, an address, and a telephone number on every spender message. The address field takes the street the law asks for and the postcode the mail needs.
Default postcode-lookup-that-fills-the-street
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The postcode lookup is the pick: two short values you know produce the street you would mistype. The country-first manual entry is the runner-up for companies outside the lookup coverage. Never ship a single free-text address box: one line cannot be split into street, postcode, and city again.
Reviewed for Which built location input breaks from the wireframe its catalog entry chose? Open the built lookups, country shapes, and map pins beside the catalog entry recording the chosen option and its reason.
country-first-then-the-local-shapeCountry First, Then the Local Shapepage
breadcrumb "Home" "Settings" "Organization"
header
heading "Organization"
text "What Harbor and Pine is and where it works."
region "The address"
field "Country" select = "Netherlands"
text muted "The fields below follow the country."
field "Street and number" text = "Canal Street 12"
row
field "Postcode" text = "1011 AB"
field "City" text = "Amsterdam"
footer
action "Cancel"
action primary "Save address"
Rationale Postcode.nl and GOV.UK ask for postcode plus number first, because you know both by heart. The lookup then fills the street you would mistype. Two short fields beat one long one.
Cost The lookup covers the Netherlands and nowhere else, so every other country falls back to manual entry. Two paths through one field are two paths to test.
Reference GOV.UK
warehouse-address-with-a-map-pinOffice Address with a Map Pinpage
breadcrumb "Home" "Settings" "Offices" "Rotterdam office"
header
heading "Rotterdam office"
text "Where the finance team sits."
region "The address"
fact "Office" "Rotterdam office"
fact "City" "Rotterdam"
fact "Rooms" "Finance room, audit room"
image "Map pin, Rotterdam office, Rotterdam, pin placed by Harbor and Pine"
footer
action "Move the pin"
action primary "Save address"
Rationale Google Places resolves a half-typed street to a full address, because you know where you are but not how the database spells it. One field holds the whole search.
Cost An address search reads a provider the back office pays per keystroke, and you may choose a wrong first result without reading. The manual fallback must stay one tap away.
Reference Google Places
the-postcode-refused-with-the-expected-shapeThe Postcode Refused with the Expected Shapepage
breadcrumb "Home" "Settings" "Organization"
header
heading "Organization"
text "What Harbor and Pine is and where it works."
region "The address"
field "Street and number" text = "Canal Street 12"
row
field "Postcode" text = "1011"
field "City" text = "Amsterdam"
message "A Dutch postcode holds four digits, a space and two letters, like 1011 AB." critical
footer
action "Cancel"
action primary "Save address"
Rationale Stripe Checkout asks for the country first because the shape of an address follows it: a Dutch postcode is nothing like a German one. The labels and the validation change with the country.
Cost Every supported country needs its own shape, its own labels, and its own validation. Three countries fit this pattern. Thirty need a library.
Reference Stripe
a-verified-address-with-a-change-commandA Verified Address with a Change Commandpage
breadcrumb "Home" "Settings" "Organization"
header
heading "Organization"
text "What Harbor and Pine is and where it works."
region "The address"
fact "Street" "Canal Street 12"
fact "Postcode and city" "1011 AB Amsterdam"
fact "Country" "Netherlands"
status "Verified" positive
message "Verified against the chamber of commerce on 2 Sep 2026. Changing it verifies the account again." info
action "Change the address"
Rationale The office is pinned on a map beside its address, because auditors navigate to a pin, not to a street name. The company places the pin instead of guessing it from the postcode.
Cost A map is a third-party surface with its own terms and its own load time. A static pin box proves the pattern. The live map is a dependency, not a field.
subject-plus-body-with-variable-chipsSubject Plus Body with Variable ChipsDefaultpage
breadcrumb "Home" "Settings" "Email" "Payout sent"
header
heading "Payout sent"
text "The message a spender looks for. Sent when a payout is sent."
region "The message"
field "Subject" text = "Your payout of EUR 125.00 is on its way"
text "Hi spender, your payout left Harbor and Pine. It arrives soon."
row
status "spender_name" neutral
status "expense_number" neutral
status "arrival_date" neutral
text muted "Variables fill per expense when the mail is sent."
field "Insert a variable" select = "Choose a variable"
footer
action "Send a test"
action primary "Save message"
Rationale The GOV.UK error template on a postcode states the shape and gives an example. You repair 1011 without opening a second page, following the example beside the field.
Cost The shape differs per country, so the message must follow the country field. A Dutch message under a Belgian address refuses a value that was right.
Reference GOV.UK
side-by-side-preview-of-the-customer-messageSide-by-Side Preview of the Spender Messagepage
breadcrumb "Home" "Settings" "Email" "Payout sent"
header
heading "Payout sent"
text "The message a spender looks for."
split 1:1
region "Edit"
field "Subject" text = "Your payout of EUR 125.00 is on its way"
field "Body" textarea = "Hi spender, your payout left Harbor and Pine."
card "Spender preview"
heading small "Your payout of EUR 125.00 is on its way"
text "Hi Ava,"
text "Your expense EXP-1042 was paid. It arrives Sat 14 Mar 2026."
text muted "Harbor and Pine, Canal Street 12, 1011 AB Amsterdam"
footer
action "Send a test"
action primary "Save message"
Rationale Banks and company registers show a verified address as a fact with a change command. Payouts and invoices depend on a verified value. The badge states what the last check found.
Cost Verification is a check against a register the back office does not hold, so the badge only grows older from its check date. A move the company forgets to report leaves a true badge on a false address.
Email Body Editor#
patterndefaultguidancelitespec/frontend/patterns/forms-and-fields#email-body-editorQuestion Claimwick mails every spender when a payout is sent, and the message carries the spender name, the expense number, and the arrival date. The editor takes the company words once and fills the spender facts per expense.
Default subject-plus-body-with-variable-chips
Compact The preview sits below the editor. You scroll between writing and reading. The compact option is side-by-side-preview-of-the-customer-message.
Rationale The subject plus body with variable chips is the pick: you write the sentence and place the facts where they belong. The side-by-side spender preview is the runner-up beside it, because a template is believed once it is seen filled. Never ship a template with no preview: variables are spelled in one place and filled in another, and only the filled copy proves they meet.
Reviewed for Which built message composer breaks from the wireframe its catalog entry chose? Open the built subject-plus-body forms and variable chips beside the catalog entry recording the chosen option and its reason.
plain-text-with-a-variable-list-beside-itPlain Text with a Variable List Beside Itpage
breadcrumb "Home" "Settings" "Email" "Payout sent"
header
heading "Payout sent"
text "The message a spender looks for."
split 1:1
region "The message"
field "Subject" text = "Your payout of EUR 125.00 is on its way"
field "Body" textarea = "Hi spender, your payout left Harbor and Pine."
region "Variables"
fact "((spender name))" "Ava"
fact "((expense number))" "EXP-1042"
fact "((arrival date))" "14 Mar 2026"
text muted "Spelled with double brackets. The value fills when the mail is sent."
footer
action "Send a test"
action primary "Save message"
Rationale Klaviyo and Mailchimp draw template variables as chips inside the sentence, because a variable is a word with a value, not code. You read the message as the spender will, with the facts marked.
Cost Chips inside an editable body are a small rich-text editor, not a textarea. Backspace across a chip, paste with chips, and undo all need answers. The plain-text option exists for that reason.
Reference Mailchimp
one-tab-per-languageOne Tab per Languagepage
breadcrumb "Home" "Settings" "Email" "Payout sent"
header
heading "Payout sent"
text "The message a spender looks for."
tabs "Nederlands" "English"
region "Nederlands"
field "Subject" text = "Je uitbetaling is onderweg"
field "Body" textarea = "Hi spender, je uitbetaling is onderweg."
status "English is missing" caution
footer
action primary "Save message"
Rationale Postmark and SendGrid preview a template beside the editor with a real record filled in, because a variable is only proven by its value. The payout of Ava fills every variable this template holds.
Cost Two columns collapse to one on a phone, so the preview sits below the editor and you scroll between them. A long template makes a long round trip.
Reference Postmark
the-send-refused-with-the-missing-variable-namedThe Send Refused with the Missing Variable Namedpage
breadcrumb "Home" "Settings" "Email" "Payout sent"
header
heading "Payout sent"
text "The message a spender looks for."
message "Expense number is missing. The body asks for ((expense number)) and this test expense holds none." critical
region "The message"
field "Subject" text = "Your payout of EUR 125.00 is on its way"
field "Body" textarea = "Hi spender, your payout is on its way."
message "((expense number)) has no value on this expense. Add a fallback." critical
footer
action "Send a test"
action primary "Save message"
Rationale GOV.UK Notify writes templates as plain text with ((double bracket)) variables and lists each one beside the editor with an example value. Nothing about the message can break in a mail client that reads text.
Cost Double brackets are learned, not guessed: ((arrival date)) with one bracket fails silently or loudly depending on the parser. The editor must highlight what it recognizes as you type.
Reference GOV.UK
a-sent-copy-with-the-values-filled-inA Sent Copy with the Values Filled Inpage
breadcrumb "Home" "Expenses" "EXP-1042" "Mail"
header
heading "Payout sent"
text "Sent for expense EXP-1042."
region "The sending"
fact "To" "ava@harbor-pine.example"
fact "Sent" "8 Oct 2026, 09:33"
fact "Expense" "EXP-1042"
status "Delivered" positive
text "Hi Ava,"
text "Your expense EXP-1042 was paid. It arrives Sat 14 Mar 2026."
footer
action "Resend"
Rationale Contentful and Shopify Translate keep one tab per locale with the missing ones badged. A spender reads exactly one language, and you know which ones exist. The badge names the gap instead of hiding it.
Cost Two tabs are two templates to keep in step. A variable added to one is added to the other. The Dutch sentence grows while the English one waits. Tabs hide the drift they also cause.
Reference Shopify
the-send-refused-with-the-missing-variable-named: The Send Refused with the Missing Variable Named#
page
breadcrumb "Home" "Settings" "Email" "Payout sent"
header
heading "Payout sent"
text "The message a spender looks for."
message "Expense number is missing. The body asks for ((expense number)) and this test expense holds none." critical
region "The message"
field "Subject" text = "Your payout of EUR 125.00 is on its way"
field "Body" textarea = "Hi spender, your payout is on its way."
message "((expense number)) has no value on this expense. Add a fallback." critical
footer
action "Send a test"
action primary "Save message"
Rationale GOV.UK Notify refuses a send whose personalization is missing and names the variable. A blank where the expense number belongs is worse than no mail. The message offers the fallback shape, not the complaint.
Cost The refusal can only name what the test record lacks. A variable filled on the test expense but empty on a real one still sends blank. Every variable needs a fallback, not a test.
Reference GOV.UK
a-sent-copy-with-the-values-filled-in: A Sent Copy with the Values Filled In#
page
breadcrumb "Home" "Expenses" "EXP-1042" "Mail"
header
heading "Payout sent"
text "Sent for expense EXP-1042."
region "The sending"
fact "To" "ava@harbor-pine.example"
fact "Sent" "8 Oct 2026, 09:33"
fact "Expense" "EXP-1042"
status "Delivered" positive
text "Hi Ava,"
text "Your expense EXP-1042 was paid. It arrives Sat 14 Mar 2026."
footer
action "Resend"
Rationale Postmark activity keeps the sent copy with every variable filled. Reading what was sent settles a dispute about what the spender was told. The record is read-only: history does not take edits.
Cost A copy per sending is storage that grows with every payout, and each copy holds personal data under retention rules. The record must expire with the payout, not live beside it.
Reference Postmark