Skip to content
academia.sh

Lesson 04 / 22

Balance and Alignment

Defining alignment as a shared invisible axis; computing the number of alignment axes and area-weighted horizontal balance, and measuring optical alignment and the ragged right edge.

Contents

Up to this point, every decision was made through individual elements: how large this element is, how close these two are, how heavy this button is. What holds a screen together, though, is not the elements themselves but the invisible lines between them. If the filter panel’s left edge and the page title’s left edge line up on the catalog page, a relationship is established even though no line exists between them.

This lesson ties three questions to a number: how many alignment axes a layout has, how weight is distributed along the horizontal axis of the screen, and how irregular a text block’s right edge is.

Alignment Is a Shared Axis

Alignment is multiple elements sharing the same invisible line. That line can be the elements’ left edge, right edge, vertical center axis, or text baseline. The term also appeared in the How Computers Work course, in the sense of memory addresses landing on a word boundary; here it refers to a visual axis, and the only thing the two uses share is the idea of “landing on a common boundary.”

Alignment’s function is to establish a relationship. The proximity principle spends space to bind elements together; alignment does the same job between distant elements while spending nothing. If the page title at the very top of the screen and a footnote at the very bottom sit on the same left axis, they read as belonging to the same column even with 500 pixels between them.

In exchange, alignment produces a cost: every new axis is one more rule. Every distinct left edge on screen is one more starting point the reader has to learn. The axis count is therefore a directly measurable property of a layout.

Axis Count and Horizontal Balance

The program below takes the blocks on the catalog results page, counts how many distinct left and right axes are used, and computes the layout’s area-weighted horizontal center. The second measure gives balance: each block’s area is treated as weight, the blocks’ shared center of weight is found, and its deviation from the container’s center axis is measured.

// 04-alignment.mjs — alignment axis count and area-weighted horizontal balance

const CONTAINER = 1200;

// Blocks on the catalog results page: (name, left edge, width, height)
const before = [
  { name: "page title", x: 24, w: 600, h: 40 },
  { name: "search field", x: 32, w: 520, h: 44 },
  { name: "filter panel", x: 24, w: 264, h: 480 },
  { name: "filter heading", x: 40, w: 232, h: 24 },
  { name: "results list", x: 312, w: 600, h: 480 },
  { name: "record title", x: 328, w: 560, h: 24 },
  { name: "record metadata", x: 336, w: 540, h: 18 },
];

const after = [
  { name: "page title", x: 24, w: 1152, h: 40 },
  { name: "search field", x: 24, w: 1152, h: 44 },
  { name: "filter panel", x: 24, w: 264, h: 480 },
  { name: "filter heading", x: 24, w: 264, h: 24 },
  { name: "results list", x: 312, w: 864, h: 480 },
  { name: "record title", x: 312, w: 864, h: 24 },
  { name: "record metadata", x: 312, w: 864, h: 18 },
];

function axes(blocks) {
  const left = new Set(blocks.map((b) => b.x));
  const right = new Set(blocks.map((b) => b.x + b.w));
  return { left: [...left].sort((a, b) => a - b), right: [...right].sort((a, b) => a - b) };
}

function balance(blocks) {
  const totalArea = blocks.reduce((t, b) => t + b.w * b.h, 0);
  const moment = blocks.reduce((t, b) => t + b.w * b.h * (b.x + b.w / 2), 0);
  const center = moment / totalArea;
  const deviation = center - CONTAINER / 2;
  return { center, deviation, ratio: (100 * Math.abs(deviation)) / (CONTAINER / 2) };
}

function report(name, blocks) {
  const a = axes(blocks);
  const b = balance(blocks);
  console.log(name);
  console.log(`  left axes (${a.left.length}): ${a.left.join(", ")}`);
  console.log(`  right axes (${a.right.length}): ${a.right.join(", ")}`);
  console.log(`  total axis count: ${a.left.length + a.right.length}`);
  console.log(
    `  area-weighted center: ${b.center.toFixed(1)}px   container center: ${CONTAINER / 2}px`
  );
  console.log(`  deviation: ${b.deviation.toFixed(1)}px   ${b.ratio.toFixed(1)}% of half-width`);
  console.log("");
}

report("before", before);
report("after", after);
before
  left axes (6): 24, 32, 40, 312, 328, 336
  right axes (7): 272, 288, 552, 624, 876, 888, 912
  total axis count: 13
  area-weighted center: 459.7px   container center: 600px
  deviation: -140.3px   23.4% of half-width

