Skip to main content
On this page

Multi-step tasks

Divide a task across steps where splitting helps, and keep someone oriented as they move through it.

Overview​

A multi-step task splits one piece of work across several screens, each with its own submit, so someone completes it in stages rather than all at once. Opening an account, processing a refund and arranging a meter exchange are usually this shape.

Splitting has a cost as well as a benefit. Every step is a page load, and someone working at speed will feel each one, so the gain has to be real.

This pattern covers how a task is divided and how someone moves through it. The design of the fields inside any one step is in Forms.

The core decision​

Whether to split a task across steps depends on what splitting would give someone. Split it where doing so gives them at least one of these:

  • An order the task already has. Where an answer decides which questions follow, or the system has to act on an answer before it can ask the next, steps will lay out a sequence that exists rather than imposing one. A dependency between two fields doesn't need a step of its own, since a conditional reveal within one page can handle it. See Conditional logic.
  • Work they can't lose. Each step that submits is banked, so a closed tab or a dropped connection will cost someone the step they're on rather than the whole task.
  • Stages that match the work. Where the task genuinely arrives in phases, changes hands, or waits on information someone doesn't have yet, steps will follow how it already runs.
  • Something they can finish and return to. A named step that has been completed is easier to hand on or pick up again than a position partway down a long page.
  • Fewer unrelated decisions at once. Where the questions on a page have little to do with each other, separating them lets someone deal with one at a time.

Where splitting gives someone none of that, it's perfectly fine to keep the task on one page, even if that means a long form. A single page is faster for keyboard users, and it lets someone check an earlier answer while filling a later field.

Dividing the steps​

Divide by what someone is doing, not by how many fields fit on a screen. Each step should be nameable as one thing: 'Identify customer', 'Describe the fault', 'Take payment'.

  • Keep each step to one subject. If a step needs two unrelated headings, it is likely better as two steps.
  • Put identification and eligibility first, so a task that cannot proceed stops early rather than after everything has been entered.
  • Order the steps the way the work happens. Payment comes after the details it is paying for.
  • Keep one decision on one step. Someone choosing between options needs all the options in front of them, so a choice split across two screens will be made with half the information.

Branching and skipped steps​

Where an earlier answer makes a later step irrelevant, skip that step rather than showing it empty or disabled.

  • Pass only the steps that apply. Progress Stepper derives the total from the steps array, so a step someone skips has to come out of the array, or the count will promise them a step they'll never reach.
  • Determine the route and its number of steps as early as you can. A total that changes partway through will look like a mistake, so the questions that decide the route belong in the first step or two.
  • Where the route can't be known up front, leave the count out rather than showing a number you'll have to revise.
  • Skip the same steps on the way back, so going back follows the route someone actually took.
  • If there is a review step, show only the steps the user encountered. Leave a skipped step out of the review rather than listing it as empty or 'Not applicable'.

One step per page​

Give each step its own page and its own URL rather than swapping the contents of a single screen.

  • The step heading is not a live region. Progress Stepper renders the step name as a heading, and a heading that changes in place says nothing to a screen reader, so a step that advances without a page load has to announce itself in a status message. A page load does that work for you.
  • The browser back button should go back a step. Where steps swap in place, back takes someone out of the task altogether, which is rarely what they meant by it.
  • A URL per step can be refreshed, linked and returned to. Someone who reloads partway through should land on the step they were on, not at the beginning.

Do not run a multi-step task inside a Modal. A modal has nowhere to put a URL, a back button or a review step.

Showing progress​

Progress Stepper shows where someone is and what is coming next. Put it at the top of every step, above the form.

Loading...
Edit live code
<Stack spaceBetween={6}>
  <ProgressStepper
    currentStep={2}
    steps={['Identify customer', 'Describe the fault', 'Take payment', 'Review']}
  />
  <Form onSubmit={(e) => e.preventDefault()}>
    <FormFields>
      <TextField id="fault" name="fault" label="What is the fault?" />
    </FormFields>
    <FormActions>
      <SecondaryCTAButton type="button" iconLeft="chevron-left">
        Back
      </SecondaryCTAButton>
      <PrimaryCTAButton type="submit" iconRight="chevron-right">
        Continue
      </PrimaryCTAButton>
    </FormActions>
  </Form>
</Stack>
  • Back sits to the left of the forward button, so each chevron points the way its button goes.
  • The stepper is not navigation. It is display-only, so moving back needs a control of its own.
  • The stepper does not list every step. It names the step someone is on and the one after it. Where someone needs the shape of the whole task before they commit to it, put that on a start page.

Where a task has no persistent indicator, because it is only two steps, the step still needs a heading that says what it is.

Moving between steps​

