Writing

Render the interface for the person in front of it

Most interfaces are laid out once, for an average person, and then shrunk to fit a phone. The person using it is never average. They might be on a bus with one thumb free, at a desk with a screen reader, or in a hurry to find one thing. I built a lab to ask what it would take to render the interface for that person, in that moment, and to measure whether it worked.

The lab grew out of earlier work on keyboard focus order. That work took a finished page and asked one question: in what order should a keyboard user reach the controls? It treated the order as a set of continuous ranks, wrote reading order and landmark grouping as constraints, and verified the result. The page itself was taken as given. Rendering for one person removes that given, and three questions come first.

Three questions before focus order

  1. What to render. Which components earn a place on this screen, for this person, within the space there is.
  2. How to group the controls. Which controls belong together, so that people see them as one thing and a screen reader meets them as one unit.
  3. Where to put them. A position for every component on the grid, so that the task is quick to finish and nothing important falls out of reach.

Each question has several reasonable answers, from a designer's hand-written rules to methods that learn. The lab lets you swap the algorithm at every stage and scores every combination on the same yardstick.

One yardstick for every algorithm

The context describes the person and the moment: device, input method, what they came to do, expertise, hurry, low vision, reduced motor control, and which hand holds the phone. The catalogue has 19 components, from a logo and a search box to a results list, a form, and a chat button. Each component reflows for the device, and touch or motor needs raise every interactive target to 44 pixels.

Behind the catalogue sits a simulated world. Each component has hidden weights that give the probability someone uses it in a given context, and each belongs to one of five task flows: the page shell, browsing, finding, completing a task, and getting help. No learner sees the weights. A learner sees only what a real system would see: the context, and whether each component it showed was used.

The world is a simulator, so every number on this page is a number about the simulator, not about people. Its value is that the same yardstick applies to every algorithm, so a difference in the score is a difference in what the algorithm did.

StageWhat it is scored on
What to renderThe value of what was rendered as a fraction of the best possible set under the same space budget, which the lab computes exactly from the hidden weights
How to groupAgreement with the hidden task flows, as an adjusted Rand index: 1 is perfect, 0 is what chance would give
Where to put themExact counts on the final layout: overlaps, components off the screen, split groups, priority pairs out of order, top priorities below the fold, and actions out of thumb reach
The whole pageSimulated task time: Fitts' law for each move between the steps of the person's task, 0.2 seconds for every component scanned before the target, 0.35 seconds per Tab stop for keyboard and screen-reader users, and 2.5 seconds for each step that was not rendered
The lab's context panel. Dropdowns are set to Phone 390 x 760, Touch, and Find something, with sliders for expertise at 0.3 and hurry at 0.6, checkboxes for low vision and motor, and a choice of right or left hand. Below are six preset contexts and six person cards: Maya, first-year on the bus, selected; Devon, screen-reader user; Priya, staff at her desk; Luis, low vision on a tablet; Ana, limited dexterity; and Sam, keyboard only. A summary line reads: area budget 2.00 screens, at most 8 components, the oracle would render 8.
The context drives every stage. Picking a person sets the whole context at once; here, a first-year student on a bus with one thumb free, looking for a form deadline.

What to render: the log decides more than the model

Every selector produces a score per component and hands it to the same exact solver, which picks the best set that fits the space budget and a cap on the number of components. The only difference between selectors is where the scores come from. I compared hand-written rules, random choice, a logistic model per component, and two bandit methods that learn while they run: LinUCB, which adds a bonus for what it is unsure about, and Thompson sampling, which acts on a random draw from what it believes.

After 600 rounds of random contexts, each learner was scored on 60 new ones:

SelectorShare of the best possible value
Random0.37
Hand-written rules0.77
Thompson sampling0.88
Logistic model, learning online0.89
LinUCB0.92

