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

Style=Regular — full step + connector

  1. Incomplete Step
  2. Next
  1. Active Step
  2. Next
  1. Completed step. Completed Step
  2. Next
  1. Error in step. Error
  2. Next
Each row pairs the named state with a trailing incomplete sibling so the connector tone (dashed gray vs. solid teal) renders authentically.

Style=End — last step, no connector

  1. Last Incomplete
  1. Last Active
  1. Completed step. Last Completed
  1. Error in step. Last Error
tsx
<div style={{ ...col, gap: '2rem' }}>
      <section style={card}>
        <h3 style={heading}>Style=Regular — full step + connector</h3>
        <div style={{ ...col, gap: '20px' }}>
          <Steps
            steps={[
              { label: 'Incomplete Step' },
              { label: 'Next' },
            ]}
            current={0}
          />
          <Steps
            steps={[
              { label: 'Active Step' },
              { label: 'Next' },
            ]}
            current={0}
          />
          <Steps
            steps={[
              { label: 'Completed Step' },
              { label: 'Next' },
            ]}
            current={1}
          />
          <Steps
            steps={[
              { label: 'Error', state: 'error' },
              { label: 'Next' },
            ]}
            current={0}
          />
        </div>
        <div style={caption}>
          Each row pairs the named state with a trailing incomplete sibling
          so the connector tone (dashed gray vs. solid teal) renders
          authentically.
        </div>
      </section>

      <section style={card}>
        <h3 style={heading}>Style=End — last step, no connector</h3>
        <div style={{ ...col, gap: '20px' }}>
          <Steps steps={[{ label: 'Last Incomplete' }]} current={-1} />
          <Steps steps={[{ label: 'Last Active' }]} current={0} />
          <Steps steps={[{ label: 'Last Completed' }]} current={1} />
          <Steps
            steps={[{ label: 'Last Error', state: 'error' }]}
            current={0}
          />
        </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.