Skip to Content

Stepper

Keep the user informed on their progress by dividing content into logical steps.

import * as React from 'react';
import { Button, Stepper } from '@itwin/itwinui-react';

const steps = [
  { name: 'First Step' },
  { name: 'Second Step' },
  { name: 'Third Step' },
  { name: 'Last Step' },
];

export default () => {
  const [currentStep, setCurrentStep] = React.useState(2);

  return (
    <div className='demo-container'>
      <div className='demo-stepper'>
        <Stepper
          currentStep={currentStep}
          steps={steps}
          onStepClick={(index) => {
            setCurrentStep(index);
          }}
        />
      </div>

      <div className='demo-button-container'>
        <Button
          disabled={currentStep === 0}
          onClick={() => {
            if (currentStep !== 0) setCurrentStep(currentStep - 1);
          }}
        >
          Previous
        </Button>
        <Button
          styleType='cta'
          disabled={currentStep === steps.length - 1}
          onClick={() => {
            if (currentStep < steps.length - 1) setCurrentStep(currentStep + 1);
          }}
        >
          Next
        </Button>
      </div>
    </div>
  );
};

In defined, multi-step user interactions, it is useful to inform the user of the number of steps involved in the process, and the current step within that sequence. A UI ‘Stepper’ guides the user through the steps and provides some interaction to return to previous steps. A standard example of a Stepper would be an e-Commerce checkout process, but there are many others where the user must perform sequential steps.

Variants

There are two different styles of stepper, depending on the amount of steps required to complete the process.

Short Stepper

In the default “short” stepper, each step is named under a circle.

import * as React from 'react';
import { Button, Stepper } from '@itwin/itwinui-react';

const steps = [
  { name: 'Previous Step' },
  { name: 'Current Step' },
  { name: 'Next Step' },
];

export default () => {
  const [currentStep, setCurrentStep] = React.useState(1);

  return (
    <div className='demo-container'>
      <div className='demo-stepper'>
        <Stepper
          currentStep={currentStep}
          steps={steps}
          onStepClick={(index) => {
            setCurrentStep(index);
          }}
          type='default'
        />
      </div>

      <div className='demo-button-container'>
        <Button
          disabled={currentStep === 0}
          onClick={() => {
            if (currentStep !== 0) setCurrentStep(currentStep - 1);
          }}
        >
          Previous
        </Button>
        <Button
          styleType='cta'
          disabled={currentStep === steps.length - 1}
          onClick={() => {
            if (currentStep < steps.length - 1) setCurrentStep(currentStep + 1);
          }}
        >
          Next
        </Button>
      </div>
    </div>
  );
};

Designers should confirm the display is acceptable due to the number of steps and labels at various resolutions. If text wrapping occurs or the stepper’s visual quality decreases significantly when the window is re-sized, use the long stepper instead.

Long Stepper

In the long stepper, there are no labels underneath the step indicators to save space. Instead, the label for the step currently in progress appears under the diagram along with ‘Step X of X’. This allocates more space for longer step names, as well as allowing more step indicators to be contained within the diagram.

import * as React from 'react';
import { Button, Stepper } from '@itwin/itwinui-react';

const steps = [
  { name: 'First Step' },
  { name: 'Second Step' },
  { name: 'Third Step' },
  { name: 'Fourth Step' },
  { name: 'Fifth Step' },
  { name: 'Sixth Step' },
  { name: 'Last Step' },
];

export default () => {
  const [currentStep, setCurrentStep] = React.useState(2);

  return (
    <div className='demo-container'>
      <div className='demo-stepper'>
        <Stepper
          currentStep={currentStep}
          steps={steps}
          onStepClick={(index) => {
            setCurrentStep(index);
          }}
          type='long'
        />
      </div>

      <div className='demo-button-container'>
        <Button
          disabled={currentStep === 0}
          onClick={() => {
            if (currentStep !== 0) setCurrentStep(currentStep - 1);
          }}
        >
          Previous
        </Button>
        <Button
          styleType='cta'
          disabled={currentStep === steps.length - 1}
          onClick={() => {
            if (currentStep < steps.length - 1) setCurrentStep(currentStep + 1);
          }}
        >
          Next
        </Button>
      </div>
    </div>
  );
};

Usage