The more useful result came from training the same logistic model offline on different logs. Trained on 400 sessions of a page that used the hand-written rules, it scored 0.68, below the rules themselves at 0.77. Trained on 400 sessions where the page showed components at random, it scored 0.94. A page that never shows a component never learns what that component is worth, and a model trained on its log inherits the blind spot. If you want a learner to improve on your rules, the log it learns from has to include what the rules left out.

Lab 1. On the left, the components selected by the logistic model after 300 online rounds, shown as chips; selected ones are outlined and the rest are struck through. A bar shows the area budget used, and tiles read 12 of 12 components, 58 percent of the budget, an oracle fraction of 0.93, and 1.5 milliseconds. On the right, a line chart of cumulative regret over 600 rounds for five learners: random rises steeply to about 923, while rules, Thompson, logistic, and LinUCB stay far lower, with LinUCB lowest. Solid lines are the page's own run to 300 rounds; dashed lines are the Python run to 600.
Lab 1. Regret is the value the best set would have earned minus what the learner earned, added up over rounds. A learner whose curve flattens has learned.

Grouping the controls

Two signals say which controls belong together. One is meaning: each component has a position on six axes such as navigation, search, and transaction. The other is behavior: how often two components are used in the same visit, measured from the simulated sessions. The lab blends the two, and a slider sets the mix. Four methods turn the blend into groups: grouping by landmark role, which is what the focus-order work assumed; agglomerative clustering; spectral clustering, which cuts the graph of affinities where it is weakest; and a soft assignment that a gradient can move.

Grouping methodAgreement with the hidden task flows
Landmark role0.41
Agglomerative clustering0.56
Spectral clustering0.58
Soft assignment0.51

Grouping by role, the familiar header, navigation, main, and footer, recovered the task flows worst. People use controls in flows, and a flow often crosses landmarks. The search box and the results list belong together for someone looking for something, even though one sits in the header and the other in the main region.

Grouping also matters for access. The lab requires every group to be contiguous in reading order, and checks it with the same verifier the focus-order work used. A screen-reader user meets a split group as two unrelated pieces, and so does a browser agent reading the same accessibility tree.

Lab 2. On the left, a 12 by 12 heat map of affinity between the selected components, darker where two components are more alike; the hero banner, featured cards, and recently viewed form a dark block. On the right, the four groups found by spectral clustering: search box and results list, flow find; logo, primary navigation, breadcrumbs, and footer links, flow shell; secondary action, settings, and notifications, flows do and shell; hero banner, featured cards, and recently viewed, flow browse. Tiles read: 4 groups, mean affinity 0.77 inside groups and 0.55 across, agreement with the flows 0.59. Three checks hold, and a note says the page's grouping matches Python's exactly.
Lab 2. The heat map is the blend of meaning and co-use. Each group card lists the hidden flows its members come from, which is how you can see a grouping go wrong.

Placing them: the objective is not the yardstick

Three methods place the components on a 12-column grid. The flow layout is what a style sheet would give: groups in priority order, each wrapping across the width. Simulated annealing tries random moves and keeps the ones that lower the cost. The third is a placement objective written so a gradient can flow through it. It penalizes overlap, leaving the screen, high priorities below the fold, lower priorities read before higher ones, groups pulled apart, and actions out of thumb reach. Every result is then rounded to the grid, pushed apart until nothing overlaps, and checked.

Over 216 runs, covering six contexts, three selectors, and four grouping methods:

PlacerTask time, secondsSplit groupsActions out of reach
Flow layout3.260.060.74
Annealing3.540.250.36
Placement objective3.320.140.40

The flow layout was hard to beat. The optimized placement came within 2 percent of it on task time and cut the actions out of thumb reach by almost half. Annealing was 9 percent slower, because its random moves scramble priority order and the reader pays for that in scanning. The step that makes every layout valid, rounding to the grid and pushing overlaps apart, also undoes much of what the objective asked for. In a separate run, raising the weight on priority order from 0.05 to as much as 1.0 changed the mean task time by 0.02 seconds or less. Tuning the objective is not the same as improving the measure you care about, and the lab makes that gap visible.

