Steps
A stepper is the line at the top of a wizard that says how many steps there are and which one you are on. It is a progress bar that can be read: each segment has a name, the ones behind you are ticked, and the one you are on is the one that is lit.
- 1CompletedAccountEmail and password
- 2AddressWhere to send it
- 3ReviewCheck and confirm
Import
import { Steps } from "@forte-ui/react";Steps is a namespace of six parts. The full anatomy, with every optional layer present:
<Steps.Root current={1}>
<Steps.Item>
<Steps.Trigger onClick={() => go(0)}>
<Steps.Indicator />
<Steps.Title>Account</Steps.Title>
<Steps.Description render={<span />}>Email and password</Steps.Description>
</Steps.Trigger>
</Steps.Item>
</Steps.Root>Most lists need less: Root → Item → Indicator + Title, and a Description where a step has something to say.
Examples
A wizard
The shape a real form takes: current in state, and two buttons that move it. Stepping past the last index marks every step complete, which is how the header says "done" without a fifth step to say it on.
- 1Account
- 2Address
- 3Payment
- 4Review
Labels below
labelPlacement="below" centres each title under its circle and gives every step an equal share of the row. The connector then runs from one circle's centre to the next — which is what the equal shares are for. It suits a short wizard whose titles are a word or two; a title that needs a description reads better beside the circle.
- 1CompletedCart
- 2CompletedAddress
- 3PaymentCard or invoice
- 4Done
Vertical
orientation="vertical" stacks the steps with the connector running down the side. The line runs alongside whatever the Description holds for its full height, so the active step can open up into its own content — a form section with its buttons — and the line simply gets longer.
- 1Pick a plan
Every plan starts with a two-week trial. You can move between them at any point, and the difference is prorated.
- 2Invite your teamOptional
- 3Connect a repositoryGitHub, GitLab or Bitbucket
Sizes
size is the circle's diameter and the text beside it. The connector keeps its thickness in every size; its minimum length grows with the circle.
- 1CompletedDetails
- 2Options
- 3Confirm
- 1CompletedDetails
- 2Options
- 3Confirm
- 1CompletedDetails
- 2Options
- 3Confirm
Variants
solid fills every circle and is the default. outline rings a step until it is done, so the filled circles are exactly the completed ones and the active step is the one with the soft fill inside its ring. dot drops the number and shrinks the circle to a point on a line — a timeline rather than a numbered list.
- 1CompletedDetails
- 2Options
- 3Confirm
- 1CompletedDetails
- 2Options
- 3Confirm
- 1CompletedDetails
- 2Options
- 3Confirm
Tones
tone colours "done" and "here" and nothing else. A step not yet reached stays neutral in every tone, so the colour only ever says "this far" — and the error state is always danger, whatever the tone.
Icons
Children of Steps.Indicator replace the number, in every state. The fill colour still carries the status, and a completed step still gets its visually hidden "Completed" for a screen reader — the icon is not asked to mean "done" on its own.
- CompletedAccount
- CompletedAddress
- Payment
- Delivery
A direct-child svg is sized off the circle's text by the stylesheet, so it follows the size preset without a class of its own, and it rides currentColor through every status.
Clickable steps
Wrap a step's parts in a Steps.Trigger and the whole face becomes one <button>. It takes disabled from its Item, and aria-current="step" when the step is the active one, so the current step is announced when the button takes focus rather than only when the list is walked.
This one lets the reader jump anywhere. A stricter wizard would disable the steps ahead of the furthest one reached — and a step that is a route goes in through render: render={<Link href="/checkout/address" />} replaces the button with the framework's link and keeps the step's styling and states.
Errors
status on an item overrides what current would have given it. Here the reader is on the third step, but the second one failed validation on the way past. The connector after it stays empty: the line only fills after a step that is actually done, so a broken line reads as "something back there needs attention".
- 1CompletedAccount
- 2Has an errorAddressPostcode not recognised
- 3Payment
- 4Review
Without current
Leave current off the root and each item's status is the whole story. That is the shape for a list that is not a straight line — an order's history, where the steps are events rather than pages and "complete" does not imply the one before it.
- 1CompletedOrder placedMonday, 09:12
- 2CompletedPayment confirmedMonday, 09:14
- 3PackedIn progress at the Leeds warehouse
- 4Shipped
- 5Delivered
How the connector works
The line after a step belongs to that step, and it fills when that step is complete. Nothing else about it is conditional — which is the only rule that reads the same whether the steps run across the row or down the side, and it is why the last step's connector is the one that is hidden, rather than the first's.
The fill grows along the line, over --forte-steps-fill-duration, from the step it belongs to. Under reduced motion the duration shortens rather than disappearing, for the same reason Progress keeps its fill transition there: a line that teleports is harder to read, not easier. The check in a completed circle is drawn on the same way — it is stroke-dashoffset over a duration, not a scale, because the mark is information rather than decoration and must still draw when motion is reduced.
Accessibility
The keyboard story is the platform's: a clickable step is a real <button>, so Tab reaches it and Enter or Space presses it. There is no roving focus to learn. A static list has nothing to focus.
Everything else the component does by default:
Rootis an<ol>. The steps are ordered — that is the whole point — and "list, 4 items" tells a reader how long the road is before they walk it. The visible number is real text, so a screen reader hears "2 Address" the way a sighted reader sees it.- The active step carries
aria-current="step". On the<li>in a static list, and on theTriggerin a clickable one, so it is announced when the button takes focus. - The two icon states get a word. A check mark has no text of its own; without one, a done step and an undone one would both be heard as "1 Account".
Indicatoradds a visually hidden "Completed" or "Has an error" after the number —labelson the root localises both. - The connector is
aria-hidden. It repeats what the list's order already says. - State is never colour alone. Active and complete share a fill; what tells them apart is the check, the halo and the title's weight. Under forced colors, where every fill goes to
Canvas, the reached steps are namedHighlightand the rest get aCanvasTextring so they stay circles at all. - A disabled step dims to
opacity: 0.55, and toGrayTextat full opacity under forced colors —opacityis one of the few properties that mode does not override, so left alone a dimmed step would keep full contrast and read as enabled.
Theming
Every property below is declared on Steps.Root, so override them there — through its className or an inline style — not on an ancestor, where the root's own declaration would beat the inherited value. The presets work the same way: size, variant and tone re-point a few of these knobs and nothing else. The three --forte-steps-indicator-* colour slots are the exception: they sit on each Indicator, set per status, and are the thing to re-point for one step rather than all of them.
| Property | Controls | Default |
|---|---|---|
--forte-steps-tone-solid | Fill of an active or completed indicator | var(--forte-color-primary) |
--forte-steps-tone-on-solid | Text and icon colour on that fill | var(--forte-color-on-primary) |
--forte-steps-tone-soft | The active indicator's halo, and the outline variant's active fill | var(--forte-color-primary-soft) |
--forte-steps-tone-text | Text colour on the soft fill | var(--forte-color-primary-text) |
--forte-steps-indicator-size | Diameter of the indicator | var(--forte-space-6) |
--forte-steps-indicator-font-size | Text size of the number inside the indicator | var(--forte-font-size-2) |
--forte-steps-indicator-border-width | Width of the indicator's ring. Painted in the indicator's own colour in solid, so it only shows in outline. | 2px |
--forte-steps-indicator-radius | Corner radius of the indicator | var(--forte-radius-full) |
--forte-steps-dot-size | Diameter of the indicator in the dot variant. Retuned by size. | var(--forte-space-3) |
--forte-steps-active-halo | Width of the ring around the active indicator | var(--forte-space-1) |
--forte-steps-gap | Space between the indicator and the title — and between a connector's end and the circle it points at. Follows data-forte-density. | var(--forte-control-gap) |
--forte-steps-description-gap | Space between the title and the description | var(--forte-space-1) |
--forte-steps-connector-thickness | Thickness of the connector | 2px |
--forte-steps-connector-min | Shortest a connector can get, and the space between stacked steps | var(--forte-space-6) |
--forte-steps-track-color | Colour of a connector that has not been reached | var(--forte-color-border) |
--forte-steps-fill-color | Colour of a connector after a completed step | var(--forte-steps-tone-solid) |
--forte-steps-title-color | Colour of a title on a step not yet reached | var(--forte-color-foreground-muted) |
--forte-steps-title-color-active | Colour of the active and completed steps' titles | var(--forte-color-foreground) |
--forte-steps-title-font-size | Text size of the title | var(--forte-font-size-2) |
--forte-steps-title-font-weight | Weight of a title, when not the active one | var(--forte-font-weight-medium) |
--forte-steps-title-font-weight-active | Weight of the active step's title | var(--forte-font-weight-semibold) |
--forte-steps-description-color | Colour of the description | var(--forte-color-foreground-muted) |
--forte-steps-description-font-size | Text size of the description | var(--forte-font-size-1) |
--forte-steps-trigger-p | Padding of a clickable step, which is the inset of its hover fill | var(--forte-space-1) |
--forte-steps-trigger-radius | Corner radius of a clickable step's hover fill and focus ring | var(--forte-radius-control) |
--forte-steps-trigger-bg-hover | Fill of a clickable step under the pointer | var(--forte-color-panel-hover) |
--forte-steps-fill-duration | How long the connector takes to fill after its step completes | var(--forte-duration-normal) |
--forte-steps-draw-duration | How long the check takes to draw on | var(--forte-duration-normal) |
--forte-steps-indicator-bg | Fill of the indicator | var(--forte-color-panel-active) |
--forte-steps-indicator-color | Colour of the number or icon inside the indicator | var(--forte-color-foreground-muted) |
--forte-steps-indicator-border-color | Colour of the indicator's ring | var(--forte-steps-indicator-bg) |
The parts also expose their state as data attributes — data-orientation, data-label-placement, data-size, data-variant and data-tone on the root; data-status on every item, indicator and trigger; data-interactive and data-disabled on an item — so a Tailwind arbitrary variant such as data-[status=complete]:... can target them without a wrapper.
API reference
Steps.Root
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
current | number | The zero-based index of the step the user is on. Every step before it becomes `complete`, it becomes `active`, and every step after it stays `incomplete` — a wizard sets this one number and nothing else. Leave it unset to drive each step's `status` by hand, for a list that is not a straight line. | |
labelPlacement | StepsLabelPlacement | inline | Where a horizontal step's title sits. `"inline"` puts it beside the indicator, with the connector filling the space to the next step. `"below"` centres it under the indicator and gives every step an equal share of the row — the layout for a short wizard whose titles are one or two words. Ignored when `orientation="vertical"`. |
labels | StepsLabels | The words read after a step's number for the states the visuals carry with an icon alone. Override to localise them. | |
orientation | StepsOrientation | horizontal | Which way the steps run. `"vertical"` stacks them with the connector running down the side, which is the layout for a step that carries content of its own — a form section, a set of actions — under its title. |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<ol>` with another element or component. | |
size | StepsSize | md | Indicator diameter and text size for the whole list. |
tone | StepsTone | primary | Which semantic colour the active and completed steps draw from. The error state is always `danger`, and the steps not yet reached stay neutral in every tone, so the colour only ever says "this far". |
variant | StepsVariant | solid | How the indicator is drawn. `"solid"` fills every indicator. `"outline"` rings a step until it is done, so the filled circles are exactly the completed ones. `"dot"` shrinks the indicator to a plain dot with no number in it — a timeline rather than a numbered list. |
Steps.Item
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
disabled | boolean | false | Disables the step's `Trigger`, if it has one, and marks the step `data-disabled` for styling. A step without a trigger has nothing to disable, so this only dims it. |
status | StepsStatus | This step's state, when it is not the one `current` on the root would give it. `"error"` is the usual reason — a step the user went past that failed validation — but any of the four can be set, and a root with no `current` is driven entirely by these. |
Steps.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<button>` with another element or component — `render={<Link href="/checkout/address" />}` when each step is a route. |
Steps.Indicator
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Steps.Title
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<span>` with another element or component. |
Steps.Description
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<div>` with another element or component. Pass `render={<span />}` inside a `Trigger`, where a `<div>` is not valid content for the button. |