Short vs Long Stepper

  • Use a short stepper if a process has less than five steps and if the label for each step can be summarized with 1-2 words.

  • Use a long stepper if a process has five steps or more or if the label for each step are long (20 characters or over) and/or cannot be summarized

  • A long stepper may be used for a process corresponding to the short stepper’s criteria if desired. However, using a short stepper for a process that would require a long stepper is not recommended.

Tooltip

Additional (optional) information can be displayed within a tooltip when hovering over a step indicator.

import * as React from 'react';
import { Button, Stepper } from '@itwin/itwinui-react';

const steps = [
  { name: 'Completed step', description: 'Completed tooltip' },
  { name: 'Current step', description: 'Current tooltip' },
  { name: 'Next step', description: 'Next tooltip' },
  { name: 'Last step', description: 'Last tooltip' },
];

export default () => {
  const [currentStep, setCurrentStep] = React.useState(1);

  return (
    <div className='demo-container'>
      <div className='demo-stepper'>
        <Stepper
          currentStep={currentStep}
          steps={steps}
          onStepClick={(index) => {
            setCurrentStep(index);
          }}
        />
      </div>

      <div className='demo-button-container'>
        <Button
          disabled={currentStep === 0}
          onClick={() => {
            if (currentStep !== 0) setCurrentStep(currentStep - 1);
          }}
        >
          Previous
        </Button>
        <Button
          styleType='cta'
          disabled={currentStep === steps.length - 1}
          onClick={() => {
            if (currentStep < steps.length - 1) setCurrentStep(currentStep + 1);
          }}
        >
          Next
        </Button>
      </div>
    </div>
  );
};

Localization

The ‘Step X of X’ label that appears in the default long stepper can be replaced with a localized string.

import * as React from 'react';
import { Button, Stepper } from '@itwin/itwinui-react';

const steps = [
  { name: 'First Step' },
  { name: 'Second Step' },
  { name: 'Third Step' },
  { name: 'Fourth Step' },
  { name: 'Fifth Step' },
  { name: 'Sixth Step' },
  { name: 'Last Step' },
];

const localization = {
  stepsCountLabel: (currentStep, totalSteps) =>
    `Localized step ${currentStep} of ${totalSteps}:`,
};

export default () => {
  const [currentStep, setCurrentStep] = React.useState(2);

  return (
    <div className='demo-container'>
      <div className='demo-stepper'>
        <Stepper
          currentStep={currentStep}
          steps={steps}
          onStepClick={(index) => {
            setCurrentStep(index);
          }}
          type='long'
          localization={localization}
        />
      </div>

      <div className='demo-button-container'>
        <Button
          disabled={currentStep === 0}
          onClick={() => {
            if (currentStep !== 0) setCurrentStep(currentStep - 1);
          }}
        >
          Previous
        </Button>
        <Button
          styleType='cta'
          disabled={currentStep === steps.length - 1}
          onClick={() => {
            if (currentStep < steps.length - 1) setCurrentStep(currentStep + 1);
          }}
        >
          Next
        </Button>
      </div>
    </div>
  );
};

Custom Icon

By default, each step circle shows the step number. However, users can customize the icon or content displayed for each step in the Stepper component by providing the stepContent property in each item of the steps array.

This property is a function that returns custom content to represent the status of each step. A common use case for this customization is to indicate step completion.

import * as React from 'react';
import { Button, Stepper } from '@itwin/itwinui-react';
import { SvgCheckmarkSmall } from '@itwin/itwinui-icons-react';

export default () => {
  const [currentStep, setCurrentStep] = React.useState(0);

  const steps = Array(4)
    .fill()
    .map((_, index) => ({
      name: `Step ${index + 1}`,
      stepContent: () => (index < currentStep ? <SvgCheckmarkSmall /> : null),
    }));

  return (
    <div className='demo-container'>
      <div className='demo-stepper'>
        <Stepper
          currentStep={currentStep}
          steps={steps}
          onStepClick={(index) => {
            setCurrentStep(index);
          }}
        />
      </div>

      <div className='demo-button-container'>
        <Button
          disabled={currentStep === 0}
          onClick={() => {
            if (currentStep !== 0) {
              setCurrentStep(currentStep - 1);
            }
          }}
        >
          Previous
        </Button>
        <Button
          styleType='cta'
          disabled={currentStep === steps.length - 1}
          onClick={() => {
            if (currentStep < steps.length - 1) {
              setCurrentStep(currentStep + 1);
            }
          }}
        >
          Next
        </Button>
      </div>
    </div>
  );
};