Lab 3 for the phone context. On the left, a phone frame with eight components placed on a column grid, colored by group: search box and a tall results list at the top, filters below them, then primary and secondary actions, primary navigation, logo, and a chat button near the fold line. Numbered badges give the reading order. A dashed diamond near the bottom right marks the thumb's reach, and a dashed path with times between 0.23 and 0.68 seconds connects the four steps of the find task. On the right, sliders for the objective weights with five presets, a loss curve that falls quickly and flattens over 291 steps, and a checklist: no overlaps, inside the viewport, and every group contiguous in reading order hold; one priority pair out of order and one action outside thumb reach are reported as residuals.
Lab 3. The dashed path is the person's task, timed with Fitts' law at each move. The checklist is measured on the final layout, not taken from the optimizer.

Real time means holding still

A context does not stay put. The person gets in a hurry, changes what they came for, or puts down the laptop and picks up a phone. Each change re-runs the whole pipeline in a few hundred milliseconds. A layout that rearranges itself every time is worse than a slightly slower one that stays put, because people lose their place.

The placement objective can start from the previous frame. Components that stay on screen begin where they were and are anchored there, and new ones start from the flow layout. Over an eight-frame sequence of changing contexts, the components that stayed moved 6,550 pixels in total with the warm start and 26,789 from a cold start, about a quarter as far. On single frames the ratio ran from a twenty-fourth to six tenths. Stability has a cost. On three of the seven frames the preserved layout was slower than a fresh one, and worst when the person switched to getting help: 2.78 seconds against 1.49, because the old arrangement no longer fit the new intent.

Two more controls help. Hysteresis keeps a component on screen until its score drops clearly below the threshold, and admits a new one only when it clears the threshold by the same margin, so components do not flicker in and out. A move cap limits how far a kept component may travel in one frame. Together they cut the total movement by about a third. The cap is applied before rounding and repair, so it limits what the optimizer asks for rather than what the person sees: the largest single move still reached 226 pixels against a 120-pixel cap.

A line chart titled Frame cost: warm against cold. Over eight frames, the pixels moved by components kept from the previous frame with a cold start rise to about 7,800 at frame 2 and stay between about 1,400 and 7,000 after that. With a warm start they stay between near zero and about 2,600. Both lines are dashed, marking the Python run of the sequence.
Lab 4. Pixels moved between frames by the components that stayed on screen, with and without the warm start.

What a designer needs in order to steer it

An algorithm that decides the layout is only useful if a designer can steer it, understand it, and disagree with it. The second half of the lab is about that.

With a demo set of constraints on the desktop browse context, every constraint held under all three placers. The cost appeared elsewhere: the flow layout went from no split groups to two, and the optimized placement from none to one.

Two panels. The first, titled Why is Primary action here?, reads: rendered because the logistic selector scored it 0.89 against a threshold of 0.35 and the knapsack kept it; the oracle would render it too; it spends 1 percent of the area budget. Grouped with secondary action, affinity 0.91. Placed at 16, 600, reading position 4 of 8, Tab stop 4. A table lists the penalties on it now: order 421.9 and reach 1.3, and a note says filters pushes it because filters has lower priority but reads earlier. The second panel lists the reading order and Tab stops: search box T1, results list T2, filters T3, primary action T4, secondary action T5, primary navigation T6, logo T7.
Every decision about a component, in one place: why it was rendered, what it is grouped with, where it sits in reading order, and what is pushing on it.
Lab 5. Buttons read Prefer A, Prefer B, Skip this pair, Start over, and Apply the best-ranked setting to lab 3. Below are two phone layouts of the same components side by side. A was made by annealing with a high priority-order weight; B by the flow layout with a higher fold weight. Both start with the search box and results list; they differ in where filters, the actions, navigation, the logo, and the chat button sit relative to the fold and the thumb's reach.
Lab 5. Choose the layout you would ship. After a few choices, the lab shows which visible qualities your choices weigh.