Every step submits before the next one loads, so each has its own forward control. Put a chevron on it. See CTA Button.

  • Give every step after the first a way back. Going back should not clear anything, and the step someone returns to should still hold the answers they gave it.
  • Make leaving deliberate. Where a task can be abandoned, give it an explicit way out rather than relying on the browser back button. Where leaving discards what has been entered, see Destructive actions.

Saving progress​

Every step that submits gives someone a point they can get back to, which is one of the main reasons to split a task at all. A task that only persists on the final submit will lose everything to a closed tab or a dropped connection.

Where each step's answers go depends on what the API accepts.

  • Where the API takes a step at a time, submit each step as it is completed. The work will be durable as soon as the step is done, and the step can be validated on the server where it stands. See Validation.
  • Where the API only takes the whole task, hold the answers in the client until the final submit and treat that store as temporary.
  • One task may need both. A step that creates a record server-side can sit alongside steps held in the client until the end.

Anything held in the client is still customer data. Keep it to what the task needs, and clear it once the task is submitted or abandoned.

Returning to an unfinished task​

Where a step's answers are saved, say so when the user leaves, and tell them where to find the task again.

Reviewing before submitting​

Where a task commits something that is awkward to undo, end it with a review step showing every answer before the final submit. Someone who has been through five screens has no other way to see what they are about to send.

Build it from Summary List, and give each row a way back to the step that set it.

Loading...
Edit live code
<SummaryList>
  <SummaryItem label="Customer">
    <Cluster spaceBetween={2}>
      <ClusterItem>Jane Doe</ClusterItem>
      <ClusterItem>
        <TertiaryCTAButton size="small" icon="edit">
          Change customer
        </TertiaryCTAButton>
      </ClusterItem>
    </Cluster>
  </SummaryItem>
  <SummaryItem label="Fault">
    <Cluster spaceBetween={2}>
      <ClusterItem>No hot water</ClusterItem>
      <ClusterItem>
        <TertiaryCTAButton size="small" icon="edit">
          Change fault
        </TertiaryCTAButton>
      </ClusterItem>
    </Cluster>
  </SummaryItem>
</SummaryList>
  • The change control goes inside the value. SummaryItem takes a label and its value, with no separate slot for an action.
  • Name what each control changes. Passing icon to a CTA Button makes it icon-only and turns its children into both the tooltip and the accessible name, so 'Change customer' is what a screen reader announces even though only the icon is drawn.
  • A change control returns to the review. Someone who goes back to correct one answer should land on the review again, not walk forward through every remaining step.
  • Say what submitting does. FormActions takes a message for text above the buttons, which is where that expectation belongs. See Form.

Validation​

Validation and error handling follow the standard form guidance in Forms, down to the error types and the components that carry them. What a multi-step task adds is a decision about where each check runs and where its errors appear.

Ideally, validate each step as someone moves on from it, so a mistake will surface on the screen that produced it.

Inline validation​

Every step should perform inline validation of its own fields when someone moves to the next step, whatever the backend is doing.

ErrorSummaryNotification links each error to its field and moves focus to the summary, and both only work while those fields are on screen, so each step should raise its own errors.

Backend validation​

Where backend validation happens depends on whether a step's data can be submitted on its own.

  • Where a step can be persisted by itself, validate it on the server as the step is sent, so a rejection will land on the step and the field that caused it.
  • Where everything goes at the final submit, server-side problems can only surface at the end. Name the step each one belongs to and link back to it, because a summary pointing at a field three pages back has nothing to move focus to.

Either way, an API timeout or a rejected request is not a validation error. It belongs in generalError on Form, which says what happened without implying someone mistyped. See Error types.

Accessibility​

Much of this page is accessibility guidance already, so this is a summary of where it sits rather than a separate set of rules.

  • Each step is its own page load, because the step heading is not a live region and a step that advances in place is announced to nobody. See One step per page.
  • The step heading carries the count. Progress Stepper names it 'Step 2 of 4, Describe the fault' and hides its segmented bar from screen readers, since the heading already says the same thing. Set headingAs to keep the page outline in order.
  • Nothing on the stepper takes role="progressbar" or aria-current. The first strips the heading of its meaning and the second has no labelled steps to point at.
  • Change controls on the review step are named individually, so a column of edit buttons does not reach a screen reader as six identical controls. See Reviewing before submitting.
  • Going back does not clear a step, so nobody is made to re-enter an answer they already gave. See Moving between steps.
  • Errors stay on the step that owns them, where the summary can still move focus to the field. See Validation.

For accessibility guidance across Nebula, see the accessibility guide.

Copy guidance​

Label the forward button with what it does​

'Continue' is right where a step only moves on. Where submitting the step also saves or sends something, say that instead.

Do

Save and continue

Don't

Next

Name the outcome on the final step​

The last button commits the task, so name what it does rather than the act of sending a form.

Do

Process refund

Don't

Submit

Keep the back control plain​

One word, since the destination is a screen someone has already seen.

Do

Back

Don't

Go back to previous step

References​