Skip to content
academia.sh

Lesson 11 / 22

Typeface Selection

The distinction between legibility and readability, what each typeface class promises, computing apparent size from x-height, and auditing a typeface candidate for alphabet coverage and character distinguishability.

Contents

The previous lesson completed the layout skeleton: columns, gutters, and grid margins took their place according to screen width, the minimum size of touch targets was set, and pointer and touch were shown to expect different things from the same interface. The skeleton stands; the text that will fill it has not been chosen yet. Nearly all the weight the catalog interface carries is text: record titles, author names, shelf codes, ISBN numbers, dates, the label on the borrow button.

This lesson answers a single question: by what criteria, rather than taste, is a typeface chosen? The criteria fall into four groups, and three of them are computable — apparent size, alphabet coverage, and character distinguishability. The fourth, the hierarchy load a family can carry, is a direct consequence of the two-channel rule from the first lesson.

Legibility and Readability Are Separate Things

The two criteria are often called by a single word and confused with each other.

Legibility is the degree to which a single character can be distinguished from its neighbors. Whether the letter I in a shelf code is distinguishable from the digit 1 is a legibility question. It is measured at the character level and is a property of the typeface itself.

Readability is the degree to which a block of text can be read fluently. Line length, leading, alignment, and contrast ratio all factor into this criterion. It is measured at the block level and is a property not of the typeface but of the design.

The practical consequence of the distinction is this: a legibility problem is solved by changing the typeface, a readability problem by changing the layout. In the catalog interface, a shelf code being misread falls into the first class; a search results list that tires the eye falls into the second. Neither improves with the other’s solution.

The catalog interface has two separate masses of text, and their criteria differ. The results list and the metadata fields consist of short, scanned strings that resemble each other; there, legibility is decisive. The summary paragraph in the record detail, by contrast, is read continuously; there, readability is decisive.

Classes and What They Promise

Typefaces are grouped into classes by the formal features they carry. A class makes it possible to talk about a typeface without naming a product, and more importantly, it says what each class promises.

  • Serif. Carries finishing strokes at the letter ends. These strokes produce a horizontal flow cue that helps in long continuous text. At small sizes the finishing strokes thin out and can disappear at low resolution.
  • Sans-serif. Has no finishing strokes. It stays steadier at small sizes and in interface labels; because letterforms are simplified, the distinction between similar-looking characters can weaken.
  • Monospace. Every character occupies the same advance width. Wherever column alignment is needed — shelf code, ISBN, date — it gives alignment for free. In continuous text it slows reading down because word shapes are distorted.
  • Variable font. Carries a continuous weight axis in a single file; a separate file for each weight is not needed. What it provides is not a new form but a cheaper weight channel.

On the CSS side, these classes correspond to generic family keywords: serif, sans-serif, monospace. These were introduced in the Visual Presentation with CSS course as the last item in a family list; on the design side, they are a direct class decision.

The catalog interface’s decision follows from this distinction: a single sans-serif family for body text and headings, a monospace family for the shelf code and ISBN fields. Two families, because there are two different jobs; a third family cannot be justified, because no third job can be named.

Nominal Size Is Not Apparent Size

Two typefaces set at the same size do not look the same size. Nominal size is the measure of the em box; what the eye perceives as size is the body height of the lowercase letters, that is, the x-height value. A font file declares this measure in its own metadata in em units, so it can be computed.

// xheight.mjs — computing apparent size from a font's declared metrics

// Every font file declares its metrics in em units (here em = 1000 units).
// Below are the metrics declared by three candidate families.
const candidates = [
  { name: "candidate-1", xHeight: 480, capHeight: 700, ascent: 750, descent: 250 },
  { name: "candidate-2", xHeight: 520, capHeight: 715, ascent: 800, descent: 230 },
  { name: "candidate-3", xHeight: 548, capHeight: 730, ascent: 776, descent: 275 },
];

const EM = 1000;
const BASE_PX = 16; // nominal size chosen for body text

console.log("candidate     x/em    x-height  cap height  x/cap height");
for (const c of candidates) {
  const xRatio = c.xHeight / EM;
  const xPx = xRatio * BASE_PX;
  const capHeightPx = (c.capHeight / EM) * BASE_PX;
  console.log(
    `${c.name.padEnd(13)} ${xRatio.toFixed(3)}   ${xPx.toFixed(2).padStart(9)} px ${capHeightPx.toFixed(2).padStart(8)} px  ${(c.xHeight / c.capHeight).toFixed(3)}`
  );
}