Layout

We recommend the following layout:

  1. Title: Displayed above the stepper so the user acknowledges what the process is about.

  2. Stepper

  3. Content

  4. Buttons: A call-to-action button for progress & one or two default buttons for canceling and/or going back one step. If the user has not completed a required task, the call-to-action button may be disabled. If the stepper is in a full screen page, the buttons can be placed within a sticky footer.

import * as React from 'react';
import {
  Button,
  Input,
  Label,
  Stepper,
  InputGroup,
  InputGrid,
  Radio,
} from '@itwin/itwinui-react';

const stepLabels = [
  { name: 'User Info' },
  { name: 'Color Selection' },
  { name: 'Explanation' },
];

export default () => {
  const [currentStep, setCurrentStep] = React.useState(0);
  const [disableProgress, setDisableProgress] = React.useState(true);

  React.useEffect(() => {
    setDisableProgress(true);
  }, [currentStep]);
  const stepOne = (
    <InputGrid>
      <Label required>Name</Label>
      <Input
        key='name'
        placeholder='Enter name'
        onChange={({ target: { value } }) => {
          setDisableProgress(!value);
        }}
      />
      <Label>Occupation</Label>
      <Input key='occupation' placeholder='Enter occupation' />
    </InputGrid>
  );

  const stepTwo = (
    <InputGroup
      key='color'
      label='Choose your favorite color'
      required
      onChange={({ target: { value } }) => {
        setDisableProgress(!value);
      }}
    >
      <Radio name='color' value='Red' label='Red' />
      <Radio name='color' value='Orange' label='Orange' />
      <Radio name='color' value='Yellow' label='Yellow' />
      <Radio name='color' value='Green' label='Green' />
      <Radio name='color' value='Blue' label='Blue' />
      <Radio name='color' value='Purple' label='Purple' />
    </InputGroup>
  );

  const stepThree = (
    <InputGrid>
      <Label required>Why is this your favorite color</Label>
      <Input
        key='explanation'
        placeholder='Enter text here...'
        onChange={({ target: { value } }) => {
          setDisableProgress(!value);
        }}
      />
    </InputGrid>
  );

  const steps = [stepOne, stepTwo, stepThree];

  return (
    <>
      <div className='demo-container'>
        <h2 className='demo-header'>Color survey</h2>
        <div className='demo-stepper'>
          <Stepper
            currentStep={currentStep}
            steps={stepLabels}
            onStepClick={(index) => {
              setDisableProgress(true);
              setCurrentStep(index);
            }}
          />
        </div>
        <div className='demo-current-step'>{steps[currentStep]}</div>
        <div className='demo-button-container'>
          <Button
            disabled={currentStep === 0}
            onClick={() => {
              if (currentStep !== 0) {
                setDisableProgress(true);
                setCurrentStep(currentStep - 1);
              }
            }}
          >
            Back
          </Button>
          <Button
            styleType='cta'
            disabled={disableProgress}
            onClick={() => {
              if (currentStep < steps.length - 1) {
                setDisableProgress(true);
                setCurrentStep(currentStep + 1);
              }
            }}
          >
            {currentStep === 2 ? 'Register' : 'Next'}
          </Button>
        </div>
      </div>
    </>
  );
};

Props

Prop Description Default
currentStep
Current step index, 0 - based.
number
steps
An array of step objects.
StepProperties[]
type
The type of Stepper to display.
"default" | "long"
'default'
localization
Option to provide localized strings.
StepperLocalization
onStepClick
Click handler on completed step.
(clickedIndex: number) => void
stepProps
Callback that can provide additional props for <li> representing a step.
(index: number) => DetailedHTMLProps<LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>
trackContentProps
Allows props to be passed for track content.
(index: number) => DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
circleProps
Allows props to be passed for circle.
(index: number) => DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
nameProps
Allows props to be passed for name.
(index: number) => DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
labelProps
Allows props to be passed for label.
DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
labelCountProps
Allows props to be passed for label count.
DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
as
"symbol" | "object" | "div" | "a" | "abbr" | "address" | "area" | "article" | "aside" | "audio" | "b" | "base" | "bdi" | "bdo" | "big" | "blockquote" | "body" | "br" | "button" | "canvas" | ... 158 more ... | FunctionComponent<...>