after
  left axes (2): 24, 312
  right axes (2): 288, 1176
  total axis count: 4
  area-weighted center: 608.6px   container center: 600px
  deviation: 8.6px   1.4% of half-width

The “before” layout looks flawless when examined element by element: every block sits neatly inside its own container, the internal spacing is reasonable, nothing overflows. The number says something different. Seven blocks use thirteen distinct axes; the left edges start at 24, 32, 40, 312, 328, and 336 pixels. None of these values is wrong on its own; the problem is that all of them exist at the same time. An 8-pixel difference is not perceived as alignment, and it is not perceived as deliberate indentation either; it just blurs the edge.

The “after” layout carries the same information on four axes. The record title now aligns with the results list’s left axis rather than with its own container’s internal spacing. The internal spacing has not disappeared; it moved outside the list, between the blocks.

The second measure also shows another flaw in the first layout. The area-weighted center sits at 459.7 pixels, 140.3 pixels left of the container’s center axis. There is a 288-pixel-wide strip of empty space on the right, and that strip serves no function. When the results list is widened to the container’s right edge, the deviation drops to 8.6 pixels.

Zero Deviation Is Not the Goal

The balance measure’s purpose is not to impose symmetry. A symmetric layout is balanced, but not every balanced layout is symmetric: an asymmetric layout with a narrow, tall panel on the left and a wide, short block on the right can also produce a deviation close to zero.

The measure’s function is to catch unintended imbalance. A 23 percent deviation is usually not a decision but a leftover: content was not extended to the right, the container’s width changed, but the blocks were never updated. The criterion used throughout this course is: a deviation exceeding 10 percent of the half-width requires a justification. If a justification exists, the deviation stays — a reading column may be deliberately pushed left, or the space on the right may be deliberately reserved. If no justification exists, it is corrected.

Optical Alignment

Mathematical alignment does not always produce perceived alignment. Shapes with edges that are not straight look misaligned when their boxes are aligned. Correcting this difference is called optical alignment.

There are three common cases. The first is round shapes: a circle looks smaller when it sits in the same box as a square, because its edge touches the box at only four points. The second is punctuation: a line beginning with a quotation mark looks indented by one step, because of the open space inside the mark. The third is triangular shapes: a triangle pointing right looks shifted left when centered exactly in its box, because its mass is concentrated at its base.

What these three cases share is that the fix is not immeasurable but uncomputable: the difference is real, but it is specific to the shape and has no general formula. The applicable rule is this: an optical correction is applied only to shapes with edges that are not straight, only at large sizes, and only with the amount of the shift documented. Undocumented optical corrections are the most common source of the consistency problem taken up in the next lesson: nobody knows why that 2 pixels is there, and nobody dares to remove it.

The Right Edge of Text

A text block’s left edge is an axis; its right edge is an irregular profile produced by the line lengths. This profile is called the ragged right edge, and the degree of its irregularity can be measured.

The program below wraps a paragraph at three different line lengths, extracts the distribution of the line lengths, and computes how far the word gaps would stretch if justification were applied. The measurement uses a fixed-width assumption: each character is taken as 8 pixels, normal word spacing as 4 pixels.

// 04-ragged-right.mjs — depth of the ragged right edge and the spacing cost of justification

const text =
  "A catalog record's detail view brings together the book's title, the author's " +
  "name, the publication year, the shelf code, and whether the book is currently " +
  "on the shelf, all within one single screen. A reader arrives at this screen " +
  "asking two separate questions: is this the exact book being sought, and can " +
  "it be borrowed right now today. The layout of the view answers both apart.";

// Fixed-width measurement: every character is assumed to have equal width.
function wrap(text, measure) {
  const words = text.split(" ");
  const lines = [];
  let current = "";
  for (const w of words) {
    const candidate = current ? `${current} ${w}` : w;
    if (candidate.length <= measure) current = candidate;
    else {
      lines.push(current);
      current = w;
    }
  }
  if (current) lines.push(current);
  return lines;
}

const CHARACTER_WIDTH = 8; // average character width in 16px body text
const NORMAL_SPACE = 4; // 0.25em word spacing

