Voice and content

Two different kinds of text get written against this system, and only one of them is the system's business.

Chrome is the functional text a component renders: labels, hints, placeholders, errors, empty states, toasts, confirmations, validation. It tells the reader what a thing is, what just happened, or what to do next. The system has rules for it, they apply to every app built on the system, and @elirobinson/eslint-config enforces part of them.

Content is everything a product says in its own voice — marketing prose, editorial pages, conversational surfaces, written deliverables. That voice is a design decision belonging to the product, and the system has nothing to say about it.

Where one surface carries both, the chrome follows the rule below and the content is left alone.

The chrome rule

State the fact, then the consequence, then the action — and stop.

❌ You have not been charged. This is almost always a passing blip on their
   side, so try again in a moment. If it keeps happening, reply to your
   receipt email and we'll sort it out.

✅ You have not been charged. Try again.

Both are true. The second respects the reader. The failure mode is padding a true message with reassurance until it reads like marketing: frequency claims you have no data for, blame attribution, filler pacing, unprompted apology, escalation paths nobody asked for, exclamation marks.

Length is the tell. Past two short sentences, functional copy is explaining, reassuring, or selling.

The full rule, with every phrase it forbids, is in the package rather than on this page — it changes with the lint rule, and a copy here would go stale:

pnpm ds patterns    # the rule, with worked examples
pnpm ds contracts   # the functional-copy constraint, its check and what verifies it

@elirobinson/eslint-config warns on the literal phrases, scoped to copy props (title, description, label, placeholder, and friends) and the children of chrome components (Alert, Toast, EmptyState). It never reads ordinary prose. It ships as a warning because every repo upgrading into it has copy written before it existed; tighten it once that copy is clean:

designSystem({ copy: { severity: 'error' } });

In components

  • Button labels are verbs — "Save changes", "Send message". A label that names a noun leaves the reader guessing what the button does to it.
  • Empty states say what goes here and how to add it, and give the reader the control that does it — see EmptyState.
  • Error messages name the problem and the fix, in that order, without assigning fault.
  • An Alert is for a sentence, a Badge for a word. If a message needs a third sentence, it needs a page.
  • Icon-only controls still need a name in text: aria-label is chrome too, and it is read aloud in place of the label a sighted reader never needed.
  • No decorative emoji in chrome. Where a mark carries meaning, use an icon with a text label; a screen reader announces an emoji by its Unicode name, which is rarely the word you meant.

The product's own voice

The system does not set it, and reading the chrome rule as an instruction to flatten it does more damage than the padding it removes.

That includes person. Whether a product writes "I" or "we" is a decision about what the product is — one name can cover a single person's site and a company's product — so the system does not fix it. Choose per product, then hold it: the tell of a voice nobody decided is one that switches partway down a page.

Miltinson's own brand voice — how it addresses a reader, its casing and punctuation conventions, its word lists, its iconography — is a voice pack: design-system-docs/miltinson.voice.json. Every surface that shows it is generated from that one file, so the copies cannot disagree again. It is the pack this system ships by default, not a rule of the system: a product declares its own with ds init --voice, and ds voice prints the pack in force and says which one it is.

pnpm ds voice            # the pack in force, and whether it is yours or inherited
pnpm ds init --voice     # scaffold a voice.json to replace it