Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

UI/UX Design  Module 6 – Visual design for interfaces

Visual hierarchy, layout and grids

Make one MealBox action lead a screen with size, weight, colour and space, put gaps on a scale and edges on a column grid, and check it with a blur test.

  • Beginner
  • 25 minutes
  • Examples run with Node.js 24.21.0, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Create a clear hierarchy with size, weight, colour, contrast and space
  • Arrange a screen on a column grid with a spacing scale
  • Check that the order of the code matches the order of the screen
  • Run a blur test and read its ranking

Before you start

What you’ll learn · Before you start

What you will learn

  • Create a clear hierarchy with size, weight, colour, contrast and space
  • Arrange a screen on a column grid with a spacing scale
  • Check that the order of the code matches the order of the screen
  • Run a blur test and read its ranking

Before you start

On this page

MealBox’s plan page, drawn for this lesson, has eight blocks in one 16 px regular style at the same contrast ratio of 10.31:1. Nothing leads: in a blur test of the page, the Choose plan button ranks sixth of eight and the feature list ranks first. All the numbers in this lesson come from layouts invented for it.

visual hierarchy: The order in which a screen asks to be read, set by differences in size, weight, colour, contrast, space and position between its elements. is the order in which a screen asks to be read, and a handful of levers set it. You will rank elements with those levers, put every gap on a spacing scale and every left edge on a column grid, keep the code in the order of the screen, and check the result with a blur test that runs as a program.

Five levers decide what a screen leads with

Nielsen Norman Group names three ways to build a visual hierarchy: colour and contrast, scale, and grouping. This lesson splits them into five levers that a layout can measure: size, weight, colour (a text colour or a fill), contrast against the background, and space around the element. Position adds a sixth. Apple’s layout guidance notes that people start reading from the top and the leading side, so the most important item belongs there.

Lever What it does Watch for
Size Bigger elements are noticed first Many large elements cancel each other
Weight Heavier type reads as more important Bold everywhere is no emphasis
Colour A fill or an accent colour marks one element Colour must not carry meaning alone
Contrast Strong contrast draws the eye Text still needs 4.5:1 at the least
Space Room around an element sets it apart Uneven gaps look accidental

Step through the plan page and add the levers one at a time. The sub-labels show the values the program below prints:

Interactive · step by step Five levers rank a flat MealBox plan page Step through the plan page from the first example. Each step adds one lever to some blocks and the sub-labels show the values the example prints.
Five levers rank a flat MealBox plan page, step 6 of 6. Showing Page title (32 px above), Plan name (24 px above), Price (32 px), Recommended tag (pale fill), Choose plan (3 levers), Compare plans (1 lever), Feature line, Help link (48 px above). In focus: Choose plan, Compare plans.Page title32 px abovePlan name24 px abovePrice32 pxRecommended tagpale fillChoose plan3 leversCompare plans1 leverFeature lineHelp link48 px above

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A flat page ranks nothing. Eight blocks, all 16 px regular in one dark grey at a contrast ratio of 10.31:1. No block differs from the body style by any lever, so the eye has no place to start and the price looks as important as the help link.
  2. Size sets the top of the ranking. The page title and the price go to 32 px and the plan name to 20 px, which makes three sizes in all. The buttons stay at 16 px: size is kept for the content people read first, not for the actions.
  3. Weight separates the two actions. The page title and the price take weight 700 and the plan name 600. The primary action takes 600 and the secondary 500, so both stay readable and the primary one is plainly heavier.
  4. Colour marks one primary action. Only Choose plan gets a filled button, white on blue at 6.70:1. Compare plans is blue text without a fill, also 6.70:1, and the tag has a pale blue fill. Colour here supports the weight and the words, never replaces them.
  5. Space opens each new group. Extra space above the page title, the plan name, the primary button and the help link starts a new group at each of them. The gaps come from a scale, which the next section builds, instead of from guesses.
  6. The primary action leads. Choose plan now differs from the body style by three levers and Compare plans by one. Five blocks differ by two or more levers, against none before, and the page uses three font sizes.

The program measures the same eight blocks in two versions. A block counts a lever when it differs from the body style: a size at least 1.25 times the body, a weight at least 200 higher, a different colour or a fill, or twice the body’s space above. It also prints each block’s contrast ratio, worked out with the WCAG formula.

Levers on a flat and a restyled plan page Python · hierarchy_levels/hierarchy_levels.py
# MealBox plan page, drawn for this lesson: two versions of the same eight elements (invented values).
# A "lever" is a way an element differs from the body style: size, weight, colour or fill, space above.

def lum(h):
    c = [int(h[i:i + 2], 16) / 255 for i in (1, 3, 5)]
    c = [v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in c]
    return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]

def ratio(a, b):
    hi, lo = sorted((lum(a), lum(b)), reverse=True)
    return (hi + 0.05) / (lo + 0.05)

BODY = (16, 400, "#374151", None, 12)          # size px, weight, text colour, fill, space above px
# name: (size, weight, text colour, fill, space above)
FLAT = {
    "Page title": (16, 400, "#374151", None, 12), "Plan name": (16, 400, "#374151", None, 12),
    "Price": (16, 400, "#374151", None, 12), "Recommended tag": (16, 400, "#374151", None, 12),
    "Choose plan (primary)": (16, 400, "#374151", None, 12), "Compare plans (secondary)": (16, 400, "#374151", None, 12),
    "Feature line": (16, 400, "#374151", None, 12), "Help link": (16, 400, "#374151", None, 12),
}
RESTYLED = dict(FLAT)
RESTYLED.update({
    "Page title": (32, 700, "#111827", None, 32), "Plan name": (20, 600, "#111827", None, 24),
    "Price": (32, 700, "#1D4ED8", None, 12), "Recommended tag": (16, 600, "#1E3A8A", "#DBEAFE", 12),
    "Choose plan (primary)": (16, 600, "#FFFFFF", "#1D4ED8", 24),
    "Compare plans (secondary)": (16, 500, "#1D4ED8", None, 12), "Help link": (16, 400, "#374151", None, 48),
})

