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

Usage

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

  1. 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.
  2. canPrev / canNext gate without unmounting. Disabled buttons stay mounted and visible at 40% opacity. Step layout never shifts as you move between edges of the sequence.
  3. 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.
  4. Tap feedback via shared tokens. Each button rides on TAP_SCALE + SPRINGS.snap from @craft-bits/core/motion, so the press feel matches every other action in the library. Reduced-motion users see no scale animation.
  5. 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.
  6. role="group" with an aria-label of "Step navigation" so screen readers announce the cluster of buttons as a single navigation control rather than three loose actions.

Props

PropTypeDefaultDescription
onPrev() => voidrequiredHandler for the previous-step button.
onNext() => voidrequiredHandler for the next-step button.
onSkip() => voidOptional skip handler. Omit to hide the skip button.
canPrevbooleantrueWhen false, the prev button is greyed out and onPrev is suppressed.
canNextbooleantrueWhen false, the next button is greyed out and onNext is suppressed.
labelReactNodeCenter label between prev and next. Pass null to hide.
prevLabelstring"Previous step"aria-label for the prev button.
nextLabelstring"Next step"aria-label for the next button.
skipLabelstring"Skip step"aria-label for the skip button.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • The root carries role="group" and aria-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 disabled and aria-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 transform only — never width / height / top / left. Under prefers-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 a StepThroughBag plus 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.