// Nominal size required to hit the same apparent size.
// The target is the x-height candidate-3 produces at 16 pixels.
const targetX = (548 / EM) * BASE_PX;
console.log(`\ntarget x-height: ${targetX.toFixed(2)} px (candidate-3, 16px)`);
console.log("candidate     equivalent nominal size   diff");
for (const c of candidates) {
  const required = targetX / (c.xHeight / EM);
  const diff = required - BASE_PX;
  console.log(
    `${c.name.padEnd(13)} ${required.toFixed(2).padStart(18)} px ${(diff >= 0 ? "+" : "") + diff.toFixed(2)} px`
  );
}

// Vertical measure carried by the line box: ascent + descent.
console.log("\ncandidate     ascent+descent (em)   at equivalent size (px)");
for (const c of candidates) {
  const total = (c.ascent + c.descent) / EM;
  const required = targetX / (c.xHeight / EM);
  console.log(
    `${c.name.padEnd(13)} ${total.toFixed(3).padStart(16)} ${(total * required).toFixed(2).padStart(21)}`
  );
}
candidate     x/em    x-height  cap height  x/cap height
candidate-1   0.480        7.68 px    11.20 px  0.686
candidate-2   0.520        8.32 px    11.44 px  0.727
candidate-3   0.548        8.77 px    11.68 px  0.751

target x-height: 8.77 px (candidate-3, 16px)
candidate     equivalent nominal size   diff
candidate-1                18.27 px +2.27 px
candidate-2                16.86 px +0.86 px
candidate-3                16.00 px +0.00 px

candidate     ascent+descent (em)   at equivalent size (px)
candidate-1              1.000                 18.27
candidate-2              1.030                 17.37
candidate-3              1.051                 16.82

The output’s second block shows that “16 pixels” says nothing by itself. To hit the same apparent size, candidate-1 has to go up to 18.27 pixels; the difference is 2.27 pixels, that is, fourteen percent of the base size. This is why swapping a typeface requires the whole size scale to be readjusted. The scale is the subject of the next lesson, and its input is this computation.

The third block gives a second result. The sum of ascent and descent sets the base height of the line box. Because candidate-3 operates at a smaller nominal size for the same apparent size, its line box comes out shorter too: 16.82 pixels against 18.27. Of two text blocks set at the same leading multiplier, one will sit tighter even though both are at the same apparent size.

The x-height ratioed to cap height (the x/cap height column) gives a second signal. As this ratio rises, lowercase letters approach the size of capitals; the leading bump of words that start with a capital weakens. Where word shape speeds up scanning — record titles in search results — this is the cost of too high a ratio.

Distinguishing Characters From Each Other

Legibility has two auditable faces: whether the needed characters exist in the typeface, and whether the ones that exist are not confused with each other. Both are tested against the text that actually appears in the interface, not against a sample sentence.

// coverage.mjs — auditing catalog text for typeface coverage and distinguishability

// Fields that actually appear in the catalog interface.
const records = [
  { title: "Işık ve Gölge: Bir Optik Tarihi", author: "Öztürk, Şükran", year: 1998, shelfCode: "QC353.I8", isbn: "9789750718533" },
  { title: "Çağdaş Türk Şiirinde İmge", author: "Ilgaz, İbrahim", year: 2004, shelfCode: "PL248.C1", isbn: "9789944882101" },
  { title: "Ağ Protokolleri El Kitabı", author: "Gündoğdu, Elif", year: 2011, shelfCode: "TK5105.5", isbn: "9786055829476" },
];

// 1) Coverage check: the code-point ranges a candidate font declares.
const ranges = {
  "latin-basic-only": [[0x0020, 0x007e]],
  "latin-1-supplement": [[0x0020, 0x007e], [0x00a0, 0x00ff]],
  "latin-extended-a": [[0x0020, 0x007e], [0x00a0, 0x00ff], [0x0100, 0x017f], [0x2010, 0x2027]],
};

function covers(ranges, code) {
  return ranges.some(([low, high]) => code >= low && code <= high);
}

const text = records.map((r) => `${r.title} ${r.author} ${r.year} ${r.shelfCode} ${r.isbn}`).join(" ");
const used = [...new Set([...text])].sort((a, b) => a.codePointAt(0) - b.codePointAt(0));

console.log(`distinct characters across catalog text: ${used.length}`);
for (const [name, r] of Object.entries(ranges)) {
  const missing = used.filter((c) => !covers(r, c.codePointAt(0)));
  const written = missing
    .map((c) => `${c}(U+${c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")})`)
    .join(" ");
  console.log(`${name.padEnd(24)} missing ${String(missing.length).padStart(2)}: ${written || "-"}`);
}

// 2) Distinguishability: character sets that can be confused with each other.
const confusable = [
  ["I", "l", "1", "|"],
  ["0", "O", "o"],
  ["5", "S", "s"],
  ["8", "B"],
  ["I", "İ", "i", "ı"],
];

