Welcome to Oshon · v1.0  ·  Now in public beta for enterprise teams Read the launch notes
NavigationUpdatedFreeWCAG 2.2 AA

Steps

Progressive stepper: Incomplete / Active / Completed / Error.

Preview

Live preview
@oshon-ai/components
  1. Completed step. Identity
  2. Completed step. Permissions
  3. Audit hooks
  4. Review

Installation

Install the runtime packages:

pnpm
pnpm add @oshon-ai/components @oshon-ai/tokens @oshon-ai/primitives

Or scaffold the component source directly into your codebase (shadcn-style):

pnpm
pnpm dlx @oshon-ai/cli add steps

Wire the tokens into your Tailwind v4 stylesheet:

css
/* app/globals.css */
@import 'tailwindcss';
@import '@oshon-ai/tokens/css';
@import '@oshon-ai/tokens/tailwind';

New here? Walk through the full setup — prereqs, theming, your first render.

Usage

Import the component and render it. Every component supports the standard tier, size, and disabled props where applicable.

tsx
'use client';
import { Steps } from '@oshon-ai/components';

export default function Example() {
  return <Steps />;
}

State overview (Figma 8587:61207)

Progression: completed, active, and upcoming

  1. Completed step. Account
  2. Completed step. Profile
  3. Plan
  4. Payment
  5. Confirm
Steps before the current one are completed, with a solid teal connector. The current step is active. Upcoming steps are incomplete, with a dashed gray connector.

Error state

  1. Completed step. Account
  2. Error in step. Profile
  3. Plan
  4. Payment
  5. Confirm
A per-step state: 'error' override marks one step red without moving the overall progress position.
tsx
<div style={{ ...col, gap: '2rem', width: '100%', maxWidth: '760px' }}>
      <section style={card}>
        <h3 style={heading}>Progression: completed, active, and upcoming</h3>
        <Steps steps={FIVE_STEPS} current={2} />
        <div style={caption}>
          Steps before the current one are completed, with a solid teal
          connector. The current step is active. Upcoming steps are incomplete,
          with a dashed gray connector.
        </div>
      </section>

      <section style={card}>
        <h3 style={heading}>Error state</h3>
        <Steps
          steps={[
            { label: 'Account' },
            { label: 'Profile', state: 'error' },
            { label: 'Plan' },
            { label: 'Payment' },
            { label: 'Confirm' },
          ]}
          current={1}
        />
        <div style={caption}>
          A per-step <code>state: &apos;error&apos;</code> override marks one step
          red without moving the overall progress position.
        </div>
      </section>
    </div>

Wizard — interactive next/back

  1. Completed step. Account
  2. Profile
  3. Plan
  4. Payment
  5. Confirm
Active step: Profile (2/5). Click any completed (filled teal) circle to revisit it. Future steps are non-clickable per the Figma rule.
tsx
<WizardDemo />

Skip-ahead — allowSkipAhead escape hatch

  1. Account
  2. Profile
  3. Plan
  4. Payment
  5. Confirm
With allowSkipAhead every circle is clickable, including future incomplete steps. Use sparingly — the default forward-block matches the Figma annotation.
tsx
<SkipAheadDemo />

Error override — per-step state

  1. Completed step. Upload
  2. Error in step. Validate
  3. Publish
After a validation failure, mark the broken step state: 'error' and leave current on it. The user sees both the failure cue and the next-action affordance in one rail.
tsx
<div style={{ ...col, width: '100%', maxWidth: '760px' }}>
      <Steps
        steps={[
          { label: 'Upload' },
          { label: 'Validate', state: 'error' },
          { label: 'Publish' },
        ]}
        current={1}
      />
      <div style={caption}>
        After a validation failure, mark the broken step
        <code>{` state: 'error' `}</code>
        and leave <code>current</code> on it. The user sees both the
        failure cue and the next-action affordance in one rail.
      </div>
    </div>

Vertical — Settings-style flow

  1. Completed step. Connect a workspaceLink your Slack or Teams account to start syncing
  2. Invite teammatesSend up to 10 invites — they keep their seats
  3. Pick a planFree for the first 14 days, no card required
  4. Launch
