Steps
Ordered process navigation built on Tabs. Use Tabs when the views
are peers rather than phases in a flow.
Playground
State
Each option needs a unique value, a label, and a state. state
supplies the default marker, value controls selection, and disabled
controls access.
incomplete— empty circleloading— loadercompleted— checkerror— critical ×disabled— incomplete marker by default
const options = [ { value: 'completed', label: 'Completed', state: 'completed' }, { value: 'loading', label: 'Loading', state: 'loading' }, { value: 'error', label: 'Error', state: 'error' }, { value: 'incomplete', label: 'Incomplete', state: 'incomplete' },]
<Steps fill defaultValue="loading" label="Task state" options={options}> <TabPanel value="completed"><Text size="small">This work is complete and ready to use.</Text></TabPanel> <TabPanel value="loading"><Text size="small">This work is currently running.</Text></TabPanel> <TabPanel value="error"><Text size="small">Resolve this error before continuing.</Text></TabPanel> <TabPanel value="incomplete"><Text size="small">This work has not started yet.</Text></TabPanel></Steps>An incomplete step can still be selected. Add disabled only when it is locked;
disabled steps keep their layout space and use the incomplete marker by default.
Pass marker or return a value from renderMarker to keep a custom marker. Use
loading for work running in the background.
Priority
secondary is the default: selected markers have a subtle fill inside their
outline. primary fills the selected marker with its tone and uses a white icon,
like a primary Button. Its 2px resting marker uses border-3 and its 2px rail
uses border-3; a completed, unselected marker keeps the primary fill at 80%
without an outline. Its completed connector moves one step stronger to border-2.
tertiary removes the marker stroke and makes its marker the icon size plus 2px on
each side.
<Steps fill priority="primary" tone="brand" defaultValue="review" label="Deployment progress" options={[ { value: 'build', label: 'Build', state: 'completed' }, { value: 'review', label: 'Review', state: 'loading' }, { value: 'deploy', label: 'Deploy', state: 'incomplete' }, ]}/>Tone
Set tone on Steps to color every step’s label, hint, and icon, plus active
and completed markers. Inactive incomplete/loading marker rings stay neutral.
Set option.tone when one step needs an override. Labels strengthen on hover and
selection; hints stay at the muted text-3 tone. In secondary and
tertiary, the marker icon always matches the label’s current color; a secondary
marker’s resting stroke uses the matching muted border tone. With every priority,
the connector after a completed step is one border step stronger: border-2 from
the standard border-3 rail. Error markers stay critical and gain a critical fill
when selected.
const options = [ { value: 'done', label: 'Done', hint: 'Complete', state: 'completed' }, { value: 'current', label: 'Validate', hint: 'Checking dependencies', state: 'loading' }, { value: 'review', label: 'Review', hint: 'Needs attention', state: 'incomplete', tone: 'warning' }, { value: 'deploy', label: 'Deploy', hint: 'Waiting', state: 'incomplete', tone: 'info' }, { value: 'error', label: 'Error', hint: 'Resolve before continuing', state: 'error' },]
<Steps tone="brand" defaultValue="current" options={options} />Labels and hints ellipsize within each step. Hovering or focusing a clipped step shows its full label and hint in a tooltip.
Steps and option tones are neutral (default), brand, info, success,
warning, and critical. Options inherit the Steps tone until option.tone is
set. Omit the Steps tone, or pass an empty string, for neutral; set an option to
neutral or an empty string to opt it out of an inherited tone.
Markers and hints
Marker precedence is:
renderMarker(...)result, unless it returnsundefinedoption.marker- Built-in marker from
state
Return null from renderMarker for an intentionally empty ring.
A number marker is shown directly; an icon-shape string renders an Icon.
hint adds secondary text.
const options = [ { value: 'draft', label: 'Draft', hint: 'Saved', state: 'completed', marker: 1 }, { value: 'approval', label: 'Approval', hint: 'In review', state: 'loading', marker: 'hourglass' }, { value: 'publish', label: 'Publish', hint: 'Not started', state: 'incomplete', marker: 'send' },]
<Steps defaultValue="approval" label="Publishing progress" options={options} />Use renderMarker when a marker depends on the current step state. Its returned
node wins; return undefined to fall back to marker, then the state marker.
const options = [ { value: 'draft', label: 'Draft', state: 'completed', marker: 1 }, { value: 'approval', label: 'Approval', state: 'loading' }, { value: 'publish', label: 'Publish', state: 'incomplete', marker: 'send' },]
<Steps defaultValue="approval" label="Publishing progress" options={options} renderMarker={(option, { selected }) => option.state === 'loading' ? <Icon shape={selected ? 'refresh-ccw-dot' : 'hourglass'} /> : undefined }/>Panels
Add a TabPanel with the same value as its option. Panels stay
mounted while inactive. Omit them when a router renders the content.
Selection
Use defaultValue for uncontrolled selection. Use value and onStateChange
when the application owns it.
const options = [ { value: 'build', label: 'Build', state: 'completed' }, { value: 'setup', label: 'Setup', state: 'loading' }, { value: 'next', label: 'Next steps', state: 'incomplete' },]const [phase, setPhase] = useState('setup')
<Steps value={phase} tone="brand" options={options} onStateChange={(_event, { next }) => next && setPhase(next)}/>The application also owns state changes and decides when to enable future
steps. Steps does not advance or complete them automatically.
Flow actions
Place Back and Continue beside Steps so validation and side effects stay in the
application.
const options = [ { value: 'build', label: 'Build', state: 'completed' }, { value: 'setup', label: 'Setup', state: 'loading' }, { value: 'next', label: 'Next steps', state: 'incomplete' },]const [phase, setPhase] = useState('setup')const index = options.findIndex((option) => option.value === phase)
<Steps value={phase} options={options} onStateChange={(_event, { next }) => next && setPhase(next)}/><Button label="Back" disabled={index === 0} onClick={() => setPhase(options[index - 1].value)} /><Button label="Continue" disabled={index === options.length - 1} onClick={() => setPhase(options[index + 1].value)} />Size
small, medium (default), and large use 24px, 28px, and 32px markers.
<Steps size="small" options={[ { value: 'build', label: 'Build', state: 'completed' }, { value: 'setup', label: 'Setup', state: 'loading' }, { value: 'next', label: 'Next steps', state: 'incomplete' }, ]}/>Orientation
Horizontal steps scroll when needed. Vertical steps place the active panel before the next step.
<Steps orientation="vertical" options={[ { value: 'build', label: 'Build', state: 'completed' }, { value: 'setup', label: 'Setup', state: 'loading' }, { value: 'next', label: 'Next steps', state: 'incomplete' }, ]}> <TabPanel value="build"><Text size="small">Build output is ready.</Text></TabPanel> <TabPanel value="setup"><Text size="small">Preparing the environment.</Text></TabPanel> <TabPanel value="next"><Text size="small">Review the result.</Text></TabPanel></Steps> Keyboard and accessibility
Pass label to name the tablist. Arrow keys follow the orientation; Home and
End move to the edges; Enter and Space activate a focused step. Disabled
steps are skipped.
Markers and connectors are decorative. Put important state text in the active panel or an application-owned live region.
Events
Events match Tabs.
| Callback | When | Cancelable | Payload |
|---|---|---|---|
onStateChange(event, { next, prev }) | Before selection | Yes | Next and previous values |
onChange(event) | After selection | No | Native change event |
onValueChange(event, { value }) | After selection | No | Selected value |
Steps props
| Prop | Type | Default | Description |
|---|---|---|---|
| options | StepOption[] | — | Ordered process phases. |
| priority? | primarysecondarytertiary | 'secondary' | Visual emphasis. Primary uses a solid selected marker and a stronger completed connector; secondary is the outlined default; tertiary is compact and borderless. |
| tone? | neutralbrandcriticalinfosuccesswarning | 'neutral' | Tone applied to every step without its own tone. Error steps stay
critical and disabled steps stay neutral. |
| renderMarker? | (option, state) => ReactNode | — | Builds a custom marker from a step and its current state. A returned node
replaces marker and the built-in state marker; return undefined to use
those fallbacks, or null for an empty ring. |
| children? | ReactNode | — | Optional <TabPanel value="…"> panels, one per tab value. Each is a
self-managing <a-tabpanel> that shows itself when its value is the active
tab. Omit them to use Tabs as a bare selectable strip. To place panels in a
different layout region, or to unmount an inactive panel, drive selection with
a controlled value and render the content yourself (see the docs). |
| value? | string | — | Controlled active value — the tab value to mark selected (and, when a
<TabPanel value="…"> shares it, the panel to reveal). When set, you own
selection: the strip renders exactly what this says, and a user pick only
requests a change via onStateChange — apply it by updating this prop.
Leave undefined (and use defaultValue) for uncontrolled. |
| defaultValue? | string | — | Initial active value for the uncontrolled case. After first render Tabs
owns selection itself. |
| onStateChange? | (event, detail) => void | — | Fired whenever the active tab changes — event-first. detail is
{ next, prev } (values; null = none). Cancelable: event.preventDefault()
vetoes it (uncontrolled), or in controlled mode answer by updating value. |
| onChange? | (event) => void | — | Fired after the active tab changes — a native change event. |
| onValueChange? | (event, attrs) => void | — | Like onChange, but with a { value } snapshot as the 2nd argument. |
| onFocus? | (event) => void | — | Focus entered the strip (any tab) — wired to focusin (focus lands on a tab,
not the tablist). |
| onBlur? | (event) => void | — | Focus left the strip entirely — wired to focusout. |
| label? | string | — | Accessible name for the tablist (aria-label). |
| size? | smallmediumlarge | 'medium' | Size — small 24px · medium 28px · large 32px tall, matching Button's scale (the tab's label leading runs a touch tighter, offset by 1px more block padding per side). |
| orientation? | horizontalvertical | 'horizontal' | Layout + arrow-key axis. Horizontal ellipsizes labels when tabs overflow (scroll is opt-in via CSS); vertical stacks them. |
| fill? | boolean | false | Makes horizontal tabs share the available inline space equally. |
| disabled? | boolean | — | Disable the whole strip. |
Inherited props (className, id, slot, style, tabIndex, title)
| Prop | Type | Default | Description |
|---|---|---|---|
| className? | string | — | CSS class on the component's root element (merged with the component's own
classes). Use it directly for layout and positioning — grid/flex placement,
margins, alignment — rather than wrapping the component in a <div>/<span>. |
| id? | string | — | HTML id attribute. |
| slot? | string | — | Assigns the element to a named <slot> of a parent web component (e.g.
slot="header" inside a <Card>, slot="footer" inside a <Dialog>). |
| style? | CSSProperties | — | Inline styles on the component's root element. Set layout/positioning here
(or via className) directly on the component instead of adding a wrapper. |
| tabIndex? | number | — | Tab order. Set to -1 to skip the element when tabbing. |
| title? | string | — | HTML title attribute — native browser tooltip on hover. |
Plus every standard DOM event handler (onClick,
onFocus, onKeyDown, …) and any
data-* or aria-* attribute — forwarded as-is to
the underlying <a-*> element.
Step option props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Stable phase identity. Values must be unique within the sequence. |
| label | ReactNode | — | Visible phase label. It ellipsizes in a constrained step; its full content is available in a tooltip only when clipped. |
| state | incompleteloadingcompletederror | — | Application-owned process state. Selection and availability are separate;
error keeps a critical icon and outline, and selection adds the fill. |
| hint? | ReactNode | — | Secondary text shown below the label. It ellipsizes in a constrained step; its full content is available in a tooltip only when clipped. |
| tone? | neutralbrandcriticalinfosuccesswarning | inherits Steps `tone | Tone for this phase. It colors the label, hint, marker, and completed
connector. Overrides the Steps tone; error steps stay critical and
disabled steps stay neutral. |
| marker? | StepMarker | — | Replaces the marker derived from state. A number is shown directly; an
Anta icon shape is rendered as an <Icon>. |
| disabled? | boolean | — | Disables this phase. A custom marker or renderMarker result is kept;
otherwise it uses the incomplete marker. |
| className? | string | — | CSS class on the option's rendered row. |
| style? | CSSProperties | — | Inline styles on the option's rendered row. |
Web Component
For framework-free use, compose the same light DOM that Steps emits.
a-steps and its a-step-* children are structural elements styled by the
shipped Steps stylesheet. The Tabs Web Component inside
owns selection and panel behavior; markers, states, and tones stay in your markup.
import '@antadesign/anta/elements'import '@antadesign/anta/components/Steps.css'<div> <a-steps> <a-tabs role="tablist" aria-label="Setup progress" priority="secondary" default-state="setup" data-steps fill noslide> <a-tab role="tab" value="build" tabindex="0" tone="success"> <a-step-marker aria-hidden="true"><a-icon shape="check"></a-icon></a-step-marker> <a-step-desc><a-tab-label>Build</a-tab-label><a-step-hint>Complete</a-step-hint></a-step-desc> </a-tab> <a-tab role="tab" value="setup" tabindex="-1" tone="brand"> <a-step-marker aria-hidden="true"><a-loader></a-loader></a-step-marker> <a-step-desc><a-tab-label>Setup</a-tab-label><a-step-hint>In progress</a-step-hint></a-step-desc> </a-tab> <a-tab role="tab" value="review" tabindex="-1"> <a-step-marker aria-hidden="true"><a-icon shape="circle-large"></a-icon></a-step-marker> <a-step-desc><a-tab-label>Review</a-tab-label><a-step-hint>Waiting</a-step-hint></a-step-desc> </a-tab> </a-tabs> <a-tabpanel role="tabpanel" value="build">Build output is ready.</a-tabpanel> <a-tabpanel role="tabpanel" value="setup">Prepare the environment.</a-tabpanel> <a-tabpanel role="tabpanel" value="review">Review the result.</a-tabpanel> </a-steps></div> Styling
Use priority, size, and orientation first. Set tone on each option. Each
option also accepts className, style, and data-* attributes. Markers are
light DOM; connectors are pseudo-elements.
Dotted connector. For horizontal steps, replace the connector’s solid background with a dotted border:
.dotted-steps a-tab:not(:last-child)::before { height: 0; background: none; border-block-start: 2px dotted var(--border-2-brand);}className on Steps applies to a-steps. .dotted-steps is only the demo
scope; use your own selector when applying the rule.
Fill the container. Pass fill to spread horizontal steps across their
container. The connectors take the remaining space, placing the middle step in
the middle and the last step at the far edge.
<Steps fill defaultValue="review" label="Deployment progress" options={[ { value: 'build', label: 'Build', state: 'completed' }, { value: 'review', label: 'Review', state: 'loading' }, { value: 'deploy', label: 'Deploy', state: 'incomplete' }, ]}/>