Accordion Step Flow
How to use an Accordion to show the user progressing step-by-step through a series of tasks.
The Accordion Step Flow is a user experience where the user progresses step-by-step through a series of tasks, with that series represented by an Accordion. It is commonly leveraged on extra-small screens instead of a Step Indicator.
We recommend this flow on Extra-Small screens over the Step Indicator because its vertical orientation makes it a better fit for a phone's narrow screen.
In this flow:
only the current step is expanded; the user may collapse it if they wish
the user may not click on future steps to jump ahead in the flow; future steps are "disabled"
the user may click on past steps to jump back in the flow
Let's start out with the complete example, and then we'll break it down piece by piece.
Components
Here are the components and props that we've used:
Accordion
The root of everything is the Accordion
.
<Accordion skin="steps" purpose="list">...</Accordion>
skin
- the "steps" skin gives us the right look and feel for eachCollapsible
inside the Accordion.
purpose
- applies the proper semantics (so that the Accordion will use<ol>
and<li>
tags rather than<div>
tags)
Collapsible
One layer deeper, we have Collapsibles
. In most cases, we can use the StepCollapsible
convenience component:
<StepCollapsiblecollapsibleId={stepNumber.toString()}disabled={stepNumber > currentStep}aria-current={stepNumber === currentStep ? 'step' : undefined}stepNumber={`Step ${stepNumber}`}stepTitle={stepTitle}stepValue={stepValue}>...</StepCollapsible>
If you've got a non-standard use-case, you can use the lower-level components themselves:
<CollapsiblecollapsibleId={stepNumber.toString()}disabled={stepNumber > currentStep}aria-current={stepNumber === currentStep ? 'step' : undefined}><CollapsibleSummary><CollapsibleSummaryButton><CollapsibleSummaryButtonStepNumber>{`Step ${stepNumber}`}</CollapsibleSummaryButtonStepNumber><CollapsibleSummaryButtonStepTitle>{stepTitle}</CollapsibleSummaryButtonStepTitle><CollapsibleSummaryButtonStepValue>{stepValue}</CollapsibleSummaryButtonStepValue></CollapsibleSummaryButton></CollapsibleSummary><CollapsibleContent>...</CollapsibleContent></Collapsible>
collapsibleId
- identifies theCollapsible
to its parentAccordion
disabled
- allows us to apply a disabled look and functionality to theCollapsible
. We do this for steps that are in the future, since we don't want to allow users to jump ahead in the flowaria-current
helps assistive technologies understand the user's current progress through the step flowstepNumber
- pretty straightforward. It's just the text that appears on the far left of each collapsiblestepTitle
- the text that appears nextstepValue
- the value associated with the step, which is displayed on the far righterror
- not shown, but if the step has an error, you can passtrue
in order to call attention to the error
SelectionSet and TextButton
Inside of a Collapsible
, you can put whatever you want inside of your Collapsible
; just make sure that the user has some way of continuing to the next step. This example lets the user pick between Selection Set choices, clicking a "Continue" button to proceed:
<><SelectionSet><SelectionSetInput value="Standard"><SelectionSetLabel>Standard</SelectionSetLabel></SelectionSetInput><SelectionSetInput value="Square"><SelectionSetLabel>Square</SelectionSetLabel></SelectionSetInput><SelectionSetInput value="Rounded"><SelectionSetLabel>Rounded</SelectionSetLabel></SelectionSetInput></SelectionSet><TextButton>Continue</TextButton></>
Logic
With our UI all squared away, we can start adding our logic. A good way to approach this is to think UI in terms of state and events.
State
We need to track 4 pieces of information:
The
currentStep
Which
Collapsible
s are expanded vs collapsedThe
value
for each stepWhether or not there's an error on each step
Events
Someone can click a Collapsible
If the step is in the past, we want close our current step and expand the clicked step
If the step is the current step, we want to just expand/collapse the current step
If the step is in the future, we want to ignore the click, because the user isn't allowed to jump ahead
Someone can select an option from our one of our
SelectionSet
s
We'll just update the selected option for the specified step number.
Someone can click the "Continue" button
We'll close our current step and expand the next step.
Reducer
When you see a scenario where multiple pieces of state change in response to a single event (e.g. we need to update currentStep
and the list of expandedCollapsibles
at the same time), that's a time reach for React.useReducer
rather than React.useState
.
Let's write a reducer to describe the behavior of this UI.
function accordionStepFlowReducer(state, action) {switch (action.type) {case 'step-clicked':if (action.payload.stepNumber === state.currentStep) {// if it was the current-step, we'll toggle the expanded-statereturn {...state,expandedCollapsibles: {[action.payload.stepNumber]: action.payload.expanded,},}} else if (action.payload.stepNumber > state.currentStep) {// ignore clicks on future stepsreturn state}// otherwise swap to the clicked stepreturn {...state,currentStep: action.payload.stepNumber,expandedCollapsibles: { [action.payload.stepNumber]: true },}case 'next-button-clicked':return {...state,currentStep: state.currentStep + 1,expandedCollapsibles: { [state.currentStep + 1]: true },}case 'option-selected':return {...state,selectedOptions: {...state.selectedOptions,[action.payload.stepNumber]: action.payload.value,},}default:throw new Error('Unrecognized action!')}}
We've defined our 3 events:
type StepClickPayload = {type: 'step-clicked'payload: { stepNumber: number; expanded: boolean }}
type nextButtonClickedPayload = {type: 'next-button-clicked'}
type OptionSelectedPayload = {type: 'option-selected'payload: { stepNumber: number; value: string }}
All that's left to do is to dispatch
those events from our UI.
Putting it all together
Here's the code all together (except for the reducer that we've just written):
function AccordionStepFlow() {const [stepsState, dispatch] = React.useReducer(accordionStepFlowReducer, {currentStep: 1,expandedCollapsibles: { 1: true },selectedOptions: {},})const { currentStep, expandedCollapsibles, selectedOptions } = stepsStatereturn (<AccordionexpandedCollapsibles={expandedCollapsibles}onRequestExpandedChange={(collapsibleId, expanded) => {const stepNumber = parseInt(collapsibleId, 10)dispatch({ type: 'step-clicked', payload: { stepNumber, expanded } })}}skin="steps"purpose="list"><StepCollapsiblecollapsibleId="1"disabled={1 > currentStep}stepNumber="Step 1"stepTitle="Shape"stepValue={selectedOptions['1']}><SelectionSetvariant="single-select"skin="buttons"selectedValue={selectedOptions['1'] || null}onSelectedValueChange={value => {dispatch({type: 'option-selected',payload: { stepNumber: 1, value },})}}><SelectionSetInput value="Standard"><SelectionSetLabel>Standard</SelectionSetLabel></SelectionSetInput><SelectionSetInput value="Square"><SelectionSetLabel>Square</SelectionSetLabel></SelectionSetInput><SelectionSetInput value="Rounded"><SelectionSetLabel>Rounded</SelectionSetLabel></SelectionSetInput></SelectionSet><TextButton onClick={() => dispatch({ type: 'next-button-clicked' })}>Continue</TextButton></StepCollapsible><StepCollapsiblecollapsibleId="2"disabled={2 > currentStep}stepNumber="Step 2"stepTitle="Paper Weight"stepValue={selectedOptions['2']}><SelectionSetvariant="single-select"skin="buttons"selectedValue={selectedOptions['2'] || null}onSelectedValueChange={value => {dispatch({type: 'option-selected',payload: { stepNumber: 2, value },})}}><SelectionSetInput value="Standard"><SelectionSetLabel>Standard</SelectionSetLabel></SelectionSetInput><SelectionSetInput value="Premium"><SelectionSetLabel>Premium</SelectionSetLabel></SelectionSetInput></SelectionSet><TextButton onClick={() => dispatch({ type: 'next-button-clicked' })}>Continue</TextButton></StepCollapsible><StepCollapsiblecollapsibleId="3"disabled={3 > currentStep}stepNumber="Step 3"stepTitle="Finish"stepValue={selectedOptions['3']}><SelectionSetvariant="single-select"skin="buttons"selectedValue={selectedOptions['3'] || null}onSelectedValueChange={value => {dispatch({type: 'option-selected',payload: { stepNumber: 3, value },})}}><SelectionSetInput value="Matte"><SelectionSetLabel>Matte</SelectionSetLabel></SelectionSetInput><SelectionSetInput value="Glossy"><SelectionSetLabel>Glossy</SelectionSetLabel></SelectionSetInput><SelectionSetInput value="Linen"><SelectionSetLabel>Linen</SelectionSetLabel></SelectionSetInput></SelectionSet><TextButton>Go to cart</TextButton></StepCollapsible></Accordion>)}