function measureColumn(measure) {
  const lines = wrap(text, measure);
  const bodyLines = lines.slice(0, -1); // the last line is not part of the rag calculation
  const lengths = bodyLines.map((s) => s.length);
  const average = lengths.reduce((t, u) => t + u, 0) / lengths.length;
  const deviation = Math.sqrt(
    lengths.reduce((t, u) => t + (u - average) ** 2, 0) / lengths.length
  );
  const shortest = Math.min(...lengths);
  const ragDepth = measure - shortest;

  // Justification: the missing width is distributed across the word gaps.
  const stretches = bodyLines.map((s) => {
    const gaps = s.split(" ").length - 1;
    const shortfall = (measure - s.length) * CHARACTER_WIDTH;
    return gaps > 0 ? shortfall / gaps : 0;
  });
  const maxStretch = Math.max(...stretches);

  console.log(`measure ${measure} characters  (${measure * CHARACTER_WIDTH}px)`);
  console.log(`  line count: ${lines.length}  (in rag calculation: ${bodyLines.length})`);
  console.log(`  line lengths: ${lengths.join(", ")}`);
  console.log(`  average ${average.toFixed(1)}  standard deviation ${deviation.toFixed(2)}`);
  console.log(`  rag depth: ${ragDepth} characters  (${((100 * ragDepth) / measure).toFixed(1)}% of the measure)`);
  console.log(
    `  largest word gap when justified: ${(NORMAL_SPACE + maxStretch).toFixed(1)}px  ` +
      `(${((NORMAL_SPACE + maxStretch) / NORMAL_SPACE).toFixed(2)} times normal)`
  );
  console.log("");
}

measureColumn(45);
measureColumn(68);
measureColumn(92);
measure 45 characters  (360px)
  line count: 10  (in rag calculation: 9)
  line lengths: 37, 45, 41, 43, 38, 42, 39, 40, 42
  average 40.8  standard deviation 2.39
  rag depth: 8 characters  (17.8% of the measure)
  largest word gap when justified: 16.8px  (4.20 times normal)

measure 68 characters  (544px)
  line count: 6  (in rag calculation: 5)
  line lengths: 68, 68, 63, 68, 68
  average 67.0  standard deviation 2.00
  rag depth: 5 characters  (7.4% of the measure)
  largest word gap when justified: 7.6px  (1.91 times normal)

measure 92 characters  (736px)
  line count: 5  (in rag calculation: 4)
  line lengths: 87, 92, 89, 91
  average 89.8  standard deviation 1.92
  rag depth: 5 characters  (5.4% of the measure)
  largest word gap when justified: 7.1px  (1.77 times normal)

The narrow column’s right edge is deep: one line ends at 37 characters, leaving 17.8 percent of the measure empty. Here the right edge is not an axis but a jagged profile.

Justification promises to flatten this profile, but it pays for that with word spacing. In the narrow column, the widest word gap grows to 4.20 times normal. When word gaps exceed letter spacing, the gaps stack up between lines into vertical channels inside the text, and reading no longer advances along the line but jumps between the gaps.

This ties a design decision to a number: justification is not applied unless the line length is large enough. The measurement brings the stretch down to 1.77 times in the 92-character column; it climbs to 4.20 times in the 45-character column. The description text in the catalog interface’s filter panel lives in narrow columns, so the ragged right edge is kept. The same calculation has a flip side: if the rag depth exceeds a quarter of the measure, the problem is not a lack of justification but a column that is too narrow.

Summary

  • Alignment is an invisible axis shared by elements; it establishes a relationship between distant elements without spending space, and in exchange every new axis loads one more rule onto the reader.
  • Axis count is a directly countable layout property; in the measured layout, seven blocks were brought down from thirteen axes to four, and no information was lost.
  • The area-weighted horizontal center’s deviation from the container’s center measures balance; a deviation exceeding 10 percent of the half-width requires a justification, but zero deviation is not the goal.
  • Optical alignment is needed only for shapes with edges that are not straight, has no formula, and when applied, the amount of the shift is documented.
  • The depth of a ragged right edge can be measured; if the rag depth exceeds a quarter of the line length, the column is too narrow.
  • Justification stretches a narrow column’s word gaps to several times normal — 4.20 times in the case measured here; it is this stretch ratio, not aesthetics, that determines the decision.

Next Step

This lesson measured a single screen’s internal consistency. The real question sits between screens: what is lost when a gap that is 8 pixels on the catalog interface’s search page becomes 10 pixels on the record detail page? The next lesson treats repetition as the source of learnability, extracts an interface’s decision inventory, and counts how many different values are used to do the same job.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close