[object Object]

A screen flow without a progress indicator feels like a vending machine that ate your dollar. The user has no idea how many more screens are coming, whether their input survived, or whether the system is still working. Completion rates drop. Tickets rise.

The fix is small. Here are the three patterns that move the needle and the one that doesn’t.

The default progress indicator is not enough

The native Lightning lightning-progress-indicator shows N circles for N screens. It is honest. It is also brittle.

Three problems:

  • It doesn’t represent branching. If your flow has decisions, the indicator either over-counts or stalls.
  • It is purely visual; no copy explains what each step does.
  • It does not show async work. A 6-second Apex call between screens looks like a frozen page.

Use it as a baseline, not as the design.

Pattern 1: named-step indicator with branch awareness

Build a Flow variable currentStepKey and a collection flowSteps with the active path’s steps. Pass both to a custom LWC progress component.

<!-- screenFlowProgress.html -->
<template>
  <div class="progress-row" role="progressbar"
       aria-valuemin="1" aria-valuemax={totalSteps} aria-valuenow={currentIndex}>
    <template for:each={steps} for:item="step" for:index="idx">
      <div key={step.key} class={step.className} aria-current={step.ariaCurrent}>
        <span class="step-num">{step.displayNumber}</span>
        <span class="step-label">{step.label}</span>
      </div>
    </template>
  </div>
</template>
import { LightningElement, api } from 'lwc';

export default class ScreenFlowProgress extends LightningElement {
  @api currentStepKey;
  @api stepsJson;

  get steps() {
    const all = JSON.parse(this.stepsJson || '[]');
    const currentIdx = all.findIndex(s => s.key === this.currentStepKey);
    return all.map((s, i) => ({
      ...s,
      displayNumber: i + 1,
      className: i < currentIdx
        ? 'step done'
        : i === currentIdx ? 'step current' : 'step upcoming',
      ariaCurrent: i === currentIdx ? 'step' : null
    }));
  }

  get currentIndex() {
    return JSON.parse(this.stepsJson).findIndex(s => s.key === this.currentStepKey) + 1;
  }

  get totalSteps() {
    return JSON.parse(this.stepsJson).length;
  }
}

When the flow takes a branch, an Assignment element updates flowSteps with the new active path’s steps and the indicator re-renders. The user sees a coherent journey even when the underlying flow is a graph.

Pattern 2: explicit async waits

When the flow has a > 1 second Apex action between screens, do not let it look frozen. Add an explicit “waiting” screen with a spinner and a copy that names the operation.

Screen: "Verifying your address..."
  Components:
    - lightning-spinner
    - text: "We're checking your address against the postal database. This usually takes 3-5 seconds."

  Visibility logic: shown only while addressVerificationStatus = 'IN_PROGRESS'

  Auto-advance via subflow polling every 1.5s

The friction-free way to implement auto-advance: an LWC on the screen polls a WaitStatus__c field via Lightning Data Service and fires the navigation event when status flips to DONE.

Users who see the system explain itself stay calm. Users who see a frozen screen click “Cancel” and call support.

Pattern 3: review screen before commit

For any flow that creates or updates more than two records, the second-to-last screen should be a “Review your details” screen showing every input value and the actions that will happen.

Screen: Review
  Components:
    - heading: "Review before submitting"
    - display-text: "We'll create:"
        - 1 Opportunity
        - 1 Contract Draft
        - 1 Approval Request to {!ApproverName}
    - data-table (read-only): all collected inputs
    - button: "Edit a section" -> branches back to that section
    - button: "Submit" -> next screen

The “Edit a section” branch is what makes this a trust pattern instead of a friction pattern. Without it, users abandon the flow rather than restart it.

The anti-pattern: completion percentages

Avoid showing “37% complete” or any continuous percentage on a screen flow. Users perceive percentage as a promise. If the flow branches and percentage jumps backwards (or forwards by 22%), trust collapses.

Stick to discrete named steps. They can shrink or grow on branching without feeling wrong.

Accessibility

The progress indicator is informational, not decorative. Use proper ARIA:

  • role="progressbar" on the container
  • aria-valuemin, aria-valuemax, aria-valuenow
  • aria-current="step" on the active step
  • Each step has visible text, not just an icon

Test with a screen reader on at least the first and last screens. Most teams skip this and ship a flow that’s unusable for low-vision users — and unusable for everyone in poor screen conditions.

Performance budget

The progress component is on every screen. Be ruthless:

  • No external API calls in the component.
  • Parse the steps JSON once in connectedCallback, not in a getter.
  • No nested SLDS containers; a flat row beats a grid.

If your record page is also tight on budget, the record page performance budget piece covers the broader LWC perf rules — they apply here too.

Measuring it

Two metrics, weekly:

  • Step abandonment rate — % of users who reach step N but never reach step N+1. Compute from FlowInterview history.
  • Average completion time — median time from first screen to final commit. Outliers > 3x median are usually users hitting a single confusing step.

If a step has > 25% abandonment, it is the design that’s wrong, not the user. Edit copy first, restructure second.

Bottom line

  • Use a named-step indicator that updates on branches; never use a continuous percentage.
  • Insert explicit “waiting” screens with named copy when async work exceeds one second.
  • Add a review screen for any flow that touches more than two records; include per-section edit branches.
  • Treat the progress indicator as informational ARIA; test with a screen reader.
  • Measure step-level abandonment weekly; high-abandonment steps are design problems, not user problems.
[object Object]
Share