The preference fit has a limit worth knowing. In the Python run, a synthetic designer who disliked priority violations, slow task times, and tall pages made 40 choices. The fit put that designer's first two candidates first and the third fourth. Page height varied by less than a tenth of a screen across the candidates, and the fit got its sign wrong. A preference on something the choices barely exercise cannot be learned reliably, however many choices you make.

Learning the principles by playing

Each lab ends with a short card on the principle underneath it, with a small demo of the idea on its own. There is a knapsack you can drag where choosing by value per pixel loses to the exact answer, dots where position and color disagree about grouping, Fitts' law with sliders for distance and size, a noisy score crossing one threshold against two, the paired-comparison curve, and a two-by-two experiment that separates main effects from interactions.

The Fitts' law demo. A dashed line runs from a start point to a target box, labeled target W 40 px, D 400 px, 0.62 s. A chart plots movement time against distance for a 40-pixel target, with dashed curves for 8-pixel and 200-pixel targets above and below it. Sliders set the distance to 400 pixels and the width to 40 pixels.
A principle card. Movement time grows with the log of distance over target size, which is why a far target can still be quick if it is large.

The last lab is a game. You get the same components, the same person, and the same yardstick as the placement objective, and you drag the components onto the screen yourself. Then the lab scores you against the flow layout and the machine and tells you what you missed. My first attempt, stacking everything from the top of a phone screen, lost by 49 points. It placed every component without an overlap, but nine priority pairs read out of order and the scanning cost alone was 1.6 seconds more than the machine's.

Lab 7, Beat the machine. The verdict reads: The machine beat you by 49 points; 41 points against the machine's 90 and the flow baseline's 80. Tiles give task times of 4.35 seconds for the player, 3.07 for the machine, and 3.06 for the flow baseline. A table breaks this down: scanning 2.80 seconds for the player against 1.20 for both others, order violations 9 against 1 and 1, actions out of reach 3 against 2 and 1. A lesson notes that the machine kept all 4 flow steps on the first screen and 2 of 3 actions inside the thumb's reach. On the right, the machine's phone layout: search box and results list at the top, filters, then primary and secondary actions near the thumb, with navigation, logo, and chat at the fold.
Lab 7. Placing the components yourself is the quickest way to feel what the yardstick rewards. The lesson line names what the machine did that you did not.

How the lab is built

The reference implementation is a Python module that needs only numpy. The objectives are written as circuits of linear maps and the ReLU function, so every comparison, minimum, and threshold is exact and still passes a gradient; only the squared terms are smooth. The page is a single HTML file that re-implements the pipeline in JavaScript, so changing the context re-solves everything in the browser. On load, the page runs the three circuits exported from Python and checks that its own losses and gradients match them. The rendering module has 28 tests, including exact checks of the solver against brute force and numerical checks of every gradient.

What I would try next

  1. Anchor relative positions when the device changes. Today the warm start keeps a component's old coordinates, so moving from a 1280-pixel desktop to a 390-pixel phone anchors components to places that no longer exist.
  2. Put the rounding and repair inside the objective, or fit the weights to the task time directly, so the weights reach the final layout.
  3. Fit the objective weights themselves from a designer's choices, instead of ranking a fixed set of candidates.
  4. Replace the simulator with real sessions. The learners need only the context and whether each component that was shown was used.
  5. Hand the grouped and placed layout to the focus-order solver and test the rendered page with a screen reader, which closes the loop the focus-order work opened.

Limits

The world is simulated, the task-time model is a simple model of pointing, scanning, and tabbing, and nothing here has been tested with people. The catalogue has 19 components and five flows, far fewer than a real site. The figures and screenshots come from the lab's build of October 18, 2026. Small differences in the yardstick, such as the 2 percent between the flow layout and the optimized placement, are within what a different simulated world could reverse. What the lab shows reliably is the mechanism: where an algorithm's choices come from, what each choice costs on a shared measure, and where the objective and the measure part ways.

Sources