def levers(style):
    size, weight, colour, fill, space = style
    found = []
    if size >= BODY[0] * 1.25: found.append("size")
    if weight >= BODY[1] + 200: found.append("weight")
    if fill or colour != BODY[2]: found.append("colour")
    if space >= BODY[4] * 2: found.append("space")
    return found

def report(title, page):
    print(title)
    print(f"{'element':27}{'size':>5}{'wt':>5}{'contrast':>10}  levers")
    for name, s in page.items():
        c = ratio(s[2], s[3] or "#FFFFFF")
        print(f"{name:27}{s[0]:>5}{s[1]:>5}{c:>9.2f}:1  {', '.join(levers(s)) or '-'}")
    sizes = sorted({s[0] for s in page.values()})
    strong = [n for n, s in page.items() if len(levers(s)) >= 2]
    print(f"font sizes in use: {sizes}; elements with 2+ levers: {len(strong)}\n")

report("Flat page", FLAT)
report("Restyled page", RESTYLED)
primary = len(levers(RESTYLED["Choose plan (primary)"]))
secondary = len(levers(RESTYLED["Compare plans (secondary)"]))
print(f"primary action levers: {primary}, secondary: {secondary}")

Output

Flat page
element                     size   wt  contrast  levers
Page title                    16  400    10.31:1  -
Plan name                     16  400    10.31:1  -
Price                         16  400    10.31:1  -
Recommended tag               16  400    10.31:1  -
Choose plan (primary)         16  400    10.31:1  -
Compare plans (secondary)     16  400    10.31:1  -
Feature line                  16  400    10.31:1  -
Help link                     16  400    10.31:1  -
font sizes in use: [16]; elements with 2+ levers: 0

Restyled page
element                     size   wt  contrast  levers
Page title                    32  700    17.74:1  size, weight, colour, space
Plan name                     20  600    17.74:1  size, weight, colour, space
Price                         32  700     6.70:1  size, weight, colour
Recommended tag               16  600     8.49:1  weight, colour
Choose plan (primary)         16  600     6.70:1  weight, colour, space
Compare plans (secondary)     16  500     6.70:1  colour
Feature line                  16  400    10.31:1  -
Help link                     16  400    10.31:1  space
font sizes in use: [16, 20, 32]; elements with 2+ levers: 5

primary action levers: 3, secondary: 1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hierarchy_levels.py

The flat page has no block with any lever and one font size. The restyled page has three sizes, 16, 20 and 32 px, which keeps to Nielsen Norman Group’s advice of no more than three sizes. Choose plan differs from the body by three levers and Compare plans by one, so the two actions are plainly ranked while both stay readable at 6.70:1. The counting rule is this lesson’s own, not a published standard; what it shows is a ranking you can read off numbers.

A secondary action nobody can see

Wrong: Compare plans in pale grey 16 px text, 2.54:1 on white, so the second choice looks like decoration and fails the 4.5:1 of WCAG 2.2 success criterion 1.4.3. Right: the same weight of text in the brand blue at 6.70:1, with no fill. The secondary action should look secondary, not missing.

Each lever is cheap to add and easy to overuse, which is why the ranking comes before the decoration.

Key idea

Rank the blocks before you style them: give the one primary action the most levers, give the secondary action fewer, and keep every text colour above 4.5:1.

Checkpoint

Check: The levers of visual hierarchy

  1. Question 1 of 2 A block is set in 20 px weight 600 text, in the same grey as the body (16 px, weight 400), with the usual space above it. Which levers does it use, by the lesson's counting rule?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Size and weight

    The rule counts a size at least 1.25 times the body, a weight at least 200 higher, a different colour or a fill, and twice the body's space above. This block meets the first two and not the other two.

  2. Question 2 of 2 A plan page shows two filled blue buttons of equal size, Choose plan and Compare plans. What does the hierarchy tell a visitor?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: That the two actions are equally important, so nothing leads

    A hierarchy exists only where blocks differ. Making the secondary action an outline or a text style keeps it visible and readable while the filled button leads.

A spacing scale gives every gap a reason

On the flat page the gaps were chosen by eye: 32, 8, 12, 18, 13 and 48 px. A spacing scale: A short list of allowed gaps, for example 4, 8, 12, 16, 24, 32 and 48 px, from which every margin and padding in a design is taken. is a short list of allowed gaps, and every margin and padding is taken from it. This lesson uses 4, 8, 12, 16, 24, 32 and 48 px as its example. GOV.UK’s design system publishes another: its static scale has the units 5, 10, 15, 20, 25, 30, 40, 50 and 60 px, and its responsive scale makes the larger units smaller on screens narrower than its tablet breakpoint of 640 px.

A scale does two jobs. It makes the page consistent, because a gap of 24 px always means the same degree of separation. It also keeps groups readable: in the lesson on perception, spacing alone can show a group when the smallest gap between groups is double the largest gap within one. Guessed gaps break that rule without anyone noticing. The program measures the six gaps of the plan page, snaps each to the nearest scale value and tests the 2x rule before and after.

Six gaps against a spacing scale Python · spacing_audit/spacing_audit.py
# A spacing scale checked against the vertical gaps of a MealBox plan page drawn for this lesson (invented values).
SCALE = [4, 8, 12, 16, 24, 32, 48]          # this lesson's example scale, in px

# gap above each block (px), in page order, with the group it starts or continues
GAPS = [("title to cards", 32, "new group"), ("name to price", 8, "same group"), ("price to features", 12, "same group"),
        ("features to button", 18, "same group"), ("button to link", 13, "same group"), ("cards to help", 48, "new group")]

