Skip to content

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.

steps/basic.tsx
  1. 1CompletedAccount
    Email and password
  2. 2Address
    Where to send it
  3. 3Review
    Check 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.

steps/controlled.tsx
  1. 1Account
  2. 2Address
  3. 3Payment
  4. 4Review
Choose a sign-in method and set a password.

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.

steps/label-below.tsx
  1. 1CompletedCart
  2. 2CompletedAddress
  3. 3Payment
    Card or invoice
  4. 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.

steps/vertical.tsx
  1. 1Pick a plan

    Every plan starts with a two-week trial. You can move between them at any point, and the difference is prorated.

  2. 2Invite your team
    Optional
  3. 3Connect a repository
    GitHub, 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.

steps/sizes.tsx
  1. 1CompletedDetails
  2. 2Options
  3. 3Confirm
  1. 1CompletedDetails
  2. 2Options
  3. 3Confirm
  1. 1CompletedDetails
  2. 2Options
  3. 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.

steps/variants.tsx
  1. 1CompletedDetails
  2. 2Options
  3. 3Confirm
  1. 1CompletedDetails
  2. 2Options
  3. 3Confirm
  1. 1CompletedDetails
  2. 2Options
  3. 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.

steps/tones.tsx
  1. 1CompletedDetails
  2. 2Options
  3. 3Confirm
  1. 1CompletedDetails
  2. 2Options
  3. 3Confirm
  1. 1CompletedDetails
  2. 2Options
  3. 3Confirm
  1. 1CompletedDetails
  2. 2Options
  3. 3Confirm

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.

steps/icons.tsx
  1. CompletedAccount
  2. CompletedAddress
  3. Payment
  4. 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.

steps/clickable.tsx

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".

steps/error.tsx
  1. 1CompletedAccount
  2. 2Has an errorAddress
    Postcode not recognised
  3. 3Payment
  4. 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.

steps/manual.tsx
  1. 1CompletedOrder placed
    Monday, 09:12
  2. 2CompletedPayment confirmed
    Monday, 09:14
  3. 3Packed
    In progress at the Leeds warehouse
  4. 4Shipped
  5. 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:

  • Root is 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 the Trigger in 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". Indicator adds a visually hidden "Completed" or "Has an error" after the number — labels on 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 named Highlight and the rest get a CanvasText ring so they stay circles at all.
  • A disabled step dims to opacity: 0.55, and to GrayText at full opacity under forced colors — opacity is 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.

Theming tokens for Steps
PropertyControlsDefault
--forte-steps-tone-solidFill of an active or completed indicatorvar(--forte-color-primary)
--forte-steps-tone-on-solidText and icon colour on that fillvar(--forte-color-on-primary)
--forte-steps-tone-softThe active indicator's halo, and the outline variant's active fillvar(--forte-color-primary-soft)
--forte-steps-tone-textText colour on the soft fillvar(--forte-color-primary-text)
--forte-steps-indicator-sizeDiameter of the indicatorvar(--forte-space-6)
--forte-steps-indicator-font-sizeText size of the number inside the indicatorvar(--forte-font-size-2)
--forte-steps-indicator-border-widthWidth of the indicator's ring. Painted in the indicator's own colour in solid, so it only shows in outline.2px
--forte-steps-indicator-radiusCorner radius of the indicatorvar(--forte-radius-full)
--forte-steps-dot-sizeDiameter of the indicator in the dot variant. Retuned by size.var(--forte-space-3)
--forte-steps-active-haloWidth of the ring around the active indicatorvar(--forte-space-1)
--forte-steps-gapSpace 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-gapSpace between the title and the descriptionvar(--forte-space-1)
--forte-steps-connector-thicknessThickness of the connector2px
--forte-steps-connector-minShortest a connector can get, and the space between stacked stepsvar(--forte-space-6)
--forte-steps-track-colorColour of a connector that has not been reachedvar(--forte-color-border)
--forte-steps-fill-colorColour of a connector after a completed stepvar(--forte-steps-tone-solid)
--forte-steps-title-colorColour of a title on a step not yet reachedvar(--forte-color-foreground-muted)
--forte-steps-title-color-activeColour of the active and completed steps' titlesvar(--forte-color-foreground)
--forte-steps-title-font-sizeText size of the titlevar(--forte-font-size-2)
--forte-steps-title-font-weightWeight of a title, when not the active onevar(--forte-font-weight-medium)
--forte-steps-title-font-weight-activeWeight of the active step's titlevar(--forte-font-weight-semibold)
--forte-steps-description-colorColour of the descriptionvar(--forte-color-foreground-muted)
--forte-steps-description-font-sizeText size of the descriptionvar(--forte-font-size-1)
--forte-steps-trigger-pPadding of a clickable step, which is the inset of its hover fillvar(--forte-space-1)
--forte-steps-trigger-radiusCorner radius of a clickable step's hover fill and focus ringvar(--forte-radius-control)
--forte-steps-trigger-bg-hoverFill of a clickable step under the pointervar(--forte-color-panel-hover)
--forte-steps-fill-durationHow long the connector takes to fill after its step completesvar(--forte-duration-normal)
--forte-steps-draw-durationHow long the check takes to draw onvar(--forte-duration-normal)
--forte-steps-indicator-bgFill of the indicatorvar(--forte-color-panel-active)
--forte-steps-indicator-colorColour of the number or icon inside the indicatorvar(--forte-color-foreground-muted)
--forte-steps-indicator-border-colorColour of the indicator's ringvar(--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

Props for StepsRoot
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
currentnumberThe 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.
labelPlacementStepsLabelPlacementinlineWhere 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"`.
labelsStepsLabelsThe words read after a step's number for the states the visuals carry with an icon alone. Override to localise them.
orientationStepsOrientationhorizontalWhich 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.
renderRenderProp<Record<string, unknown>>Replaces the rendered `<ol>` with another element or component.
sizeStepsSizemdIndicator diameter and text size for the whole list.
toneStepsToneprimaryWhich 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".
variantStepsVariantsolidHow 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

Props for StepsItem
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
disabledbooleanfalseDisables 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.
statusStepsStatusThis 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

Props for StepsTrigger
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
renderRenderProp<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

Props for StepsIndicator
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

Steps.Title

Props for StepsTitle
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
renderRenderProp<Record<string, unknown>>Replaces the rendered `<span>` with another element or component.

Steps.Description

Props for StepsDescription
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
renderRenderProp<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.