Lesson 19 / 24
Progress and Feedback
Computing whether a progress indicator correctly reports the work remaining, distinguishing deception that raises the completion rate, and tying feedback delay to indicator classes.
Contents
The previous lesson addressed the information the interface gives the user about others. There is one more kind of information the interface gives, and it concerns the user directly: where they are in the flow, how far they have come, how much is left.
This information feeds a single decision the user makes: continue or leave? The decision is remade at the end of every step, and the only input the user has is their estimate of how much work remains. This lesson measures that estimate, computes how the indicator changes it, and separates when an indicator that raises the completion rate is information and when it is deception.
Step Count Does Not Measure Time
The borrowing flow consists of six steps, but the steps are not equal in length. Opening a record detail takes 0.8 seconds, copy selection takes 5.1 seconds. A progress bar can be filled in two different ways: by step count (three of six steps done, 50 percent) or by time (the ratio of elapsed time to total time).
The computation below compares the two bars’ estimates of remaining time, then simulates three thousand users’ decisions to continue.
// progress.mjs — accuracy of the progress indicator and its effect on dropout function generator(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; } // The borrowing flow's six steps and their actual durations (s). const STEPS = [ ["search", 1.2], ["result list", 3.4], ["record detail", 0.8], ["copy selection", 5.1], ["confirmation", 1.0], ["receipt", 2.2], ]; const DURATIONS = STEPS.map(([, d]) => d); const TOTAL = DURATIONS.reduce((a, b) => a + b, 0); const elapsed = DURATIONS.map((_, i) => DURATIONS.slice(0, i + 1).reduce((a, b) => a + b, 0)); // Two indicators: a bar that advances by step count, a bar weighted by time. const stepBar = (i) => (i + 1) / STEPS.length; const timeBar = (i) => elapsed[i] / TOTAL; console.log("step time elapsed step bar time bar remaining (actual) step bar est. time bar est."); let stepError = 0, timeError = 0; for (let i = 0; i < STEPS.length - 1; i++) { const remaining = TOTAL - elapsed[i]; const tA = elapsed[i] * (1 - stepBar(i)) / stepBar(i); const tS = elapsed[i] * (1 - timeBar(i)) / timeBar(i); stepError += Math.abs(tA - remaining) / remaining; timeError += Math.abs(tS - remaining) / remaining; console.log( `${STEPS[i][0].padEnd(15)} ${DURATIONS[i].toFixed(1).padStart(4)} ${elapsed[i].toFixed(1).padStart(7)}` + ` ${(stepBar(i) * 100).toFixed(0).padStart(9)} % ${(timeBar(i) * 100).toFixed(0).padStart(9)} %` + ` ${remaining.toFixed(1).padStart(19)} ${tA.toFixed(1).padStart(15)} ${tS.toFixed(1).padStart(15)}` ); } const n = STEPS.length - 1; console.log(`average relative error: step bar ${(stepError / n * 100).toFixed(1)} %, time bar ${(timeError / n * 100).toFixed(1)} %`); // Dropout decision: at the end of each step, the user estimates remaining time and compares it to their patience. // The correct decision is to continue if the ACTUAL remaining time does not exceed patience. const USERS = 3000; const UNCERTAINTY = 2.2; // pessimism multiplier the user applies with no indicator function decision(indicator) { const rnd = generator(20260714); let completing = 0, unnecessaryDropout = 0, unnecessaryContinue = 0; for (let k = 0; k < USERS; k++) { const patience = 5 + rnd() * 40; // accepted remaining time (s) let exited = false; for (let i = 0; i < STEPS.length - 1; i++) { const actualRemaining = TOTAL - elapsed[i]; const correctToContinue = actualRemaining <= patience; let estimate; if (indicator === "none") estimate = elapsed[i] * UNCERTAINTY; else if (indicator === "step bar") estimate = elapsed[i] * (1 - stepBar(i)) / stepBar(i); else estimate = elapsed[i] * (1 - timeBar(i)) / timeBar(i); const keepGoing = estimate <= patience; if (keepGoing && !correctToContinue) unnecessaryContinue++; if (!keepGoing && correctToContinue) unnecessaryDropout++; if (!keepGoing) { exited = true; break; } } if (!exited) completing++; } return { rate: (completing / USERS) * 100, unnecessaryDropout, unnecessaryContinue }; } console.log("\nindicator completion unnecessary dropout unnecessary continuation wrong decisions"); for (const g of ["none", "step bar", "time bar"]) { const t = decision(g); console.log( `${g.padEnd(15)} ${(t.rate.toFixed(1) + " %").padStart(9)} ${String(t.unnecessaryDropout).padStart(21)}` + ` ${String(t.unnecessaryContinue).padStart(25)} ${String(t.unnecessaryDropout + t.unnecessaryContinue).padStart(15)}` ); } console.log(`(${USERS} users, the flow's actual total time is ${TOTAL.toFixed(1)} s)`); // Feedback delay classes and the indicator each class requires const OPERATIONS = [ ["typing in the search field", 0.04], ["applying a filter", 0.18], ["opening a record", 0.7], ["copy status query", 2.4], ["creating the borrow record", 6.5], ["generating the receipt", 14.0], ]; const classify = (t) => t <= 0.1 ? "instant" : t <= 1.0 ? "flow preserved" : t <= 10 ? "attention drifts" : "attention breaks"; const required = (t) => t <= 0.1 ? "no indicator needed" : t <= 1.0 ? "a state change is enough" : t <= 10 ? "an indeterminate wait indicator" : "determinate progress + remaining time"; console.log("\noperation time class required feedback"); for (const [name, t] of OPERATIONS) { console.log(`${name.padEnd(28)} ${(t.toFixed(2) + " s").padStart(7)} ${classify(t).padEnd(17)} ${required(t)}`); } // Artificial delay: the relationship between displayed progress and actual work console.log("\nrelationship between displayed progress and actual work"); const actualWork = [0.10, 0.25, 0.45, 0.70, 0.88, 1.00]; const conditions = { "tied to reality": actualWork, "constant speed (artificial)": [0.17, 0.33, 0.50, 0.67, 0.83, 1.00], "stuck near the end": [0.30, 0.60, 0.85, 0.93, 0.96, 1.00], }; for (const [name, g] of Object.entries(conditions)) { let maxDeviation = 0, total = 0; for (let i = 0; i < g.length; i++) { const s = Math.abs(g[i] - actualWork[i]); total += s; if (s > maxDeviation) maxDeviation = s; } console.log(`${name.padEnd(28)} average deviation ${(total / g.length * 100).toFixed(1)} points, max deviation ${(maxDeviation * 100).toFixed(1)} points`); }
step time elapsed step bar time bar remaining (actual) step bar est. time bar est. search 1.2 1.2 17 % 9 % 12.5 6.0 12.5 result list 3.4 4.6 33 % 34 % 9.1 9.2 9.1 record detail 0.8 5.4 50 % 39 % 8.3 5.4 8.3 copy selection 5.1 10.5 67 % 77 % 3.2 5.3 3.2 confirmation 1.0 11.5 83 % 84 % 2.2 2.3 2.2 average relative error: step bar 31.3 %, time bar 0.0 % indicator completion unnecessary dropout unnecessary continuation wrong decisions none 48.7 % 1226 558 1784 step bar 89.2 % 11 476 487 time bar 81.4 % 0 0 0 (3000 users, the flow's actual total time is 13.7 s) operation time class required feedback typing in the search field 0.04 s instant no indicator needed applying a filter 0.18 s flow preserved a state change is enough opening a record 0.70 s flow preserved a state change is enough copy status query 2.40 s attention drifts an indeterminate wait indicator creating the borrow record 6.50 s attention drifts an indeterminate wait indicator generating the receipt 14.00 s attention breaks determinate progress + remaining time relationship between displayed progress and actual work tied to reality average deviation 0.0 points, max deviation 0.0 points constant speed (artificial) average deviation 4.7 points, max deviation 8.0 points stuck near the end average deviation 21.0 points, max deviation 40.0 points
The Step Bar’s Error Has a Direction
In the first table, the two bars diverge at the first step. When search finishes, the step bar shows 17 percent, the time bar shows 9 percent. The actual remaining time is 12.5 seconds. A user looking at the step bar estimates 6.0 seconds — less than half the truth. The time bar gives 12.5 seconds.
Average relative error is 31.3 percent for the step bar, 0 percent for the time bar. The second number is not an achievement, it is correct by definition: the time bar already shows the ratio of elapsed time, so the estimate derived from it gives the actual remaining time. The meaningful number is the first.
The step bar’s error is not random, either. Before the long step arrives, the bar produces an optimistic estimate (6.0 instead of 12.5); after the long step has passed, it produces a pessimistic one (5.3 instead of 3.2). That is, the user thinks the remaining work is less than it actually is at the start of the flow.
Completion Rate Is the Wrong Metric
The second table is the center of gravity of this lesson. With no indicator, the completion rate is 48.7 percent; with the step bar, 89.2 percent; with the time bar, 81.4 percent.
Anyone looking only at completion rate would pick the step bar. The three columns next to it say what that choice actually is. With the time bar, the number of wrong decisions is zero: no user leaves a flow they would have preferred to continue, and none stays in a flow they could not tolerate. With the step bar, 476 users unnecessarily continue — because they think the remaining work is less than it is, they stay in a flow beyond what their patience allows.
The step bar raising completion rate from 81.4 percent to 89.2 percent looks like a seven-and-a-half-point gain. The source of that gain is the user misestimating the remaining work. This has the same structure as the regret rate in The Power of Defaults lesson: the number on the dashboard rises, and the entirety of that rise comes from being wrong.
The no-indicator condition shows a different kind of loss: 1226 unnecessary dropouts. These are users who leave because of uncertainty even though the remaining work is within their patience. This is the real justification for a progress indicator — it is put there not to keep the user in the flow but to ground the user’s decision in reality. A correct indicator raising the completion rate is a consequence of that decision, not its purpose.
Feedback Delay Determines the Indicator Class
The third table addresses a different question: what should be shown to the user while an operation is running? The answer depends on the operation’s duration and is classified with three established thresholds.
Up to 0.1 seconds, the user does not perceive the delay; the result appears to arrive instantly. Typing in the search field is in this class and needs no indicator. Adding a loading indicator in this class does harm: the indicator appearing and disappearing makes a delay that does not exist visible.
Up to 1 second, the delay is perceived but does not break the user’s train of thought. Applying a filter and opening a record are in this class. Sufficient feedback is the result itself changing — the list refreshes, the page arrives. A separate wait indicator is not required.
Up to 10 seconds, the user’s attention drifts; they start looking at something else. Copy status query and creating the borrow record are in this class. A wait indicator is required here; if how long the work will take is unknown, an indeterminate indicator is sufficient, because the only decision the user makes is not “should I wait” but “is the interface working.”
Above 10 seconds, attention breaks. Generating the receipt is in this class. Here an indeterminate indicator is not enough; for the user to be able to decide whether to leave the task and come back, progress and remaining time must be shown. This is where the second table’s computation applies.
Progress Is Bound to Actual Work
The last table is an audit tool: it measures the deviation between displayed progress and the actual proportion of work completed.
The indicator tied to reality gives zero deviation. The constant-speed indicator — where the bar advances in equal intervals and does not reflect the actual distribution of work — deviates by 4.7 points on average, 8.0 at most. The indicator stuck near the end — where the bar shoots up to 85 percent and waits there — deviates by 21.0 points on average, 40.0 at most.
The sign of the deviation matters. The indicator stuck near the end shows the work as more complete than it is; the user waits because it looks “almost done,” and their wait grows longer. This is a design that raises completion rate but grounds the user’s decision in wrong information. Another form of the same structure is holding a wait indicator up for a fixed duration even though the work has actually finished; this is a delay added to make the work done look heavier than it is.
The criterion is the same criterion as in every lesson of this topic. If the user knew the bar was tied to a constant speed rather than actual work, would they keep waiting on the screen? For the indicator stuck near the end, the answer is no: the user keeps waiting because they trust the information “85 percent done.” For the indicator tied to reality, the answer is yes; the user is already deciding with correct information.
Summary
- A progress indicator feeds a single decision the user makes: continue or leave. It is useful to the degree that it correctly reports the work remaining.
- A bar that advances by step count produces a faulty estimate when the steps are not equal in length; in the computation, average relative error is 31.3 percent, and the error runs optimistic at the start of the flow.
- Completion rate is the wrong metric: the step bar raises completion from 81.4 percent to 89.2 percent, but that gain comes from 476 users thinking the remaining work is less than it is.
- The correct metric is the number of wrong decisions; with no indicator, 1226 users drop out unnecessarily, and with the time bar the number of wrong decisions is zero.
- Feedback delay splits into four classes: up to 0.1 seconds no indicator is needed, up to 1 second a state change is enough, up to 10 seconds an indeterminate wait indicator, above that determinate progress and remaining time are required.
- A progress bar is bound to actual work; a constant-speed or stuck-near-the-end indicator grounds the user’s wait decision in wrong information.
Next Step
Across the seven lessons of this topic, the same criterion appeared seven times: would the user make the same choice if they knew how the design decision was made? In salience, in the default, in social proof, in the progress bar, the same distinction showed up every time — does the decision’s power come from the user’s knowledge or their ignorance? The next lesson systematizes this criterion: it ties the patterns of rushing, hidden cost, hard-to-cancel settings, confirmshaming, and false scarcity to countable indicators and builds a way to audit an interface with those indicators.
To keep your progress and take notes, Log in
My notes
Log in to take notes.