Interface Copy
Words are the only part of an interface the user reads literally; everything else is inferred. A wrong word therefore does more damage than a wrong margin, and faster: nobody has to squint to notice that "Submit" says nothing about what will happen.
Every string answers a question the user is already asking. A button answers "what happens if I press this?" An error answers "what broke and what do I do now?" An empty state answers "is this broken, or have I not started?" Copy that does not answer its question is decoration, and decoration made of text is read before it is found worthless.
1. Voice is constant, register moves
The product should sound recognisably itself everywhere, but register must change — expansive on a landing page, terse and factual in a disk-quota warning. Register is set by how much attention the user has and how much is at stake.
Low stakes — a toast, a menu item — want the fewest possible words. High stakes — deleting a workspace, entering card details — want precision and a slower rhythm, and this is where playfulness becomes offensive. Marketing voice in a settings screen is the commonest register failure: a user three levels deep in billing is working, not browsing, and "Let's get you set up!" reads as a colleague interrupting.
Default for product UI: second person, present tense, active voice.
2. Buttons name the outcome
A button label should be the verb phrase the user would use to describe what they just did. "Submit" describes what the form does to the server; "Create account", "Send invite", "Delete 4 files" describe what the user gets.
This matters most in dialogs, where the eye reaches the buttons before the prose. Buttons reading "OK" and "Cancel" force a re-read to work out which one destroys data — and "Cancel" is catastrophically ambiguous when the action is itself a cancellation ("Cancel subscription?" / "Cancel"). Label the buttons with outcomes and the prose becomes optional: "Keep subscription" and "Cancel subscription" need no explanation. Match the heading's verb: if the dialog asks "Discard changes?", the button says "Discard", not "Yes".
3. Errors: cause, then next action
A useful error has three parts and most shipped errors have none: what happened, why, and what to do next. The user needs neither the stack trace nor an apology. "Something went wrong" fails because it is true of every possible failure; "Invalid input" fails because it names a verdict rather than a cause.
- Bad:
Error: Invalid email.Good:This email address is missing an @ symbol. - Bad:
Oops! Something went wrong.Good:We couldn't save your changes — the server didn't respond. Your draft is stored locally, so try again in a moment.
Never blame the user: "You entered an invalid date" and "Choose a date on or after today" say the same thing, and only one is an accusation. Reserve apology for genuine service failures, so it still means something when used.
4. Empty states are the first lesson
The empty state is seen by every new user and is usually the least-considered screen in the product. "No items" wastes the best teaching moment available.
Use three parts in order: what belongs here, why it is useful, one action. "No saved views yet" plus "Saved views keep a filter and sort order so you can return in one click" plus a single "Create saved view" button turns a dead end into onboarding. One action only; four equal options reproduce the paralysis the state exists to resolve.
Distinguish the three empties that look identical: never had data, filtered to nothing, and failed to load. The second needs "Clear filters", not "Create your first item"; the third needs "Retry", and rendering it as an empty list tells the user their data is gone.
5. Confirmations state consequences
"Are you sure?" moves the burden of understanding onto the user at the moment they are least able to carry it. It tests confidence, not comprehension, and is dismissed reflexively.
State the irreversible part instead: "Delete 12 files permanently? They can't be recovered." Name the object and the count — "Delete project" is weaker than "Delete Acme Redesign and its 40 tasks" — and say when something is reversible, because most confirmation anxiety is uncertainty about reversibility. Better still, for reversible actions drop the dialog and offer undo; a confirmation taxes the common case to guard against the rare one.
6. Tone failures with names
Exclamation marks perform an enthusiasm the reader does not feel, and in a failure message they are hostile: "Something went wrong!" is cheerful about your data loss. "Oops", "Whoops", "Uh-oh" signal that the system finds its own failure charming. Mock-cheerful failure ("our hamsters need a break") carries no diagnostic content and cannot be searched for in a support forum. Jargon leaking upward — "null reference", "422", "token expired" — is implementation vocabulary; translate it: "Your session ended. Sign in to continue."
7. Mechanics to decide once
Case. Sentence case ("Save changes") or title case ("Save Changes") — pick one for every button, heading, tab, and menu item. Sentence case is safer: title case has no agreed rule set across English variants, so a codebase using it drifts within weeks.
Numerals. Digits, not words: "3 files". A non-breaking space between value and unit
("24 MB") so they never wrap apart. Format with Intl.NumberFormat.
Time. Relative time ("3 minutes ago") is right when recency is the point and precision is
not. Absolute time is right when the moment may be referenced, compared, or reported — audit
logs, receipts, scheduled events — and relative time decays, so "2 years ago" is worse than
the date. Ship relative text inside <time datetime="2026-07-25T10:00:00Z"> with the
absolute value on hover.
Plurals. Never "1 items", never "item(s)". English has two plural forms, Arabic six and
Polish four, and the form is chosen by the numeral itself, so appending an s is a bug, not
a shortcut. Use Intl.PluralRules or ICU syntax, and write the zero case as its own branch:
"No results" beats "0 results" — zero reports absence, not quantity.
8. Truncation, placeholders, translation
Truncate where the information stops being useful and keep the full value reachable: filenames truncate in the middle so the extension survives ("annual-report…-final.pdf"), sentences truncate at the end, and any value the user must act on needs the whole string exposed on hover, focus, or in a detail view.
Placeholder text is not a label. It disappears on focus, so the field's name vanishes exactly
while it is being filled, it usually fails contrast, and it is not a reliable accessible
name. Ship a visible <label>; let the placeholder carry only a format example.
Copy-driven layout needs headroom. German running text averages roughly 30% longer than
English and short labels can more than double, so never size a button to its English string.
Use logical properties (padding-inline, text-align: start) so right-to-left locales
mirror correctly, and remember that RTL flips more than text: icon order, progress direction,
back arrows and slider polarity mirror, while clocks and numerals do not.
9. The accessible name is copy
Screen reader and voice control users hear the accessible name, not the pixels. For a button
it resolves from aria-labelledby, then aria-label, then text content — and aria-label
silently overrides visible text.
That override is where copy breaks accessibility. A button reading "Save" with
aria-label="Submit form" cannot be operated by a voice user saying "click Save", because
the utterance is matched against the accessible name. WCAG 2.2 SC 2.5.3 (Label in Name)
requires the accessible name to contain the visible label: a name may extend the visible
text but must never replace it.
Link text must stand alone, because screen reader users navigate by listing every link with its context stripped. Eleven "Read more" links produce eleven identical entries. Write the destination in: "Read the migration guide".
10. Before shipping
Search for lorem, ipsum, TODO, asdf, Oops, and ! in message strings. Placeholder
copy reaching production is not a rare accident — it occupies the right shape, so it survives
visual review and is caught only by a string search.
Rules
MUST NOT — Do not use exclamation marks, "Oops", "Whoops", or jokes in messages that report a failure.
Why: Failure copy is read by a user who has just lost time or data. Performed cheerfulness signals that the system does not consider the loss serious, which converts irritation into distrust, and the words displace the diagnostic information the message should have carried.
Incorrect:
Oops! Something went wrong!
Correct:
We couldn't reach the server. Your draft is saved locally.
MUST NOT — Do not use "Click here", "Read more", "Learn more", or "This link" as the entire accessible name of a link.
Why: Screen reader users routinely navigate by generating a list of every link on the page with all surrounding context stripped, so identical generic names produce a list of indistinguishable entries and the page becomes unnavigable by that route.
Source: WCAG 2.2 SC 2.4.4 Link Purpose (In Context)
Exceptions:
- The visible text may remain "Read more" if an aria-label preserves it and extends it, e.g. "Read more about pricing".
MUST NOT — Do not ship lorem ipsum, "TODO", dummy names, or invented testimonial text in any user-facing string.
Why: Placeholder prose reads as intentional in review because it occupies the correct shape, so it passes visual inspection and is caught only by a string search. Invented attributed quotes are additionally a misrepresentation, not merely an omission.
MUST — Label every button with the verb phrase describing the outcome, not with a generic acknowledgement such as Submit, OK, Yes, or Continue.
Why: Users read buttons before body text, so the label is often the only text consulted before committing. A generic label forces a return to the prose to determine consequence, and under time pressure that re-read does not happen.
Exceptions:
- Platform-standard dialogs whose button labels are supplied by the operating system.
Incorrect:
<button type="submit">Submit</button>
Correct:
<button type="submit">Create account</button>
MUST — A confirmation dialog must state what will happen and whether it is reversible, rather than asking whether the user is sure.
Why: Asking for certainty tests confidence rather than comprehension and adds no information the user did not already have, so it is dismissed reflexively. Naming the object, the scope, and the reversibility supplies the fact the user is missing.
Exceptions:
- Reversible actions, where an undo affordance should replace the dialog entirely.
Incorrect:
Are you sure? [Yes] [No]
Correct:
Delete Acme Redesign and its 40 tasks? This cannot be undone. [Delete project] [Keep project]
MUST — Every error message must name the specific cause and the next action the user can take.
Why: An error is an interruption the user did not choose; its only value is restoring forward motion. A message true of every failure carries no information and converts a recoverable state into a support request.
Source: WCAG 2.2 SC 3.3.1 Error Identification and SC 3.3.3 Error Suggestion
Exceptions:
- Security-sensitive failures where naming the cause would leak information, such as distinguishing an unknown account from a wrong password.
Incorrect:
Something went wrong. Please try again later.
Correct:
report.pdf is 24 MB and the limit is 10 MB. Compress it or upload a smaller file.
MUST — Distinguish never-had-data, filtered-to-nothing, and failed-to-load states with different copy and different actions.
Why: The three states share a visual shape but require opposite responses: create something, clear the filter, or retry. Rendering a load failure as an empty list actively misinforms the user that their data no longer exists.
MUST — Every form field must have a persistent visible label; placeholder text may only supplement it with a format example.
Why: Placeholder text vanishes on focus, so the field name disappears at the moment it is needed, which particularly harms users with memory or attention differences. Placeholders also typically fail contrast requirements and are not a reliable accessible name.
Source: WCAG 2.2 SC 3.3.2 Labels or Instructions
Incorrect:
<input type="email" placeholder="Email address" />
Correct:
<label for="email">Work email</label>
<input id="email" type="email" placeholder="name@company.com" />
MUST — When a control has visible text, its accessible name must contain that text, in the same order — never replace it with an unrelated aria-label.
Why: Speech input matches a spoken command against the accessible name rather than the rendered pixels, so an aria-label that discards the visible word makes the control unreachable by voice and gives the user no feedback about why.
Source: WCAG 2.2 SC 2.5.3 Label in Name
Incorrect:
<button aria-label="Submit form">Save</button>
Correct:
<button aria-label="Save draft to your workspace">Save draft</button>
MUST — Resolve plurals with Intl.PluralRules or ICU message syntax, never by appending an "s" or shipping "item(s)".
Why: English has two plural categories but the CLDR set spans six, and languages such as Polish, Russian, and Arabic select a form based on the numeral itself. String concatenation therefore produces grammatically wrong output in most target locales, and "(s)" is unreadable aloud.
Source: ECMA-402 Intl.PluralRules; Unicode CLDR plural categories
Incorrect:
const label = `${count} item${count === 1 ? "" : "s"}`
Correct:
const label = t("itemCount", { count }) // "{count, plural, =0 {No items} one {# item} other {# items}}"
MUST — Never size a container to fit its English string; allow roughly 30% expansion for running text and more for short labels, and use logical properties for direction.
Why: German running text averages around 30% longer than English and short UI labels can more than double, so a container fitted to English clips or wraps badly on translation. Physical properties such as padding-left do not mirror in right-to-left locales, where icon order, progress direction, and arrows must all flip.
Source: W3C Internationalisation guidance on text expansion
Incorrect:
.button { width: 88px; padding-left: 12px; text-align: left; }
Correct:
.button { min-width: 88px; padding-inline: 12px; text-align: start; }
SHOULD NOT — Do not phrase validation messages as accusations directed at the user ("You entered an invalid…"); state the constraint instead.
Why: Second-person blame adds an emotional cost without adding information, and the constraint phrasing is strictly more useful because it tells the user what a valid value looks like rather than only that theirs was not.
Incorrect:
You entered an invalid date.
Correct:
Choose a date on or after today.
SHOULD NOT — Do not use marketing or promotional voice in utility surfaces such as settings, billing, errors, or destructive dialogs.
Why: Register signals what kind of interaction is happening. A user deep in billing is performing a task with consequences, and persuasive enthusiasm there reads as an interruption by something that wants their attention rather than a tool that is helping them finish.
Incorrect:
Ready to supercharge your workflow? Let's get you set up!
Correct:
Your plan renews on 12 August 2026.
SHOULD — Give every empty state three parts: what belongs here, why it is useful, and exactly one action.
Why: The empty state is seen by every new user before any populated screen, making it the earliest available teaching surface. A bare "No items" cannot be distinguished from a fault, and multiple competing actions reproduce the paralysis the state exists to resolve.
SHOULD — Choose sentence case or title case once and apply it to every button, heading, tab, and menu item in the product.
Why: Title case has no agreed rule set across English variants for prepositions, conjunctions, and hyphenated words, so a codebase using it drifts into inconsistency that readers perceive as carelessness without being able to name it. Sentence case has one rule and survives contributor turnover.
SHOULD — Write the zero case as its own string rather than letting it fall through to the plural form.
Why: Zero is semantically a different message from a count: it reports absence rather than quantity. "No unread messages" answers the user question, whereas "0 unread messages" makes the reader parse a numeral to reach the same conclusion.
SHOULD — Use relative time only for recent events where precision does not matter, and always expose the absolute timestamp via a element or tooltip.
Why: Relative time is easier to read for recency but loses resolution as it ages and cannot be compared, cited, or reconciled with an external record — which is exactly what a user needs from an audit log, receipt, or scheduled event.
Incorrect:
<span>2 years ago</span>
Correct:
<time datetime="2024-03-04T09:12:33Z" title="4 March 2024, 09:12 UTC">4 March 2024</time>
SHOULD — When truncating a value the user may need to act on, keep the full value reachable on hover, focus, or in a detail view, and truncate filenames in the middle so the extension survives.
Why: Truncation discards information the layout could not fit, not information the user did not need. Without a recovery path the user cannot distinguish two similarly-prefixed items, and a trailing ellipsis on a filename removes the extension, which is often the most identifying part.
SHOULD — Replace implementation vocabulary — status codes, exception names, internal entity names — with words from the user’s model of the product.
Why: Implementation terms are precise only for people who can act on them. For everyone else they add reading cost and imply the failure is theirs to diagnose, which stalls the user at exactly the point the message should be moving them forward.
Exceptions:
- Developer-facing tools, and error surfaces where a copyable code genuinely helps support — include it as secondary detail, not as the message.
Before reporting completion
Run these checks against your own output. Answer each question explicitly rather than assuming the answer, because the point of the exercise is to notice what you did not notice while building.
Confirm labels and headings answer the user’s question. (blocking)
- For every button on screen, does the label name the outcome rather than the mechanism?
- If the user read only the buttons and not the body text, could they choose correctly?
- Is the case convention (sentence or title) identical across every label, heading, tab, and menu item?
- Does any string use marketing register on a utility surface?
Confirm every failure message is actionable. (blocking)
- List every error string. Does each name a specific cause rather than a generic failure?
- Does each state what the user should do next?
- Does any error contain an exclamation mark, "Oops", a joke, or an unexplained status code?
- Does any message blame the user rather than state the constraint?
Confirm empty states teach rather than terminate.
- Does the empty state say what belongs here and why it is useful?
- Does it offer exactly one action?
- Are the never-had-data, filtered-to-nothing, and failed-to-load cases distinguished by different copy and different actions?
Confirm spoken names match visible words. (blocking)
- Does any aria-label replace rather than extend the visible text of its control?
- Does every icon-only control have a name describing the action, not the icon?
- Are any two controls on the screen announced with the same name?
- Does every form field have a persistent visible label rather than a placeholder acting as one?
- Read the list of every link name on the page alone — is each one meaningful?
Confirm the layout survives translation and direction change.
- Does every layout hold with each string 40% longer?
- Is any container sized to the exact width of its English label?
- Are directional styles written with logical properties (padding-inline, text-align: start) rather than left and right?
- Are counts, dates, currencies, and units formatted through Intl rather than concatenated?
Fail if placeholder or draft copy reaches user-facing strings. (blocking)
grep -rniE "lorem ipsum|dolor sit amet|asdf|\\bTODO\\b|FIXME|Oops|Whoops" --include="*.tsx" --include="*.jsx" --include="*.vue" --include="*.svelte" --include="*.json" src && exit 1 || exit 0
Further reference
These are not loaded by default. Read one only when its question is the question you currently have.
references/copy-rewrites.md— I have a specific bad string — a button, error, empty state, dialog, tooltip, or notification — what does the corrected version look like and why?references/accessible-naming.md— How do I write aria-label, alt text, and link text so screen reader and voice control users get the same information as sighted users?