Lesson 21 / 22
Icon Usage
The distinction between semantic and decorative icons, computing an accessible name with a small model, checking an icon set for polysemy, and consistency between scale and stroke thickness.
Contents
The previous lesson left the icon placed next to an error message unjustified. The Functional Colors lesson counted the icon as the second channel accompanying color, but the question of when the icon itself carries information and when it merely takes up space was never asked.
This lesson treats the icon through a single distinction: does the icon carry information, or does it visually reinforce the information the neighboring text already carries? The distinction is not aesthetic; it produces different outcomes in markup, and an icon that falls on the wrong side either loses information entirely or has the same information read out twice.
Two Classes, Two Different Outcomes
A semantic icon communicates a piece of information or an action on its own. There is no text next to it. In the catalog interface, the magnifying glass at the end of the search field and the remove icon at the end of a record row fall into this class. These icons have to have a name; an icon button with no name is a button that does not exist for the keyboard or a screen reader.
A decorative icon visually repeats what the neighboring text already communicates. The exclamation icon at the start of an error message falls into this class; the message already says “the ISBN number could not be validated.” These icons should not have a name; if they do, the same information is announced twice.
The distinction is tested with a simple question: if the icon is removed, does information disappear? If it does, the icon is semantic; if it does not, the icon is decorative. The same icon can fall into two different classes in two different places; the magnifying glass carries meaning when it stands alone, and is decorative inside a button labeled “Search.”
The Accessible Name Can Be Computed
An element’s accessible name is derived by following a defined order. In short, the
order is: an aria-labelledby reference, then the aria-label value, then text
derived from the element’s content, and last the title attribute. This order can be
brought to life with a model, and icon setups can be checked one by one.
// accessible-name.mjs — computing the accessible name of icon-bearing buttons // A small tree model: each node carries a tag, an attribute set, text, and children. const node = (tag, attributes = {}, children = [], text = "") => ({ tag, attributes, children, text }); // Tags that can derive a name from their content const NAME_FROM_CONTENT = new Set(["button", "a", "th", "label", "legend", "summary"]); function collectText(d, ref) { if (d.attributes["aria-hidden"] === "true") return ""; if (d.tag === "svg") { if (d.attributes.role === "img") { const title = d.children.find((c) => c.tag === "title"); return title ? title.text : ""; } return ""; } if (d.tag === "img") return d.attributes.alt ?? ""; const parts = [d.text, ...d.children.map((c) => collectText(c, ref))]; return parts.filter(Boolean).join(" ").replace(/\s+/g, " ").trim(); } function accessibleName(d, registry = {}) { // 1) aria-labelledby if (d.attributes["aria-labelledby"]) { const names = d.attributes["aria-labelledby"] .split(/\s+/) .map((id) => (registry[id] ? collectText(registry[id], registry) : "")) .filter(Boolean); if (names.length) return { name: names.join(" "), source: "aria-labelledby" }; } // 2) aria-label if (d.attributes["aria-label"] && d.attributes["aria-label"].trim()) { return { name: d.attributes["aria-label"].trim(), source: "aria-label" }; } // 3) name from content if (NAME_FROM_CONTENT.has(d.tag)) { const m = collectText(d, registry); if (m) return { name: m, source: "content" }; } // 4) title attribute (last resort) if (d.attributes.title && d.attributes.title.trim()) { return { name: d.attributes.title.trim(), source: "title (last resort)" }; } return { name: "", source: "none" }; } const icon = (attributes = {}, children = []) => node("svg", attributes, children); const titleNode = (text) => node("title", {}, [], text); const EXAMPLES = [ ["bare icon", node("button", {}, [icon()])], ["aria-label + hidden icon", node("button", { "aria-label": "Borrow" }, [icon({ "aria-hidden": "true" })])], ["role=img + title", node("button", {}, [icon({ role: "img" }, [titleNode("Borrow")])])], ["hidden icon + text", node("button", {}, [icon({ "aria-hidden": "true" }), node("span", {}, [], "Borrow")])], ["named icon + text", node("button", {}, [icon({ role: "img" }, [titleNode("Borrow")]), node("span", {}, [], "Borrow")])], ["title only", node("button", { title: "Borrow" }, [icon()])], ]; console.log("setup accessible name source result"); for (const [name, tree] of EXAMPLES) { const s = accessibleName(tree); let result; if (!s.name) result = "UNNAMED"; else if (/^(.+) \1$/.test(s.name)) result = "DUPLICATED"; else if (s.source.startsWith("title")) result = "weak"; else result = "valid"; console.log(`${name.padEnd(29)} ${JSON.stringify(s.name).padEnd(22)} ${s.source.padEnd(21)} ${result}`); }
setup accessible name source result bare icon "" none UNNAMED aria-label + hidden icon "Borrow" aria-label valid role=img + title "Borrow" content valid hidden icon + text "Borrow" content valid named icon + text "Borrow Borrow" content DUPLICATED title only "Borrow" title (last resort) weak
The first row is the most common mistake: the icon is placed inside the button, and nothing else is written. The computed name is empty; the button is visible on screen but has no name. A keyboard user who focuses the button cannot tell what it does.
The fifth row is the mistake in the opposite direction. The icon is both named and has text next to it; the name computes to “Borrow Borrow.” Naming a semantic icon in a place where it should be decorative produces this result.
The sixth row works, but weakly. The title attribute is the last link in the naming
chain, and it is only visible while the pointer rests on it; it never appears with
touch input and is not reliable with keyboard access. Giving the name through title
sits somewhere between giving a name and giving none.
The second, third, and fourth rows are valid setups, and the choice among them depends on the situation. If the icon stands alone, use the second or third; if the icon has text next to it, use the fourth — the icon is hidden and the name comes from the text.
An Icon Set Must Be Internally Consistent
Even when individual icons are marked up correctly, a consistency problem can remain at the level of the set: the same shape carrying two different meanings, or two close meanings being drawn with the same shape.
// icon-set.mjs — icon set consistency check and scale/stroke-thickness math // Meaning-to-shape mapping of icons used in the catalog interface const SET = [ { meaning: "search", shape: "magnifying-glass" }, { meaning: "filter", shape: "funnel" }, { meaning: "borrow", shape: "down-arrow-box" }, { meaning: "download", shape: "down-arrow-box" }, { meaning: "remove", shape: "trash-can" }, { meaning: "delete", shape: "x-mark" }, { meaning: "close", shape: "x-mark" }, { meaning: "add to list", shape: "plus" }, { meaning: "new record", shape: "plus-circle" }, { meaning: "error", shape: "exclamation-circle" }, { meaning: "warning", shape: "exclamation-triangle" }, { meaning: "info", shape: "i-circle" }, { meaning: "success", shape: "check-circle" }, ]; // 1) Does the same shape carry more than one meaning? const byShape = {}; for (const s of SET) (byShape[s.shape] ??= []).push(s.meaning); console.log("shape meanings status"); for (const [b, meanings] of Object.entries(byShape)) { console.log( `${b.padEnd(20)} ${meanings.join(", ").padEnd(31)} ${meanings.length > 1 ? "POLYSEMOUS" : "singular"}` ); } // 2) Are close meanings drawn with different shapes? const CLOSE_PAIRS = [["remove", "delete"], ["borrow", "download"], ["add to list", "new record"]]; console.log("\nclose-meaning pair shapes status"); for (const [a, b] of CLOSE_PAIRS) { const ba = SET.find((s) => s.meaning === a).shape; const bb = SET.find((s) => s.meaning === b).shape; console.log( `${(a + " / " + b).padEnd(27)} ${(ba + " / " + bb).padEnd(32)} ${ba === bb ? "NOT SEPARATED" : "separated"}` ); } // 3) Scale: as the box grows, does proportionally scaling stroke thickness preserve visual weight? const BASE_BOX = 24, BASE_STROKE = 1.5; console.log("\nbox proportional stroke rounded stroke/box deviation from base ratio"); for (const box of [14, 16, 18, 20, 24, 28, 32]) { const proportional = (BASE_STROKE * box) / BASE_BOX; const rounded = Math.round(proportional * 2) / 2; // round to half-pixel steps const ratio = rounded / box; const base = BASE_STROKE / BASE_BOX; console.log( `${String(box).padStart(4)} ${proportional.toFixed(3).padStart(19)} ${rounded.toFixed(2).padStart(8)} ${ratio.toFixed(4).padStart(11)} ${((ratio / base - 1) * 100).toFixed(2).padStart(25)}%` ); } // 4) Icon button touch target: 24 px icon, 44 px target const ICON = 24, TARGET = 44; console.log(`\nicon ${ICON} px, touch target ${TARGET} px -> padding per side: ${(TARGET - ICON) / 2} px`); console.log(`ratio of icon area to target: ${((ICON * ICON) / (TARGET * TARGET) * 100).toFixed(1)}%`); // 5) Icon stroke separation from the surface: non-text contrast threshold 3.0 function channel(v) { const s = v / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); } const luminance = ([r, g, b]) => 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b); function contrast(a, b) { const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05); } const decode = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)); console.log("\nicon color vs surface threshold 3.0"); for (const hex of ["#b7bcc2", "#969da6", "#757e8a", "#5e656e"]) { const k = contrast(decode(hex), [255, 255, 255]); console.log(`${hex} ${k.toFixed(2).padStart(8)}:1 ${k >= 3 ? "passed" : "FAILED"}`); }
shape meanings status magnifying-glass search singular funnel filter singular down-arrow-box borrow, download POLYSEMOUS trash-can remove singular x-mark delete, close POLYSEMOUS plus add to list singular plus-circle new record singular exclamation-circle error singular exclamation-triangle warning singular i-circle info singular check-circle success singular close-meaning pair shapes status remove / delete trash-can / x-mark separated borrow / download down-arrow-box / down-arrow-box NOT SEPARATED add to list / new record plus / plus-circle separated box proportional stroke rounded stroke/box deviation from base ratio 14 0.875 1.00 0.0714 14.29% 16 1.000 1.00 0.0625 0.00% 18 1.125 1.00 0.0556 -11.11% 20 1.250 1.50 0.0750 20.00% 24 1.500 1.50 0.0625 0.00% 28 1.750 2.00 0.0714 14.29% 32 2.000 2.00 0.0625 0.00% icon 24 px, touch target 44 px -> padding per side: 10 px ratio of icon area to target: 29.8% icon color vs surface threshold 3.0 #b7bcc2 1.91:1 FAILED #969da6 2.74:1 FAILED #757e8a 4.11:1 passed #5e656e 5.89:1 passed
The first table finds two cases of polysemy. The “x-mark” shape carries both the delete and close meanings; these two are diametrically opposed actions in terms of reversibility. The second collision is more serious: the “down-arrow-box” shape represents both borrowing and downloading.
The second table separates out which collision is actually a problem. Remove and delete are close meanings drawn with different shapes — no problem. Borrow and download are both close in meaning and identical in shape; the user cannot tell from the icon which button downloads the record’s details and which one borrows the book.
Polysemy is not always a flaw. The x-mark’s two meanings are separated by the context in which they appear: the close x-mark sits at the corner of a panel, the delete x-mark next to a list item. If context produces the separation, the collision is acceptable. Borrow and download sit side by side in the same row, so context does not produce separation there; one of the two shapes has to change.
The third table measures the scale problem. When the stroke thickness is scaled proportionally with box size and rounded to the half pixel, the stroke-to-box ratio deviates from the base ratio by 11 to 20 percent. The direction of the deviation is also irregular: the stroke thins at 18 pixels and thickens at 20 pixels. When two icons from the same set are placed side by side, one looks heavier than the other.
The solution is the same as the one from the typographic scale: no intermediate size is generated. The icon set is drawn only at the sizes where the deviation is zero — here, 16, 24, and 32 — and when an in-between size is needed, the nearest step is chosen, not a scaled value.
The final two blocks give two limits. A twenty-four-pixel icon covers only 29.8 percent of a forty-four-pixel touch target; the rest is invisible padding, a reminder that the clickable area has to be larger than the icon. The color of the icon’s stroke, in turn, is bound to the non-text contrast threshold: the neutral 300 and 400 steps do not clear the threshold, so the icon can be drawn no lighter than neutral 500.
An Icon Does Not Replace Text
A common design choice, made to save space, is to remove text labels and leave only the icon. This choice delegates a piece of information to a learned convention, and whether that convention exists varies from icon to icon.
The link between the magnifying glass and search is established. The link between “down-arrow-box” and borrowing, by contrast, is not established; that link is this interface’s own invention, and the user can only learn it by using it. Using an unestablished link without text forces the user into trial and error.
The criterion can be stated as: an icon can be used without text only if the meaning it carries is established outside this interface too. If it is not established, text stands next to the icon; the icon then moves into the decorative class and is hidden.
In two cases the text can be removed. First, when the action is reversible and the cost of a wrong click is low. Second, when the same action is also offered with text somewhere else in the interface; the icon is then a shortcut, not the only path.
Summary
- Icons split into semantic and decorative; the test is whether information disappears when the icon is removed.
- A semantic icon has to have an accessible name; a decorative icon has to be hidden, or the same information gets announced twice.
- The accessible name is computed by a defined order; leaving the name to the
titleattribute is a weak solution. - An icon set is checked for polysemy; a collision is acceptable only if context produces the separation.
- Stroke thickness does not stay proportional at every size; a set is drawn only at the sizes where the deviation is zero, and no intermediate size is generated.
- An icon can be used without text only if the meaning it carries is established outside the interface.
Next Step
This 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 come up in every lesson so far but has never been treated as a design decision in its own right. The next lesson treats interface text as design itself: it shows, with computation, how action labels are chosen, which three parts an error message consists of, and how the length of text constrains the layout.
To keep your progress and take notes, Log in
My notes
Log in to take notes.