def snap(gap):
    return min(SCALE, key=lambda s: (abs(s - gap), s))

print("gap                  px  on the scale   snapped")
for name, gap, _ in GAPS:
    print(f"{name:20}{gap:>3}  {'yes' if gap in SCALE else 'no ':12}{snap(gap):>6}")
snapped = [(name, snap(gap), kind) for name, gap, kind in GAPS]

def ratio(gaps):
    within = max(g for _, g, k in gaps if k == "same group")
    between = min(g for _, g, k in gaps if k == "new group")
    return within, between, between / within

print("\ndistinct gap values:", len({g for _, g, _ in GAPS}), "->", len({g for _, g, _ in snapped}), "after snapping")
for label, gaps in (("as drawn", GAPS), ("snapped", snapped)):
    within, between, r = ratio(gaps)
    print(f"{label:9} largest gap inside a group {within}, smallest between groups {between}, ratio {r:.2f}, 2x rule met: {r >= 2}")

Output

gap                  px  on the scale   snapped
title to cards       32  yes             32
name to price         8  yes              8
price to features    12  yes             12
features to button   18  no              16
button to link       13  no              12
cards to help        48  yes             48

distinct gap values: 6 -> 5 after snapping
as drawn  largest gap inside a group 18, smallest between groups 32, ratio 1.78, 2x rule met: False
snapped   largest gap inside a group 16, smallest between groups 32, ratio 2.00, 2x rule met: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 spacing_audit.py

Two gaps were off the scale, 18 and 13 px. As drawn, the largest gap inside a group is 18 px and the smallest between groups is 32 px, a ratio of 1.78, short of 2. Snapped to 16 and 12, the ratio is exactly 2.00 and the groups read from spacing alone. The six values also became five, which is the point of a scale: fewer distinct gaps, each with a job.

The amount of whitespace: The empty space around and between elements. It is a structural part of a layout that separates groups and gives elements room, not leftover area. on the scale depends on the task. A dashboard that an operator scans all day can sit at the lower end of the scale; an onboarding screen can sit at the upper end. Whitespace is a structural part of the layout, not what is left over.

Break the 2x rule on purpose

Press Edit and change the 18 in the “features to button” row to 28. The as-drawn ratio drops to 1.14 and even the snapped one is 1.33, because 28 snaps to 24 and a gap inside a group is now almost as large as the gap between groups.

Checkpoint

Check: A spacing scale and the 2x rule

  1. One question With the scale 4, 8, 12, 16, 24, 32 and 48 px, a layout has gaps of 13 px inside groups and 30 px between groups. After snapping each gap to the nearest scale value, does the 2x rule hold?

    Choose one answer.

    Show the answer: One question

    Answer: Yes, 12 px inside and 32 px between is a ratio of 2.67

    Snapping changes the ratio, here from 30 ÷ 13 = 2.31 to 32 ÷ 12 = 2.67. A scale can raise or lower a ratio, so test the snapped values.

Column grids give every edge a place to align

On the 1024 px version of the page, the Compare link starts 6 px to the right of the plan cards and the Help link 8 px to the left of them. The text still reads, but the page looks untidy: Apple’s guidance says people assume aligned items are related and read indented items as subordinate, so a stray edge says something false.

A column grid: A layout divided into equal columns with gutters between them and a margin on each side, so that every block starts at a column edge. fixes the edges. It divides the width into equal columns separated by gutter: The fixed gap between two neighbouring columns of a grid. Column width is what remains of the width after the margins and the gutters, divided by the number of columns., with a margin at each side, and every block starts at a column edge. The column width follows from the other numbers:

column=width−2×margin−(columns−1)×guttercolumns\text{column} = \frac{\text{width} - 2 \times \text{margin} - (\text{columns} - 1) \times \text{gutter}}{\text{columns}}

At 1024 px with 12 columns, 32 px margins and 24 px gutters, that is (1024 − 64 − 264) ÷ 12 = 58 px. Three plan cards of 4 columns each start at x = 32, 360 and 688 px. The number of columns changes with the width. Android’s documentation sorts windows into five window size class: A named range of window width, such as compact below 600 or medium from 600 to 839, used to choose a layout. It describes the window, not the device, and can change while an app runs. by width, and says they describe the window, not the device, so the class can change while the app runs.

Timeline · 5 events in order Five width classes and the grid this lesson uses Walk through the width classes from narrow to wide and see which grid each one gets in the lesson's example.
  1. Compact

    Under 600 px wide 4 columns

    Phones in portrait and narrow windows. The lesson's grid has 4 columns of 70 px with 16 px margins and gutters at 360 px, and a single column of content is the usual layout.

  2. Medium

    600 to 839 px wide 8 columns

    Tablets in portrait and split windows. The lesson's grid has 8 columns of 74 px with 32 px margins and 16 px gutters at 768 px. Two panes become possible here.

  3. Expanded

    840 to 1199 px wide 12 columns

    Tablets in landscape and small desktop windows. The lesson's grid has 12 columns of 58 px with 32 px margins and 24 px gutters at 1024 px, so three plan cards take 4 columns each.

  4. Large

    1200 to 1599 px wide

    Large tablet displays and desktop windows. This step and the next were added to the Android classes to suit desktop and connected displays, where a layout has even more room to use.

  5. Extra large

    1600 px wide and up

    Desktop displays. Content usually stops growing here: GOV.UK, for instance, sets a maximum page width of 1020 px by default so that lines of text stay short.

The timeline has 5 events, from Compact to Extra large.

The program builds the lesson’s three grids and checks where five blocks begin.

Three grids and five left edges Python · grid_audit/grid_audit.py
# A column grid applied to a MealBox plan page drawn for this lesson (invented values).
# Width classes follow Android's window size classes (in dp, read here as CSS px): compact < 600, medium < 840,
# expanded < 1200, large < 1600, extra-large from 1600.

