Preview
- Completed step. Identity
- Completed step. Permissions
- Audit hooks
- Review
Installation
Install the runtime packages:
pnpm add @oshon-ai/components @oshon-ai/tokens @oshon-ai/primitives
Or scaffold the component source directly into your codebase (shadcn-style):
pnpm dlx @oshon-ai/cli add steps
Wire the tokens into your Tailwind v4 stylesheet:
/* 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.
'use client';
import { Steps } from '@oshon-ai/components';
export default function Example() {
return <Steps />;
}State overview — Figma 8587:61207
Style=Regular — full step + connector
- Incomplete Step
- Next
- Active Step
- Next
- Completed step. Completed Step
- Next
- Error in step. Error
- Next
Style=End — last step, no connector
- Last Incomplete
- Last Active
- Completed step. Last Completed
- Error in step. Last Error
<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
- Completed step. Account
- Profile
- Plan
- Payment
- Confirm
<WizardDemo />
Skip-ahead — allowSkipAhead escape hatch
- Account
- Profile
- Plan
- Payment
- Confirm
allowSkipAhead every circle is clickable, including future incomplete steps. Use sparingly — the default forward-block matches the Figma annotation.<SkipAheadDemo />
Error override — per-step state
- Completed step. Upload
- Error in step. Validate
- Publish
state: 'error' and leave current on it. The user sees both the failure cue and the next-action affordance in one rail.<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
- Completed step. Connect a workspaceLink your Slack or Teams account to start syncing
- Invite teammatesSend up to 10 invites — they keep their seats
- Pick a planFree for the first 14 days, no card required
- Launch
<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
- Completed step. Account
- Plan
- Confirm
size=s
- Completed step. Account
- Plan
- Confirm
size=m
- Completed step. Account
- Plan
- Confirm
size=l
- Completed step. Account
- Plan
- Confirm
size=mobile
- Completed step. Account
- Plan
- Confirm
<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.
| Prop | Type | Default | Description |
|---|---|---|---|
current* | number | — | Zero-indexed active step. Steps before it default to `completed`. |
steps* | readonly StepDefinition[] | — | Step list. Must contain 1 or more entries. |
allowSkipAhead | boolean | — | If true, ALL incomplete steps remain clickable even when they are after the active step. Defaults to `false` (Figma rule). |
ariaLabel | string | Progress | Accessible label for the list landmark. Default `'Progress'`. |
className | string | — | Additional 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"). |
orientation | enum | horizontal | Visual orientation. Default `'horizontal'`. |
size | enum | m | Visual size. Default `'m'` (Figma desktop literal). |
slotClassName | StepsSlotClassNames | — | Per-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.
<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.
| Attribute | Values | Description |
|---|---|---|
data-oshon-size | xs · s · m · l · mobile | Visual size axis. Mirrors the `size` prop. |
data-oshon-tier | primary · secondary · tertiary | Visual emphasis tier (Button family). Mirrors the `tier` prop. |
data-oshon-state | enabled · active · error · disabled | Component surface state. Set automatically based on props. |
data-disabled | true · (omitted) | Set when `disabled` is true. Pair with `:disabled` CSS for native input components. |
data-state | open · closed · checked · unchecked · … | Radix-derived state for overlay components (Dialog, Tabs, Toggle, etc.). |
/* 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 thedisabledprop
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.
stepsRequired array of `{ label, state?, description?, number?, onClick?, disabled? }`. `label` is mandatory (Figma annotation). 1+ steps; the Figma authoring guidance recommends 3–8.
currentRequired 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.
onStepClickOptional `(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.
slotClassNameReach-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
<Steps
steps={[
{ label: 'Account' },
{ label: 'Profile' },
{ label: 'Confirm' },
]}
current={1}
/><Steps
steps={steps}
current={current}
onStepClick={(idx) => setCurrent(idx)}
/><Steps
steps={[
{ label: 'Upload' },
{ label: 'Validate', state: 'error' },
{ label: 'Publish' },
]}
current={1}
/><Steps
orientation="vertical"
steps={[
{ label: 'Plan', description: 'Pick a template' },
{ label: 'Build', description: 'Configure your settings' },
{ label: 'Ship' },
]}
current={1}
/>✗ Don't
<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.
<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 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"`).
<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.