console.log("\nfield      confusable set members present together");
const fields = [
  ["shelfCode", records.map((r) => r.shelfCode)],
  ["isbn", records.map((r) => String(r.isbn))],
  ["author", records.map((r) => r.author)],
];
for (const [fieldName, values] of fields) {
  const letters = new Set(values.join("").split(""));
  const overlapping = confusable
    .map((set) => set.filter((c) => letters.has(c)))
    .filter((v) => v.length >= 2)
    .map((v) => v.join("/"));
  console.log(`${fieldName.padEnd(10)} ${overlapping.length ? overlapping.join("  ") : "-"}`);
}
distinct characters across catalog text: 54
latin-basic-only         missing  9: Ç(U+00C7) Ö(U+00D6) ö(U+00F6) ü(U+00FC) ğ(U+011F) İ(U+0130) ı(U+0131) Ş(U+015E) ş(U+015F)
latin-1-supplement       missing  5: ğ(U+011F) İ(U+0130) ı(U+0131) Ş(U+015E) ş(U+015F)
latin-extended-a         missing  0: -

field      confusable set members present together
shelfCode  I/1
isbn       -
author     I/l  I/İ/i

The first block shows why a coverage problem cannot be caught by eye. Latin-1 Supplement covers most of Turkish’s letters; the five code points still missing are exactly the dotted and dotless i pair, ğ, and ş in the Latin Extended-A block. A typeface looks flawless for as long as these five letters do not appear in the sample text. Characters that fall back to a substitute family are drawn with a different x-height, and two separate typefaces appear within the same word. Code points were defined in the How Computers Work course; the audit here is a direct application of them.

The second block tells which field carries which confusion. In the shelf code field, I and 1 occur together; this field is used by being read aloud or copied by hand in a library, so the cost of confusion is concrete. The ISBN field carries only digits, so it does not collide with any set in the list. The author field, however, has a confusion specific to Turkish: capital I, lowercase l, and dotted İ occur in the same field.

This finding has two separate solutions, and they touch different places. Giving the shelf code field a monospace family separates letters and digits into two distinct drawing traditions — this is a typeface decision. The I/İ confusion in the author field is not solved by the typeface, because both are genuinely distinct letters that are needed; what is needed there is to check which form — serifed base or hooked — the candidate draws its capital I with.

The Hierarchy Load a Family Can Carry

The Visual Hierarchy lesson established as a rule that critical distinctions are expressed in at least two channels. One of the channels was weight. The number of weights a typeface family offers directly limits how many places this rule can be applied.

The catalog interface needs at least three weight levels: bold for the record title, regular for body text, and regular again but at lower contrast for suppressed metadata. A family that offers two weights covers this. A family that offers a single weight does not; in that case the entire load of the hierarchy falls on the size and contrast channels, producing the one-legged distinction case computed in the first lesson.

Figure style is a second constraint. A family’s digits can be drawn as proportional or tabular figures. In proportional figures, the 1 sits narrower than the others; in publication years stacked one under another, the column drifts. If the catalog results list aligns years in a column, tabular figures are required. A monospace family already provides this; a sans-serif family needs it declared as a separate feature.

The third constraint is whether a genuine italic cut exists. If the family has no italic cut, the browser produces an imitation by slanting the roman letters; this imitation distorts letter ends and reduces legibility at small sizes. In the catalog interface, italics are needed in only one place — the citation in the record detail — and even that single place constrains the family choice.

When these three constraints come together, the selection criterion simplifies: does the family actually offer the channels the interface needs? If it does not, the missing channel’s load transfers to other channels, and those channels are already full.

Summary

  • Legibility is measured at the character level, readability at the block level; the first is solved by a typeface change, the second by a layout change.
  • Typeface classes make it possible to decide without naming a product; each class makes one job cheaper and another more expensive.
  • Nominal size does not give apparent size; apparent size is computed from the x-height ratio, and hitting the same measure requires a different nominal size in different families.
  • Alphabet coverage is audited against the code points that actually occur in the interface; missing coverage is invisible in the sample text but shows itself through characters that fall back to a substitute family.
  • Character confusion is audited field by field; which confusable set actually co-occurs in which field is a computable question.
  • The number of weights a family offers, its digit width, and whether it has a genuine italic cut directly limit how many channels the hierarchy can be built on.

Next Step

This lesson fixed a single measure — the base size of body text — and showed that this measure changes across families. But the interface does not have a single size: page title, record title, author name, metadata, and button text sit at separate steps. If these steps are each chosen individually, the crowd of numbers grows, and the distinction thresholds measured in the first lesson are hit by chance or missed entirely. The next lesson derives the steps from a base and a ratio, rounds the resulting scale to the pixel grid, and computes whether neighboring steps actually clear the distinction threshold.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close