GRIDS = {360: (4, 16, 16), 768: (8, 32, 16), 1024: (12, 32, 24)}   # width: (columns, margin, gutter)

def width_class(w):
    for limit, name in ((600, "compact"), (840, "medium"), (1200, "expanded"), (1600, "large")):
        if w < limit:
            return name
    return "extra-large"

def column_starts(width):
    cols, margin, gutter = GRIDS[width]
    col = (width - 2 * margin - (cols - 1) * gutter) / cols
    return col, [margin + i * (col + gutter) for i in range(cols)]

print("width  class     cols margin gutter column")
for w, (cols, margin, gutter) in GRIDS.items():
    col, _ = column_starts(w)
    print(f"{w:>5}  {width_class(w):9}{cols:>4}{margin:>7}{gutter:>7}{col:>7.1f}")

# left edges (px) of the page's blocks at width 1024
EDGES = {"Plan card 1": 32, "Plan card 2": 360, "Plan card 3": 688, "Compare link": 366, "Help link": 40}
col, starts = column_starts(1024)
print("\nleft edges at 1024 px (column starts:", ", ".join(f"{s:.0f}" for s in starts[:9]) + ", ...)")
for name, left in EDGES.items():
    nearest = min(starts, key=lambda s: abs(s - left))
    off = abs(nearest - left)
    print(f"{name:13}{left:>5}  {'on the grid' if off <= 1 else f'off by {off:.0f} px (column starts at {nearest:.0f})'}")

Output

width  class     cols margin gutter column
  360  compact     4     16     16   70.0
  768  medium      8     32     16   74.0
 1024  expanded   12     32     24   58.0

left edges at 1024 px (column starts: 32, 114, 196, 278, 360, 442, 524, 606, 688, ...)
Plan card 1     32  on the grid
Plan card 2    360  on the grid
Plan card 3    688  on the grid
Compare link   366  off by 6 px (column starts at 360)
Help link       40  off by 8 px (column starts at 32)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 grid_audit.py

The three cards sit on column starts. The Compare link at 366 px is 6 px past the column that starts at 360, and the Help link at 40 px is 8 px past 32. A tolerance of 1 px is plenty: anything larger is a visible drift. GOV.UK’s grid takes the other route to the same result, with columns of whole fractions, and tells designers to put the main content in a two-thirds column. MySmartCoPilot’s layout grid calculator works out column widths for your own numbers, and the CSS grid generator writes the code for a grid.

Key idea

A column grid is one formula: width, margins, gutters and column count give the column width, and every block starts at a column edge.

Checkpoint

Check: Column grids, margins and gutters

  1. Question 1 of 2 A 4-column grid at 360 px has 16 px margins and 16 px gutters. How wide is one column?

    Type a number.

    A hint

    Subtract the two margins and the three gutters from the width, then divide by 4.

    Show the answer: Question 1 of 2

    Answer: 70 px

    (360 − 2 × 16 − 3 × 16) ÷ 4 = (360 − 32 − 48) ÷ 4 = 280 ÷ 4 = 70 px.

  2. Question 2 of 2 By the width classes in the lesson, which class is a window 768 px wide?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Medium

    Width classes are ranges of the window width: compact below 600, medium from 600 to 839, expanded from 840 to 1199, large from 1200 to 1599 and extra large from 1600.

The code must read in the order of the screen

MealBox wants the recommended Family pack plan to be the first card on screen, so a CSS rule moves it to the front while the code still lists it fourth. Sighted visitors see the recommended plan first. People who tab through the page or use a screen reader meet the cards in the order of the code. WCAG 2.2 criterion 1.3.2 asks for a correct reading sequence to be programmatically available where the order affects meaning, and criterion 2.4.3 asks for focus to move in an order that preserves meaning and operability. The W3C lists making the DOM order match the visual order as a technique for both. In other words, a reading order: The sequence in which content is read by a screen reader and reached by keyboard, taken from the order of the code. It should match the order on screen wherever the order carries meaning. that differs from the screen is a fault to check for.

Interactive · step by step Code order and screen order of the plan cards Two columns list the same five blocks. Step through to see which blocks sit in a different place in the code than on the screen.
Code order and screen order of the plan cards, step 4 of 4. Showing 1 Title, 2 Weekday lunch (now third), 3 Weekend meals (now fourth), 4 Family pack (now second), 5 Help, 1 Title, 2 Family pack, 3 Weekday lunch, 4 Weekend meals, 5 Help.1 Title2 Weekday lunchnow third3 Weekend mealsnow fourth4 Family packnow second5 Help1 Title2 Family pack3 Weekday lunch4 Weekend meals5 Help

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. The code lists the blocks. Keyboard and screen reader users meet the blocks in the order of the code. Here the recommended Family pack is fourth, because the page was built with the cards in the order of a database table.
  2. A style rule moves one card. A CSS order rule puts the Family pack first among the cards, at 1024 px and at 360 px alike. Sighted visitors see the recommended plan before the other two.
  3. Three blocks sit apart. The dashed arrows cross: positions 2, 3 and 4 hold different blocks in the code and on the screen. Tabbing from the title lands on Weekday lunch, the card that comes after the recommended plan on screen.
  4. Move the code, not the style. Put the Family pack second in the code and delete the order rule. The code now lists Title, Family pack, Weekday lunch, Weekend meals and Help, the same order as the screen at both widths, and the example prints no position that differs.

The program lists the five blocks by position on screen, with a rule that blocks within 16 px of each other in height share a row and read from left to right, and compares that with the order of the code at 1024 and 360 px.