tsx
<div style={{ ...col, width: '100%', maxWidth: '480px' }}>
      <Steps
        orientation="vertical"
        steps={[
          {
            label: 'Connect a workspace',
            description: 'Link your Slack or Teams account to start syncing',
          },
          {
            label: 'Invite teammates',
            description: 'Send up to 10 invites — they keep their seats',
          },
          {
            label: 'Pick a plan',
            description: 'Free for the first 14 days, no card required',
          },
          {
            label: 'Launch',
          },
        ]}
        current={1}
      />
    </div>

Size matrix (playground) — xs / s / m / l / mobile

size=xs

  1. Completed step. Account
  2. Plan
  3. Confirm

size=s

  1. Completed step. Account
  2. Plan
  3. Confirm

size=m

  1. Completed step. Account
  2. Plan
  3. Confirm

size=l

  1. Completed step. Account
  2. Plan
  3. Confirm

size=mobile

  1. Completed step. Account
  2. Plan
  3. Confirm
tsx
<div style={{ ...col, gap: '2rem' }}>
      {SIZES.map((size) => (
        <section key={size} style={card}>
          <h3 style={heading}>size={size}</h3>
          <Steps
            size={size}
            steps={[
              { label: 'Account' },
              { label: 'Plan' },
              { label: 'Confirm' },
            ]}
            current={1}
          />
        </section>
      ))}
    </div>

API

Every prop is documented here directly from the component's TypeScript interface. Inherited DOM attributes (aria-*, onClick, style, etc.) work as usual but are omitted from this table.

PropTypeDefaultDescription
current*numberZero-indexed active step. Steps before it default to `completed`.
steps*readonly StepDefinition[]Step list. Must contain 1 or more entries.
allowSkipAheadbooleanIf true, ALL incomplete steps remain clickable even when they are after the active step. Defaults to `false` (Figma rule).
ariaLabelstringProgressAccessible label for the list landmark. Default `'Progress'`.
classNamestringAdditional classes merged onto the root.
onStepClick((index: number, step: StepDefinition) => void)Click handler. When supplied, every step's number circle becomes a `<button>`. Future-step navigation is blocked per the Figma rule ("Users cannot navigate to incomplete steps until the previous step has been completed").
orientationenumhorizontalVisual orientation. Default `'horizontal'`.
sizeenummVisual size. Default `'m'` (Figma desktop literal).
slotClassNameStepsSlotClassNamesPer-slot className overrides.

Styling

Three layers of customization, in order of escape-hatch strength: className overrides → data-attribute targeting → CSS custom properties.

Passing Tailwind classes

Every Oshon component accepts a className prop merged AFTER the component's default classes. Use it to override spacing, color, or size without forking the component.

tsx
<Steps
  className="ring-2 ring-offset-2 ring-blue-500"
/>

Data attributes

Oshon components expose their internal state as data-oshon-* attributes so you can target them from CSS without coupling to internal class names. The most common attributes are listed below — see the component's source for the full set.

AttributeValuesDescription
data-oshon-sizexs · s · m · l · mobileVisual size axis. Mirrors the `size` prop.
data-oshon-tierprimary · secondary · tertiaryVisual emphasis tier (Button family). Mirrors the `tier` prop.
data-oshon-stateenabled · active · error · disabledComponent surface state. Set automatically based on props.
data-disabledtrue · (omitted)Set when `disabled` is true. Pair with `:disabled` CSS for native input components.
data-stateopen · closed · checked · unchecked · …Radix-derived state for overlay components (Dialog, Tabs, Toggle, etc.).
css
/* Target the secondary tier specifically */
[data-oshon-tier="secondary"] {
  --oshon-color-primary-700: var(--my-brand-color);
}

Interactive states

Every interactive component supports the standard CSS pseudo- classes plus Tailwind's state variants. Focus rings always use :focus-visible so keyboard users see them but mouse users don't.

  • :hover / hover:* — pointer hover
  • :focus-visible / focus-visible:* — keyboard focus
  • :active / active:* — pressed
  • :disabled / disabled:* — set via the disabled prop

Anatomy

The named regions a consumer composes when rendering this component. Each is documented separately so you can target keyboard nav, ARIA labels, and slot props with precision.

steps

Required array of `{ label, state?, description?, number?, onClick?, disabled? }`. `label` is mandatory (Figma annotation). 1+ steps; the Figma authoring guidance recommends 3–8.

