---
title: 'Progress and Feedback'
source: 'https://academia.sh/en/courses/user-experience/progress-and-feedback'
course: 'User Experience and Behavior Design'
language: en
updated: '2026-08-19T05:19:57+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```js
// 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.