Code order against screen order at two widths Python · reading_order/reading_order.py
# Does the order in the code match the order on screen? A MealBox plan page drawn for this lesson (invented positions).
# Each block: (top px, left px). DOM order is the order of the list, which keyboard and screen reader users follow.

WIDE = {"Title": (24, 32), "Family pack": (96, 32), "Weekday lunch": (96, 360), "Weekend meals": (96, 688), "Help": (480, 32)}
NARROW = {"Title": (24, 16), "Family pack": (88, 16), "Weekday lunch": (280, 16), "Weekend meals": (472, 16), "Help": (664, 16)}

def visual_order(layout, band=16):
    # rows read top to bottom; blocks whose tops lie within one band share a row and read left to right
    rows = []
    for name, (top, left) in sorted(layout.items(), key=lambda kv: kv[1]):
        if rows and top - rows[-1][0] < band:
            rows[-1][1].append((left, name))
        else:
            rows.append((top, [(left, name)]))
    return [name for _, row in rows for _, name in sorted(row)]

def check(label, dom):
    print(f"{label}: code order {' > '.join(dom)}")
    for width, layout in (("1024 px", WIDE), ("360 px", NARROW)):
        seen = visual_order(layout)
        apart = [i + 1 for i, name in enumerate(dom) if seen[i] != name]
        print(f"  {width}: screen order {' > '.join(seen)}")
        print(f"          positions that differ: {apart if apart else 'none'}")

# "Family pack" is the recommended plan: a CSS order rule moves it first on screen while it stays third in the code.
check("Before", ["Title", "Weekday lunch", "Weekend meals", "Family pack", "Help"])
check("After ", ["Title", "Family pack", "Weekday lunch", "Weekend meals", "Help"])

Output

Before: code order Title > Weekday lunch > Weekend meals > Family pack > Help
  1024 px: screen order Title > Family pack > Weekday lunch > Weekend meals > Help
          positions that differ: [2, 3, 4]
  360 px: screen order Title > Family pack > Weekday lunch > Weekend meals > Help
          positions that differ: [2, 3, 4]
After : code order Title > Family pack > Weekday lunch > Weekend meals > Help
  1024 px: screen order Title > Family pack > Weekday lunch > Weekend meals > Help
          positions that differ: none
  360 px: screen order Title > Family pack > Weekday lunch > Weekend meals > Help
          positions that differ: none

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 reading_order.py

Before the fix, positions 2, 3 and 4 differ at both widths. After moving the Family pack up in the code and deleting the order rule, no position differs. WCAG is careful about its scope: it asks for a meaningful sequence only where the order carries meaning, and it allows the focus order to differ from the reading order when both preserve meaning. Recommended-plan-first carries meaning here, since it is the plan the page asks people to consider first. Fix the code rather than the style, because the code is the order everyone gets.

Checkpoint

Check: Code order against screen order

  1. One question A CSS rule puts the recommended card first on screen, but it is last in the code. What is the best fix?

    Choose one answer.

    Show the answer: One question

    Answer: Move the card to the position it has on screen in the code, and remove the rule

    Criteria 1.3.2 and 2.4.3 both point to making the code order match the visual order. When the code is right, no extra attribute is needed.

A blur test shows which block the eye reaches first

The lesson on perception introduced the blur test: soften a screenshot, or squint, until text cannot be read, and see which elements still stand out. Nielsen Norman Group’s example blurred a music app’s screen by 5, 10 and 20 px. The two softer blurs confirmed the groups, but at 20 px a single tile in a vivid colour outranked the section that was meant to lead. Content can break a hierarchy that the template got right.

The program turns the test into arithmetic. It draws the plan page on a grid of 8 px cells, one value per cell from the element’s colour and how much of its box is ink, blurs the grid with a box filter, and scores each block by how much darker than the page it stays. The page layouts are invented, and a box blur is a rough stand-in for squinting, so read the ranks, not the decimals.

A blur test on the flat and restyled page Python · blur_test/blur_test.py
# A blur test as arithmetic: draw a 360 x 640 px MealBox plan page on a grid of 8 px cells, blur it, rank the elements.
# Invented layout; a box blur is a rough stand-in for squinting, not a model of vision.
W, H, CELL = 45, 80, 8
BG = 1.0                                     # white page, relative luminance 1

def lum(h):
    c = [int(h[i:i + 2], 16) / 255 for i in (1, 3, 5)]
    c = [v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in c]
    return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]

# name: (x, y, width, height, colour, coverage); coverage = share of the box that is ink (text 0.35, a fill 1.0)
def page(restyled):
    t = "#374151"
    if not restyled:
        return {"Page title": (16, 24, 128, 20, t, .35), "Plan name": (16, 88, 104, 20, t, .35),
                "Price": (16, 120, 56, 20, t, .35), "Recommended tag": (200, 92, 96, 20, t, .35),
                "Feature lines": (16, 168, 328, 56, t, .35), "Choose plan (primary)": (16, 248, 96, 20, t, .35),
                "Compare plans": (16, 312, 112, 20, t, .35), "Help link": (16, 600, 40, 20, t, .35)}
    return {"Page title": (16, 24, 224, 40, "#111827", .5), "Plan name": (16, 88, 128, 24, "#111827", .45),
            "Price": (16, 120, 120, 40, "#1D4ED8", .5), "Recommended tag": (200, 92, 120, 24, "#DBEAFE", 1.0),
            "Feature lines": (16, 168, 328, 56, t, .35), "Choose plan (primary)": (16, 248, 328, 48, "#1D4ED8", 1.0),
            "Compare plans": (16, 312, 112, 24, "#1D4ED8", .4), "Help link": (16, 600, 40, 20, t, .35)}

def blur(grid, r):
    out = [[0.0] * W for _ in range(H)]
    for y in range(H):
        for x in range(W):
            cells = [grid[j][i] for j in range(max(0, y - r), min(H, y + r + 1)) for i in range(max(0, x - r), min(W, x + r + 1))]
            out[y][x] = sum(cells) / len(cells)
    return out

