Step Nav Controls
The navigation footer for a multi-step interaction. Three buttons in a row — previous, optional skip, next — with an optional center label. The parent owns step state and decides what canPrev and canNext mean.
Reach for it when an animated visual or article needs a forward/back/skip control strip. Distinct from a full stepper (which owns step state, progress bar, gates, and replay). This primitive is purely the navigation footer — three buttons and a label, nothing more.
Step 1 / 5
Customize
Step
3 / 5
Chrome
Installation
npx shadcn@latest add https://craftbits.dev/r/step-nav-controls.jsonUsage
import { StepNavControls } from "@craft-bits/core";
<StepNavControls
onPrev={prev}
onNext={next}
canPrev={step > 0}
canNext={step < total - 1}
label="Step 3 / 7"
/>Surface a skip control by passing onSkip. Omit it on lessons that should not let the reader skip:
<StepNavControls
onPrev={prev}
onNext={next}
onSkip={skip}
canPrev={canGoBack}
canNext={hasAnsweredGate}
label={centerLabel}
/>Hide the center label entirely by passing label={null}:
<StepNavControls onPrev={prev} onNext={next} label={null} />Understanding the component
- Three roles, three tones. Previous is neutral, skip (when present) is neutral, and next uses the accent tone — so the eye lands on the forward direction. The accent next button is the implicit primary action.
canPrev/canNextgate without unmounting. Disabled buttons stay mounted and visible at 40% opacity. Step layout never shifts as you move between edges of the sequence.- Skip is opt-in. The skip button only renders when the parent passes
onSkip. Default lessons never accidentally surface a skip affordance — strict sequences omit the prop and get a clean prev/next pair. - Tap feedback via shared tokens. Each button rides on
TAP_SCALE+SPRINGS.snapfrom@craft-bits/core/motion, so the press feel matches every other action in the library. Reduced-motion users see no scale animation. - 44×44 minimum. Every button is at least
h-11 min-w-11— clearing WCAG 2.5.8 AAA touch-target floor for the lesson context where it ships. role="group"with anaria-labelof"Step navigation"so screen readers announce the cluster of buttons as a single navigation control rather than three loose actions.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
onPrev | () => void | required | Handler for the previous-step button. |
onNext | () => void | required | Handler for the next-step button. |
onSkip | () => void | — | Optional skip handler. Omit to hide the skip button. |
canPrev | boolean | true | When false, the prev button is greyed out and onPrev is suppressed. |
canNext | boolean | true | When false, the next button is greyed out and onNext is suppressed. |
label | ReactNode | — | Center label between prev and next. Pass null to hide. |
prevLabel | string | "Previous step" | aria-label for the prev button. |
nextLabel | string | "Next step" | aria-label for the next button. |
skipLabel | string | "Skip step" | aria-label for the skip button. |
className | string | — | Merged onto the root <div> via cn(). |
Accessibility
- The root carries
role="group"andaria-label="Step navigation"so the three buttons are announced as one navigation cluster. - Every button has an explicit
aria-label(customisable for localisation) — icon-only buttons never rely on visible text for their accessible name. - Disabled prev/next buttons set
disabledandaria-disabled(via the native attribute) and suppress their click handlers — keyboard and pointer interaction both no-op. - Touch targets meet WCAG 2.5.8 AAA: every button is at least 44 × 44 px (
h-11 min-w-11). - Tap animation is
transformonly — neverwidth/height/top/left. Underprefers-reduced-motion: reduce, the scale is skipped entirely. - Focus state uses a 2px ring on the accent token at a 2px offset from the surface for clear keyboard-focus visibility on both light and dark backgrounds.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/chrome/StepNavControls.tsx). The source coupled the nav row to aStepThroughBagplus a prediction-gate state machine and an optional progress bar — all four screens (replay / gate / custom / default) lived inside one component. craft-bits' version strips that scope to the navigation footer alone: prev, optional skip, next, and an optional label. Gates, replay, and progress live in sibling primitives so each can be composed independently.