Skip to content
academia.sh

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 title attribute 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.

Start typing to search.

↑↓ Esc navigate · open · close