current

Required zero-indexed active step. Steps before it default to `completed`; step at it is `active`; steps after it are `incomplete`. Per-step `state` overrides the derived value.

onStepClick

Optional `(index, step) => void`. When supplied, each clickable step number becomes a `<button>`. The Figma rule blocks navigation to incomplete future steps unless `allowSkipAhead` is true; per-step `onClick` wins over this root callback.

slotClassName

Reach-through className overrides for `root`, `step`, `number`, `label`, `description`, `connector`. Each layer is keyed by `data-oshon-slot=…` so consumers can target without forking.

Keyboard

Renders `<ol role="list">` with each step as `<li>`, current step gets `aria-current="step"`. When `onStepClick` is supplied, each step number circle becomes a `<button>` reachable by Tab; future steps are non-clickable until the user has reached them (Figma rule). Completed and error states announce a visually-hidden "Completed step" / "Error in step" prefix so screen readers match the trailing icon affordance. Decorative connector + icons carry `aria-hidden="true"`. The list landmark accepts `ariaLabel` (default `"Progress"`).

Accessibility

Every Oshon component ships axe-clean. We test in CI on every PR and publish the audit log per component.

WCAG level
2.2 AA
Screen readers tested
VoiceOver (macOS), NVDA (Windows)
Last axe audit
2026-04-28

Do / Don't

✓ Do

Three-step horizontal wizard
<Steps
  steps={[
    { label: 'Account' },
    { label: 'Profile' },
    { label: 'Confirm' },
  ]}
  current={1}
/>
Interactive — back-navigation only
<Steps
  steps={steps}
  current={current}
  onStepClick={(idx) => setCurrent(idx)}
/>
Per-step error override
<Steps
  steps={[
    { label: 'Upload' },
    { label: 'Validate', state: 'error' },
    { label: 'Publish' },
  ]}
  current={1}
/>
Vertical with descriptions
<Steps
  orientation="vertical"
  steps={[
    { label: 'Plan', description: 'Pick a template' },
    { label: 'Build', description: 'Configure your settings' },
    { label: 'Ship' },
  ]}
  current={1}
/>

✗ Don't

Hardcoding the active color
<Steps slotClassName={{ number: 'border-blue-700 text-blue-700' }} ...

Breaks white-labeling. Active state paints `--oshon-color-primary-700`; theming the primary seed restyles every active circle in the system. Use `slotClassName` only for layout overrides (margin, width), not state colors.

Allowing free-form skip-ahead navigation
<Steps allowSkipAhead onStepClick={...} ...

The Figma annotation reads "Users cannot navigate to incomplete steps until the previous step has been completed." Skipping ahead is supported as an escape hatch (data-recovery flows) but should not be the default for new wizards — it lets the user submit out of order.

Steps as page navigation
<Steps steps={[{label:'Home'},{label:'Pricing'}]} ...

Steps render a temporal progression — "do A, then B, then C." Use `<Tabs>` for sibling sections that the user can revisit at will, or page-level navigation for unrelated destinations. Steps in a non-progressive context confuses screen readers (each step gets `aria-current="step"`).

Omitting the label
<Steps steps={[{number: 1}, {number: 2}]} ...

Figma authoring guidance ("Step label is required"). Without a label, screen readers announce only the number, the trailing icon affordance has no anchor, and the active step has no recognisable text. Always supply `label`.

Design rationale

Numbered-circle stepper, not a checkmark-only progress bar — the Figma authoring set (4 states × 2 styles) treats the step number as the primary affordance and the trailing check / error glyph as a secondary cue, satisfying WCAG SC 1.4.1 Use of Color via dual-channel feedback (number color + outline weight + glyph). Connector tone derives from the PRECEDING step (solid teal after completed; dashed gray everywhere else) so the rail reads as a left-to-right "fill-up" without an explicit progress bar layered behind it. Min 3 / Max 8 steps and the "label required" rule come from Figma frame 8617:1508 and are surfaced as TS types and a11y guidance rather than runtime asserts (the visual layer treats them as guidance the consumer can override). Default forward-navigation block matches the Figma annotation; `allowSkipAhead` is the explicit escape hatch.