Skip to content

Basic Usage ​

The core of ink-stepper revolves around the <Stepper> container and <Step> components.

The Stepper Component ​

The <Stepper> component orchestrates the flow. It requires an onComplete callback, which triggers when the user presses Enter on the final step.

tsx
<Stepper
  onComplete={() => process.exit(0)}
  onCancel={() => process.exit(1)}
>
  {/* Steps go here */}
</Stepper>

Key Props ​

  • onComplete: Function called when the wizard finishes.
  • onCancel: Function called when the user presses Escape on the first step.
  • initialStep: (Optional) The index of the step to start on in uncontrolled mode (default: 0). Ignored when the controlled step prop is provided.
  • onError: (Optional) Called when an async canProceed or onExitStep throws. See Validation.

Defining Steps ​

Use the <Step> component to define each page of your wizard. Every step needs a unique name which is displayed in the progress bar.

tsx
<Step name="Configuration">
  <Text>Step content goes here.</Text>
</Step>

Accessing Step Context ​

If you need to programmatically control navigation (e.g., from your own key binding instead of just pressing Enter), you can use the function-as-child pattern to access StepContext. Ink has no click targets, so wire the control up with useInput:

tsx
import { Box, Text, useInput } from 'ink';

function NextHint({ onNext }: { onNext: () => void }) {
  useInput((input) => {
    if (input === 'n') onNext();
  });

  return <Text dimColor>Press "n" to continue.</Text>;
}

<Step name="Manual Control">
  {({ goNext }) => (
    <Box flexDirection="column">
      <Text>Custom controls:</Text>
      <NextHint onNext={goNext} />
    </Box>
  )}
</Step>

The context provides:

  • goNext(): Advance to the next step (respects canProceed).
  • goBack(): Return to the previous step (cancels the wizard from the first step).
  • goTo(index): Jump to a specific step. The index is clamped to the valid range, and unlike goNext this skips canProceed — it is a raw jump. It still fires the full lifecycle, see Lifecycle Hooks.
  • cancel(): Cancel the wizard (calls onCancel).
  • isFirst, isLast: Boolean flags for current position.
  • currentStep, totalSteps: Numeric indicators.
  • isValidating: true while an async canProceed is running.

goNext(), goBack() and goTo() are all no-ops while validation is in flight or while navigation has been disabled via useStepperInput.

Keyboard Navigation ​

Keyboard navigation is on by default:

  • Enter — advance to the next step (subject to canProceed).
  • Escape — go back to the previous step; on the first step it cancels the wizard and calls onCancel.

Turn it off with keyboardNav={false} if your steps handle all input themselves:

tsx
<Stepper onComplete={handleComplete} keyboardNav={false}>
  {/* ... */}
</Stepper>

Both keys are ignored while an async canProceed is running and while navigation has been disabled via useStepperInput.

Conditional Steps ​

Steps may be wrapped in components or rendered conditionally. They are ordered by their position in the element tree, not by the time they mounted, so a step toggled on later slots into its JSX position:

tsx
<Stepper onComplete={handleComplete}>
  <Step name="Account">{/* ... */}</Step>
  {needsBilling && <Step name="Billing">{/* ... */}</Step>}
  <Step name="Review">{/* ... */}</Step>
</Stepper>

In uncontrolled mode the user stays on the same step when another step is inserted or removed elsewhere — the active step is tracked by identity, not by index — and no lifecycle callbacks fire for that repair. If the active step itself is removed, the index is kept, so whichever step slides into that position becomes active; it only clamps to the last remaining step when the removed step was the last one.

WARNING

A <Step> must not be nested inside another <Step>; that breaks the tree-order guarantee. Wrapper components, fragments, and conditionals around a <Step> are fine.

Released under the MIT License.