Lesson 22 / 22
Microcopy
Computing interface text as a layout constraint, the width budget of action labels, the prefix collisions produced by title truncation, and the three parts of an error message.
Contents
The previous lesson showed that an icon does not replace text, and handed the discussion over to text. Interface text — button labels, field names, empty-state sentences, error messages — has appeared in every lesson of this course, but none of them treated it as a design decision. Text is not a slot to be filled in once the design is finished; it is one of the layout’s hardest constraints.
Microcopy is the functional text of an interface: the short pieces that name an action, report a state, or describe a field. This lesson takes up three questions: how much space does a label take, what does truncation break, and what parts does an error message consist of?
A Label Is a Width Budget
A button’s width derives from its text. Different phrasings that name the same action produce different widths, and those widths have to fit the action strip.
// labels.mjs — action label width and fitting the action strip // Candidate family's advance widths (em = 1000 units); the table from the Line Length lesson. const G = { " ": 260, a: 556, b: 574, c: 500, d: 574, e: 545, f: 340, g: 574, h: 560, i: 244, j: 244, k: 520, l: 244, m: 856, n: 560, o: 570, p: 574, q: 574, r: 366, s: 480, t: 358, u: 560, v: 508, w: 800, x: 500, y: 508, z: 470, A: 660, B: 660, C: 686, D: 720, E: 610, F: 610, G: 740, H: 730, I: 280, J: 280, K: 660, L: 610, M: 890, N: 730, O: 760, P: 610, R: 660, S: 620, T: 620, U: 720, V: 660, W: 890, X: 660, Y: 620, Z: 600, ",": 260, ".": 260, ":": 260, "-": 340, "'": 200, }; const DEFAULT = 560; const SIZE = 16, PADDING = 16, GAP = 8; const width = (s) => ([...s].reduce((t, c) => t + (G[c] ?? DEFAULT), 0) / 1000) * SIZE; const buttonWidth = (s) => width(s) + 2 * PADDING; const CANDIDATES = [ ["long", ["Borrow", "Add to List", "Create Reservation"]], ["short", ["Borrow", "Add to List", "Reserve"]], ["very short", ["Take", "Add", "Reserve"]], ]; const STRIP = 360; // width remaining for the record-detail column on a narrow screen console.log("label set labels button widths total 360 px"); for (const [name, labels] of CANDIDATES) { const widths = labels.map(buttonWidth); const total = widths.reduce((a, b) => a + b, 0) + GAP * (labels.length - 1); console.log( `${name.padEnd(14)} ${labels.join(" / ").padEnd(44)} ${widths.map((v) => v.toFixed(0)).join(" ").padStart(22)} ${total.toFixed(0).padStart(7)} ${total <= STRIP ? "fits" : "OVERFLOWS"}` ); } // Width difference between different phrasings of the same action console.log("\naction label text width button width"); for (const e of ["Borrow", "Please Borrow", "Borrow This Item", "Create Reservation", "Reserve"]) { console.log(`${e.padEnd(22)} ${width(e).toFixed(1).padStart(11)} ${buttonWidth(e).toFixed(1).padStart(13)}`); } // If equal-width buttons are used: the widest label determines everyone const set = CANDIDATES[0][1]; const widest = Math.max(...set.map(buttonWidth)); const equalTotal = widest * set.length + GAP * (set.length - 1); console.log(`\nif equal-width buttons are chosen: each button ${widest.toFixed(0)} px, total ${equalTotal.toFixed(0)} px`); console.log(`variable-width total: ${(set.map(buttonWidth).reduce((a, b) => a + b, 0) + GAP * 2).toFixed(0)} px`); console.log(`cost of switching to equal width: ${(equalTotal - (set.map(buttonWidth).reduce((a, b) => a + b, 0) + GAP * 2)).toFixed(0)} px`);
label set labels button widths total 360 px long Borrow / Add to List / Create Reservation 85 111 171 384 OVERFLOWS short Borrow / Add to List / Reserve 85 111 90 303 fits very short Take / Add / Reserve 68 61 90 235 fits action label text width button width Borrow 53.3 85.3 Please Borrow 105.2 137.2 Borrow This Item 124.7 156.7 Create Reservation 139.3 171.3 Reserve 58.4 90.4 if equal-width buttons are chosen: each button 171 px, total 530 px variable-width total: 384 px cost of switching to equal width: 146 px
The first table shows that text is a layout constraint. “Create Reservation” alone produces a 171-pixel button; with three buttons placed side by side, the total rises to 384 pixels and does not fit the 360-pixel strip. The same three actions come down to 303 pixels with “Reserve” instead, and fit.
The conclusion that follows is that shortening is not a matter of style but a measurement decision. The information lost in shortening is also accounted for: “Create Reservation” and “Reserve” name the same action, but “Take” does not — the information about what is being taken is lost.
The second table measures how far a courtesy word or the object of the action can inflate a label. “Borrow” measures 53.3 pixels; the courteous form “Please Borrow” comes to 105.2 pixels; the object-qualified form “Borrow This Item” comes to 124.7 pixels. Adding a word and naming the object can more than double the width. Action labels take the short imperative form; this is not a matter of tone but a budget decision of roughly seventy pixels.
The third block gives the cost of a layout habit. When equal-width buttons are chosen, the widest label determines every button, and the total rises to 530 pixels — 1.4 times the variable-width total. This 146-pixel difference cannot be absorbed on a narrow screen. Equal width is chosen only when the labels are already close in length and the strip has room to spare.
Truncation Can Destroy Distinctiveness
Record titles in the result list are truncated when they do not fit the column. Truncation’s visible cost is that part of the information disappears; its invisible cost is that two different records become identical on screen.
// truncation.mjs — title truncation, prefix collisions, and the three parts of an error message const G = { " ": 260, a: 556, b: 574, c: 500, d: 574, e: 545, f: 340, g: 574, h: 560, i: 244, j: 244, k: 520, l: 244, m: 856, n: 560, o: 570, p: 574, q: 574, r: 366, s: 480, t: 358, u: 560, v: 508, w: 800, x: 500, y: 508, z: 470, A: 660, B: 660, C: 686, D: 720, E: 610, F: 610, G: 740, H: 730, I: 280, J: 280, K: 660, L: 610, M: 890, N: 730, O: 760, P: 610, R: 660, S: 620, T: 620, U: 720, V: 660, W: 890, X: 660, Y: 620, Z: 600, ",": 260, ".": 260, ":": 260, "-": 340, "'": 200, "…": 800, }; const DEFAULT = 560; const width = (s, px) => ([...s].reduce((t, c) => t + (G[c] ?? DEFAULT), 0) / 1000) * px; function truncate(s, px, limit) { if (width(s, px) <= limit) return s; const ellipsis = width("…", px); let result = ""; for (const c of s) { if (width(result + c, px) + ellipsis > limit) break; result += c; } return result + "…"; } const TITLES = [ "Light and Shadow", "Studies in the History of the Republic of Turkey Volume I", "Studies in the History of the Republic of Turkey Volume II", "Protocol Reference Manual", ]; const SIZE = 20; // record title size, scale step 1 console.log(`record title size: ${SIZE} px`); for (const col of [180, 260, 340, 520]) { const truncated = TITLES.map((t) => truncate(t, SIZE, col)); const unique = new Set(truncated); console.log(`\ncolumn ${col} px (unique titles: ${unique.size} / ${TITLES.length})`); for (let i = 0; i < TITLES.length; i++) { const k = truncated[i]; const colliding = truncated.filter((x) => x === k).length > 1; console.log(` ${JSON.stringify(k).padEnd(52)} ${width(k, SIZE).toFixed(0).padStart(4)} px ${colliding ? "COLLIDES" : k === TITLES[i] ? "full" : "truncated"}`); } } // The three parts of an error message: what happened, why, what to do const PARTS = [ { name: "what happened", text: "The ISBN number could not be validated" }, { name: "why", text: "The last digit does not match the check digit" }, { name: "what to do", text: "Retype the number from the back cover of the book" }, ]; const MESSAGE_SIZE = 13, LINE_LIMIT = 525; console.log("\npart text width (13 px) fits one line (525 px)"); let total = 0; for (const p of PARTS) { const g = width(p.text, MESSAGE_SIZE); total += g; console.log(`${p.name.padEnd(14)} ${g.toFixed(1).padStart(19)} ${g <= LINE_LIMIT ? "yes" : "no"}`); } const all = PARTS.map((p) => p.text).join(". "); console.log(`all three combined: ${width(all, MESSAGE_SIZE).toFixed(1)} px, line count: ${Math.ceil(width(all, MESSAGE_SIZE) / LINE_LIMIT)}`);
record title size: 20 px column 180 px (unique titles: 3 / 4) "Light and Shadow" 165 px full "Studies in the His…" 174 px COLLIDES "Studies in the His…" 174 px COLLIDES "Protocol Referenc…" 178 px truncated column 260 px (unique titles: 3 / 4) "Light and Shadow" 165 px full "Studies in the History of th…" 257 px COLLIDES "Studies in the History of th…" 257 px COLLIDES "Protocol Reference Manual" 246 px full column 340 px (unique titles: 3 / 4) "Light and Shadow" 165 px full "Studies in the History of the Republ…" 336 px COLLIDES "Studies in the History of the Republ…" 336 px COLLIDES "Protocol Reference Manual" 246 px full column 520 px (unique titles: 4 / 4) "Light and Shadow" 165 px full "Studies in the History of the Republic of Turkey Volume I" 510 px full "Studies in the History of the Republic of Turkey Volume II" 516 px full "Protocol Reference Manual" 246 px full part text width (13 px) fits one line (525 px) what happened 237.3 yes why 260.1 yes what to do 302.8 yes all three combined: 813.7 px, line count: 2
Both volumes come down to the identical truncated title at all three narrower column widths. Four records appear on screen as three distinct titles; the user cannot tell from the list which volume they are borrowing. When the column widens to 520 pixels, the collision disappears.
The fix for this finding is not to widen the column. Widening is not always possible, and titles can always get longer still. The fix is to truncate in a way that preserves the distinguishing part: the volume information is separated from the title and moved to its own metadata field, shown in a field that is never truncated. The precondition for a truncation decision is knowing which piece of information is distinguishing.
The second rule is that the full form of the truncated text stays accessible. Truncation is a presentation decision; the record’s accessible name continues to carry the untruncated title.
An Error Message Has Three Parts
The last table measures the structure of an error message. A good message answers three questions: what happened, why it happened, what to do. Each of the three stays under the 525-pixel line limit on its own; combined into a single sentence, they rise to 813.7 pixels and become two lines.
Two lines are not a flaw, but squeezing the parts into a single block of text is. Kept separate, each part can be placed at a different typographic level: the “what happened” part is brought forward with weight, while “why” and “what to do” stay at a secondary text level. A user who recognizes the problem reads only the first part; one who does not keeps reading.
Not all three parts are always necessary. The “why” part is written when the user cannot guess the reason on their own; the ISBN’s check digit is such a case. The “what to do” part, by the rule set in the Error and Warning States lesson, is mandatory: if the error is recoverable, the path to recovery has to be in the message.
The Unchanging Rules of Text
The microcopy decisions accumulated along the course’s through-line come together in a few rules.
The action label names the action. Generic words like “OK” and “Submit” do not tell the user, in a confirmation dialog, what they are confirming. The pair “Borrow” and “Cancel” lets the user read what each button does straight from the text itself. This is also the only setup in which the user can decide without reading the surrounding message.
A label is written in the user’s language, not the interface’s. “Create Record” is a database operation; “Add Book” is the task the user is doing. The catalog interface’s vocabulary is the library’s vocabulary.
The same thing is called by the same name everywhere. If one place says “borrowing,” another says “checkout,” and a third says “loan,” the user assumes there are three separate operations. The rule set for visuals in the Repetition and Consistency lesson applies to text as well.
Placeholder text is not a label. The Contrast and Accessibility lesson computed this: placeholder text cannot do a label’s job because it fails to clear the contrast threshold. A field’s name stands above the field; the placeholder only shows a format example.
Summary
- Microcopy is a layout constraint; a label’s width is computed and compared against the action strip’s budget.
- Adding a courtesy word or naming the object noticeably lengthens a label; the short imperative form in action labels is a measurement decision.
- Equal-width buttons lock to the widest label and noticeably increase the total width; a narrow strip cannot absorb it.
- Truncation does not just cut information — it can make two records identical on screen; the distinguishing information is pulled out of the truncated field, and the full form of the truncated text stays accessible.
- An error message consists of what happened, why it happened, and what to do; the parts are kept separate and placed at separate typographic levels.
- A label names the action, is written in the user’s language, and the same thing is called by the same name everywhere.
Course Wrap-Up
This course opened with a single claim: every visual decision carries a function, and it is design if it can be justified. Across twenty-two lessons, the library catalog interface was where this claim was tested.
Four topics built on one another. Visual Principles established how attention is ordered, how elements are grouped, and how consistency produces learnability. Layout and Spacing set these principles into a skeleton: the grid, the spacing scale, density decisions, and the constraints screen size and input type bring. Typography and Color made the text and surface that fill the skeleton measurable — type sizes, the typographic scale, line length, color roles, contrast thresholds, and the theme transformation. Component States turned the static image into an interface that behaves over time: interaction states, waiting, emptiness, error, icons, and text.
Three rules recurred throughout the course, resurfacing in every topic. First, a critical distinction is expressed on at least two channels; this rule showed up as size and weight in hierarchy, as color and text in semantic colors, and as surface and position in component states. Second, once a decision is tied to a number, argument turns into verification; thresholds did their job because they were checkable, even when chosen somewhat arbitrarily. Third, the cheapest fix for a problem is most often not adding an element but removing one; reducing the number of levels, dropping a border, writing the reason instead of disabling a button were all examples of this rule.
There is something this course did not cover, and the boundary needs to be drawn there. Every decision made here was the visual answer to “what should the interface do.” Which tasks the interface should support, in what order the user performs those tasks, and when they need which piece of information stayed outside this course. In the catalog interface, the “search field, result list, record detail, borrow” flow was given from the start; why it was that flow was never asked.
M15/K02 User Experience and Behavior Design opens with this question. It builds the chain from user research to design decision: it covers when observation, interview, and survey methods are chosen, how user profiles grounded in real data are built, and how information architecture and flow design make task completion easier. What this course measured was the inside of a screen; what the next course measures will be the path between screens.
To keep your progress and take notes, Log in
My notes
Log in to take notes.