# Frame Progress as Milestones, Not Remaining Work

**SHOULD** · **ID:** `content-progress-as-milestones` · **Category:** content
**Source:** Custom
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/content-progress-as-milestones

> SHOULD: Chunk long task lists into a handful of named phases and show the current one ("Complete Phase 1 of 4") rather than leading with remaining work ("10 / 47 tasks complete"), which makes the 37 unfinished items the salient fact. Collapse finished phases into a done marker; keep the honest total reachable, just do not lead with it.

Group a long task list into named phases so progress reads as ground gained rather than debt outstanding

This one comes from Josh Puckett's Interface Craft, and it is purely about framing: the underlying progress is identical, only the sentence changes. "10 / 47 complete" is mathematically accurate and motivationally hostile — it makes the 37 unfinished items the salient fact, and each completed task moves the bar by two percent, which reads as futility. Chunk the same work into a handful of named phases, show only the current one, and collapse finished phases into a done marker so the remaining work visibly shrinks as the user advances. Keep the honest total reachable for anyone who wants it, but do not lead with it.

## Bad — do not do this

`content-progress-milestones-bad`

```tsx
import { useState } from 'react';

const TOTAL_TASKS = 47;

const TASKS = [
  'Verify your email address',
  'Add a profile photo',
  'Set your display name',
  'Choose a timezone',
  'Connect a payment method',
  'Invite a teammate',
  'Install the CLI',
  'Create your first project',
];

export function ProgressMilestonesBad() {
  const [done, setDone] = useState(10);

  const percent = Math.round((done / TOTAL_TASKS) * 100);
  const remaining = TOTAL_TASKS - done;

  return (
    <div className="w-full max-w-md space-y-3">
      <div className="space-y-3 rounded-lg border border-border bg-card p-5">
        <div className="flex items-baseline justify-between">
          <h4 className="font-mono text-lg font-semibold tabular-nums text-foreground">
            {done} / {TOTAL_TASKS} complete
          </h4>
          <span className="font-mono text-xs tabular-nums text-muted-foreground">
            {percent}%
          </span>
        </div>

        <div
          role="progressbar"
          aria-valuenow={percent}
          aria-valuemin={0}
          aria-valuemax={100}
          aria-label="Setup progress"
          className="h-2 w-full overflow-hidden rounded-full bg-muted"
        >
          <div className="h-full rounded-full bg-primary" style={{ width: `${percent}%` }} />
        </div>

        <p className="text-xs text-muted-foreground">
          {remaining} tasks remaining
        </p>

        <ul className="space-y-1 text-sm text-muted-foreground">
          {TASKS.map((task) => (
            <li key={task} className="flex items-center gap-2">
              <span className="size-3 shrink-0 rounded-sm border border-border" aria-hidden="true" />
              {task}
            </li>
          ))}
          {remaining > TASKS.length && (
            <li className="pl-5 text-xs italic">…and {remaining - TASKS.length} more</li>
          )}
        </ul>

        <button
          type="button"
          onClick={() => setDone((d) => Math.min(TOTAL_TASKS, d + 1))}
          className="rounded-md border border-border px-3 py-1.5 text-xs font-medium text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
        >
          Complete a task
        </button>
      </div>

      <p className="text-xs text-error">
        Press it: the bar crawls ~2% and the list never gets shorter. The count is
        accurate and demoralizing — the salient number is the 37 tasks still owed.
      </p>
    </div>
  );
}
```

## Good — do this

`content-progress-milestones-good`

```tsx
import { useState } from 'react';

// Exactly the same 47 tasks as the bad example — grouped into four named phases.
const PHASES = [
  { name: 'Identity', steps: ['Verify your email', 'Set your display name', 'Add a photo'] },
  { name: 'Workspace', steps: ['Pick a timezone', 'Name your workspace', 'Invite a teammate'] },
  { name: 'Billing', steps: ['Connect a card', 'Choose a plan'] },
  { name: 'First deploy', steps: ['Install the CLI', 'Create a project', 'Ship it'] },
];

export function ProgressMilestonesGood() {
  const [phase, setPhase] = useState(0);

  const current = PHASES[phase];
  const finished = phase >= PHASES.length;

  return (
    <div className="w-full max-w-md space-y-3">
      <div className="space-y-3 rounded-lg border border-border bg-card p-5">
        {finished ? (
          <p className="text-sm font-medium text-success">
            All four phases done — you are set up.
          </p>
        ) : (
          <>
            <h4 className="text-lg font-semibold text-foreground">
              Phase {phase + 1} of {PHASES.length} · {current.name}
            </h4>

            <ol className="space-y-1 text-sm text-muted-foreground">
              {current.steps.map((step) => (
                <li key={step} className="flex items-center gap-2">
                  <span
                    className="size-3 shrink-0 rounded-sm border border-border"
                    aria-hidden="true"
                  />
                  {step}
                </li>
              ))}
            </ol>
          </>
        )}

        {/* Completed phases collapse to a done marker — the remaining work visibly shrinks. */}
        <ul className="flex flex-wrap gap-2 border-t border-border pt-3 text-xs">
          {PHASES.map((p, i) => (
            <li
              key={p.name}
              className={
                i < phase
                  ? 'rounded-full border border-border bg-muted px-2 py-1 text-success'
                  : i === phase
                    ? 'rounded-full bg-primary px-2 py-1 font-medium text-primary-foreground'
                    : 'rounded-full border border-border px-2 py-1 text-muted-foreground'
              }
            >
              {i < phase ? `✓ ${p.name}` : p.name}
            </li>
          ))}
        </ul>

        <button
          type="button"
          onClick={() => setPhase((p) => Math.min(PHASES.length, p + 1))}
          disabled={finished}
          className="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
        >
          {finished ? 'Done' : `Finish ${current.name}`}
        </button>
      </div>

      <p className="text-xs text-success">
        Same 47 tasks, four milestones. Each press retires a whole phase, and the
        list of what is left gets shorter — progress reads as ground gained, not
        debt outstanding.
      </p>
    </div>
  );
}
```

## References

- [Josh Puckett — Interface Craft](https://interfacecraft.dev/)
- [Laws of UX: Goal-Gradient Effect](https://lawsofux.com/goal-gradient-effect/)
- [NN/g: Progress Indicators](https://www.nngroup.com/articles/progress-indicators/)