def rank(elements, r):
    grid = [[BG] * W for _ in range(H)]
    for x, y, w, h, colour, cover in elements.values():
        for j in range(y // CELL, (y + h + CELL - 1) // CELL):
            for i in range(x // CELL, (x + w + CELL - 1) // CELL):
                grid[j][i] = BG + cover * (lum(colour) - BG)
    soft = blur(grid, r)
    score = {}
    for name, (x, y, w, h, _, _) in elements.items():
        cells = [soft[j][i] for j in range(y // CELL, (y + h + CELL - 1) // CELL) for i in range(x // CELL, (x + w + CELL - 1) // CELL)]
        score[name] = BG - sum(cells) / len(cells)       # how much darker than the page the element stays
    return sorted(score, key=score.get, reverse=True), score

for label, restyled in (("Flat page", False), ("Restyled page", True)):
    for r in (1, 2):
        order, score = rank(page(restyled), r)
        top = ", ".join(f"{n} {score[n]:.2f}" for n in order[:3])
        print(f"{label}, blur radius {r * CELL} px: {top}; primary action ranks {order.index('Choose plan (primary)') + 1} of {len(order)}")

Output

Flat page, blur radius 8 px: Feature lines 0.30, Page title 0.25, Compare plans 0.25; primary action ranks 6 of 8
Flat page, blur radius 16 px: Feature lines 0.27, Plan name 0.19, Price 0.19; primary action ranks 7 of 8
Restyled page, blur radius 8 px: Choose plan (primary) 0.78, Page title 0.42, Price 0.37; primary action ranks 1 of 8
Restyled page, blur radius 16 px: Choose plan (primary) 0.69, Page title 0.36, Price 0.34; primary action ranks 1 of 8

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 blur_test.py

On the flat page the largest block of text, the feature lines, leads, and the Choose plan button ranks sixth of eight at a blur of 8 px and seventh at 16 px. On the restyled page the filled button ranks first at both radii, at 0.78 and 0.69, ahead of the page title. The result checks the design: the hierarchy built with levers survives a blur.

Weaken the button on purpose

Press Edit and change the colour in the “Choose plan (primary)” entry of the restyled page, the one 328 px wide, from “#1D4ED8” to “#DBEAFE”, a pale fill. The primary action drops to seventh of eight at both blurs, below the title, the price and the plan name. A lever can be spent and lost in a single value.

The test has limits. It ranks what you drew, not what real content will do, and it says nothing about whether the labels are clear, so it sits beside the five-second test and a task-based usability test rather than replacing them.

Checkpoint

Check: Reading a blur test

  1. One question After a blur test, the primary button ranks seventh of eight blocks. What is the most useful next step?

    Choose one answer.

    Show the answer: One question

    Answer: Give the button more levers, such as a darker fill or more weight, and test again

    A rank is a prompt to change the design and look again. Add or strengthen levers on the block that should lead, then repeat the test.

Practise

The plan page below has eight blocks that share one style, gaps off the scale, three different left edges and the recommended plan third in the code. Edit only the numbers to meet five rules, then check them against the sample tests:

Exercise · Easy · Python

Restyle a flat MealBox plan page

A MealBox plan page has eight blocks, all set in the same 16 px regular type, with random gaps, uneven left edges and the recommended plan third in the code. Edit the values of BLOCKS in solution.py so that:

  1. Three sizes at most. The page uses at most 3 different font_size values. The page title and the price are at least 1.5 times the body size (16 px), and the plan name is larger than the body text.
  2. One primary action. "Choose plan" has weight 600 or more and "Compare plans" has a lower weight, so the secondary action looks secondary and is still visible.
  3. Spacing from the scale. Every space_above is a value of SCALE (4, 8, 12, 16, 24, 32 or 48).
  4. One left edge. Every left is within 1 px of every other.
  5. Code order matches screen order. Sorting the blocks by top must give them in ascending dom_index, the position of each block in the page's code (0 comes first). The recommended plan is first on screen, so it comes first in the code.

Change only the numbers. Do not add or remove blocks.

Starter code · solution.py

"""
MealBox plan page: eight blocks drawn for this exercise (invented values).

Edit the numbers in BLOCKS so that the page has a clear hierarchy:
- at most 3 font sizes; title and price at least 1.5 times the body size; plan name larger than the body
- "Choose plan" at weight 600 or more, "Compare plans" at a lower weight
- every space_above from SCALE
- every left edge within 1 px of the others
- blocks sorted by top come out in ascending dom_index
"""

from dataclasses import dataclass

SCALE = [4, 8, 12, 16, 24, 32, 48]
BODY_SIZE = 16


@dataclass
class Block:
    name: str
    font_size: int      # px
    weight: int         # 400 regular, 500 medium, 600 semibold, 700 bold
    space_above: int    # px
    left: int           # px
    top: int            # px, where the block sits on screen
    dom_index: int      # position in the page's code, 0 first


BLOCKS = [
    Block("Page title", 16, 400, 13, 16, 24, 3),
    Block("Plan name", 16, 400, 7, 17, 88, 5),
    Block("Price", 16, 400, 20, 16, 120, 6),
    Block("Recommended tag", 16, 400, 5, 19, 152, 7),
    Block("Choose plan", 16, 400, 18, 16, 184, 0),
    Block("Compare plans", 16, 400, 18, 22, 248, 1),
    Block("Feature line", 16, 400, 9, 16, 296, 2),
    Block("Help link", 16, 400, 30, 16, 520, 4),
]
The sample tests · test_solution.py
"""Sample tests for the plan-page-hierarchy exercise."""

from solution import BLOCKS, BODY_SIZE, SCALE

BY_NAME = {b.name: b for b in BLOCKS}


def test_eight_blocks_with_the_given_names():
    assert [b.name for b in BLOCKS] == [
        "Page title", "Plan name", "Price", "Recommended tag",
        "Choose plan", "Compare plans", "Feature line", "Help link",
    ]


def test_at_most_three_font_sizes():
    sizes = {b.font_size for b in BLOCKS}
    assert len(sizes) <= 3, f"{len(sizes)} sizes in use: {sorted(sizes)}"


def test_title_and_price_are_large():
    for name in ("Page title", "Price"):
        assert BY_NAME[name].font_size >= 1.5 * BODY_SIZE, f"{name} must be at least {1.5 * BODY_SIZE:g} px"


def test_plan_name_is_larger_than_body():
    assert BY_NAME["Plan name"].font_size > BODY_SIZE


def test_primary_action_outweighs_secondary():
    primary, secondary = BY_NAME["Choose plan"].weight, BY_NAME["Compare plans"].weight
    assert primary >= 600, "the primary action needs weight 600 or more"
    assert secondary < primary, "the secondary action must have a lower weight than the primary"
    assert secondary >= 400, "the secondary action must stay readable"


def test_spacing_comes_from_the_scale():
    off = [(b.name, b.space_above) for b in BLOCKS if b.space_above not in SCALE]
    assert not off, f"off the scale: {off}"


def test_one_left_edge():
    lefts = [b.left for b in BLOCKS]
    assert max(lefts) - min(lefts) <= 1, f"left edges span {max(lefts) - min(lefts)} px"


def test_code_order_matches_screen_order():
    by_top = sorted(BLOCKS, key=lambda b: b.top)
    indexes = [b.dom_index for b in by_top]
    assert indexes == sorted(indexes), f"code order {indexes} differs from screen order"
    assert sorted(indexes) == list(range(len(BLOCKS))), "dom_index must use 0 to 7 once each"
A hint

Start with the code order. The top values already give the screen order: the page title, the plan name and the price come first, then the tag, the buttons and the feature line, and the help link comes last. Give the dom_index values the same order, 0 to 7, so that sorting by top and by dom_index agree. Then take sizes, weights, spacing and edges one rule at a time.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then work through the lesson’s questions, from the levers to the blur test. The first one comes from the concept where your answers have been weakest:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 A page uses one size, one weight and one colour for every block. Which statement is true by the lesson's counting rule?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: No block differs from the body, so the page has no ranking

    Hierarchy comes from differences. A flat page has none, so nothing leads and every block competes.

  2. Question 2 of 15 The secondary action is pale grey text at 2.5:1 on white so that it "does not compete". What is wrong with that?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: It fails the 4.5:1 minimum for text and makes a real choice look like decoration

    The secondary action needs fewer levers than the primary one, not unreadable text. An outline or text style in a colour that meets 4.5:1 keeps it visible.

  3. Question 3 of 15 A designer proposes seven font sizes on one screen so that every block has its own rank. What do you say?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: Use about three sizes and let weight, colour and space carry the finer ranking

    A few sizes give clear steps. The restyled page has 16, 20 and 32 px and still ranks eight blocks by combining levers.

  4. Question 4 of 15 What is the main job of a spacing scale?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: To restrict every gap to a short list of values, so that equal separation looks equal and groups stay readable

    A scale makes spacing consistent and keeps the 2x rule easy to meet, because the steps between groups can be chosen to be at least twice the steps inside them.

  5. Question 5 of 15 Gaps inside groups are 12 and 16 px and gaps between groups are 24 and 32 px. What is the ratio of the smallest gap between groups to the largest gap inside one, to two decimal places?

    Type a number.

    A hint

    Divide the smallest between-group gap by the largest within-group gap.

    Show the answer: Question 5 of 15

    Answer: 1.5 (anything from 1.49 to 1.51 counts)

    The smallest gap between groups is 24 px and the largest gap inside a group is 16 px, so the ratio is 24 ÷ 16 = 1.50. That is below 2, so spacing alone does not make the groups clear.

  6. Question 6 of 15 A team's scale has 5, 10, 15, 20, 25 and 30 px, and a designer's file uses 4, 8, 12, 16, 24 and 32 px. What is the sensible next step?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: Pick one scale, record it as the team''s tokens and snap the other file to it

    Published scales differ, for example 4 px steps or 5 px steps. What matters is that a team has one and uses it for every margin and padding.

  7. Question 7 of 15 On a column grid, where should a block begin?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: At a column edge

    The grid exists to give every block a column edge to start from.

  8. Question 8 of 15 A 768 px wide window has 8 columns, 32 px margins and 16 px gutters. How wide is one column?

    Type a number.

    A hint

    Take two margins and seven gutters away from 768 px, then divide by 8.

    Show the answer: Question 8 of 15

    Answer: 74 px

    (768 − 2 × 32 − 7 × 16) ÷ 8 = (768 − 64 − 112) ÷ 8 = 592 ÷ 8 = 74 px.

  9. Question 9 of 15 A tablet is used in split-screen, so your app gets a window 520 px wide. Which grid should it use?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: The compact grid, because width classes describe the window, not the device

    Android's documentation says window size classes are not intended for device-type logic; they follow the window available, which can change with split screen, rotation or folding.

  10. Question 10 of 15 Why does the order of the code matter, if the page looks right?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: Keyboard and screen reader users follow the code order

    A page that looks right on screen can still read in a different order for people who do not use a mouse.

  11. Question 11 of 15 The example prints that positions 2, 3 and 4 differ at both widths. What does WCAG 1.3.2 require here?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: A correct reading sequence where the order carries meaning, and the recommended plan first does carry meaning

    WCAG scopes the criterion to meaning: where the order of the content affects what it says, a correct sequence must be available programmatically.

  12. Question 12 of 15 At 1024 px a sidebar sits left of the content, and at 360 px it moves below. How should the code be ordered?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: In the order that makes sense for the narrow layout, then check the wide one with a position test

    WCAG accepts more than one correct order where meaning does not change. Choose a good one, and test both layouts for positions that differ.

  13. Question 13 of 15 What does a blur test show?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: Which blocks stand out once detail is lost, before any text is read

    The blur test checks hierarchy, not usability as a whole.

  14. Question 14 of 15 On the flat page the feature lines rank first at a blur of 8 px. Why?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: They are the largest block of ink, and nothing else on the page is heavier

    Without levers, the block with the most ink wins by default. That is the accidental hierarchy a blur test exposes.

  15. Question 15 of 15 A blur test of a template ranks the primary button first. After real news photos are added, a strong-coloured photo outranks it. What does that tell you?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Content can break a hierarchy that the template got right, so test with real content

    A hierarchy belongs to the page as filled with content, not to the empty template, so repeat the test with the real images and text.

Interview questions

Warm-up: how do you decide which action on a screen is the primary one?

Start from the task the screen exists for, not from what the business would like to be clicked. The primary action is the step that moves the person’s main task forward on this screen: Choose plan on a plan page, Submit on a complaint form. There is one per view. Everything else is secondary and styled with fewer levers, an outline or a text style, so that it stays visible and readable. If two actions seem equal, the task is probably two tasks or the screen is doing too much. Back the choice with evidence: ask five people what they would do next, or run a blur test and check that the primary action is among the first things that stand out.

Warm-up: why use a spacing scale instead of choosing each gap by eye?

A scale turns spacing into a small vocabulary. Every gap comes from a short list, so equal separation looks equal across screens, groups stay readable because the gap between groups is a clear step above the gap inside them, and designers and engineers talk about the same values. It also cuts the decisions in a design review: the question becomes which step of the scale, never which number of pixels. The cost is small, because a scale of six or seven steps covers almost every layout, and an exception can be made on purpose and noted.

Two harder questions follow. Decide what you would check first, before you open either first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    On Ward Desk's complaint page, a department head asks for Escalate, Close and Add comment to be filled, same-size primary buttons, "so officers never miss one". Officers use the page forty times a day. What do you do?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    A Ward Desk dashboard has three columns. A CSS grid rule puts the Urgent column on the left, though the code lists it last. A keyboard tester says the tab order jumps. What do you check, and what do you change?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Which five levers decide what a screen leads with?

    Size, weight, colour, contrast and space, plus position. The one primary action gets more of them than the secondary action, and many large or bold elements cancel each other out.

  2. How should a secondary action look next to the primary one?

    Secondary, not missing. Pale grey text at 2.54:1 on white fails the 4.5:1 of WCAG 2.2 criterion 1.4.3; the same text in the brand blue at 6.70:1 with no fill stays readable and still looks secondary.

  3. What is a spacing scale, and what does it do for a layout?

    A short list of allowed gaps, such as 4, 8, 12, 16, 24, 32 and 48 px, from which every margin and padding is taken. Gaps become consistent, and groups stay readable while the smallest gap between groups is at least double the largest gap within one.

  4. How do you work out the column width of a column grid?

    (width − 2 × margin − (columns − 1) × gutter) ÷ columns. At 1024 px with 12 columns, 32 px margins and 24 px gutters, that is (1024 − 64 − 264) ÷ 12 = 58 px, and every block starts at a column edge.

  5. What do Android's window size classes describe?

    The window, not the device: five classes sorted by width that set how many columns a layout uses. The class can change while the app runs.

  6. CSS moves the recommended plan to the front of the screen while the code lists it fourth. Why is that a fault?

    Keyboard and screen reader users meet the cards in the order of the code. WCAG 2.2 criteria 1.3.2 and 2.4.3 ask for a reading and focus order that preserves meaning, so move the card in the code instead of restyling it.

  7. What does a blur test rank, and what should it be paired with?

    Which block dominates a layout before any text is read. It works best beside a five-second test and a task-based test with people.

Key takeaways

  • A visual hierarchy comes from five levers, size, weight, colour, contrast and space, plus position, and a screen needs one primary action that has more of them than the secondary action.
  • The secondary action should look secondary, not missing: text still meets 4.5:1 and the page keeps to about three sizes.
  • A spacing scale replaces guessed gaps with a short list, and groups stay readable from spacing alone while the smallest gap between groups is at least double the largest gap within one.
  • A column grid is one formula, (width − 2 × margin − (columns − 1) × gutter) ÷ columns, blocks that start at column edges look related, and the width class of the window, which can change while the page is open, sets how many columns to use.
  • The order of the code is the order that keyboard and screen reader users get, so it should follow the order of the screen wherever the order carries meaning.
  • A blur test ranks what dominates a layout before any text is read, and it works best beside a five-second test and a task-based test with people.

Terms in this lesson

column grid
A layout divided into equal columns with gutters between them and a margin on each side, so that every block starts at a column edge.
gutter
The fixed gap between two neighbouring columns of a grid. Column width is what remains of the width after the margins and the gutters, divided by the number of columns.
reading order
The sequence in which content is read by a screen reader and reached by keyboard, taken from the order of the code. It should match the order on screen wherever the order carries meaning.
spacing scale
A short list of allowed gaps, for example 4, 8, 12, 16, 24, 32 and 48 px, from which every margin and padding in a design is taken.
visual hierarchy
The order in which a screen asks to be read, set by differences in size, weight, colour, contrast, space and position between its elements.
whitespace
The empty space around and between elements. It is a structural part of a layout that separates groups and gives elements room, not leftover area.
window size class
A named range of window width, such as compact below 600 or medium from 600 to 839, used to choose a layout. It describes the window, not the device, and can change while an app runs.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.