Skip to content

Validation

You often need to ensure data is valid before allowing the user to proceed to the next step. ink-stepper supports both synchronous and asynchronous validation via the canProceed prop on <Step>.

Synchronous Validation

Pass a boolean to canProceed. If false, the "Enter" key (and goNext()) will be ignored.

tsx
function NameStep() {
  const [name, setName] = useState('');

  return (
    <Step name="Name" canProceed={name.length > 0}>
      <Text>Enter your name:</Text>
      <TextInput value={name} onChange={setName} />
      {name.length === 0 && <Text color="red">Name is required</Text>}
    </Step>
  );
}

Asynchronous Validation

You can also pass a function that returns a boolean or Promise<boolean>. This is useful for server-side checks or simulating API calls.

tsx
const checkServer = async () => {
  // simulate delay
  await new Promise(r => setTimeout(r, 1000));
  return true; 
};

<Step name="Server Check" canProceed={checkServer}>
  {({ isValidating }) => (
    <Box>
      <Text>Checking server status...</Text>
      {isValidating && <Spinner />}
    </Box>
  )}
</Step>

When validation is in progress:

  1. isValidating (from context) becomes true.
  2. Navigation is locked — goNext(), goBack() and goTo() are no-ops, as are the Enter/Escape keys.
  3. If the promise resolves to true, the stepper advances.
  4. If false, it stays on the current step.

Error Handling

An async validator can also fail outright — the network call rejects, the server returns garbage. If a canProceed (or an onExitStep) callback throws or rejects, the Stepper catches it, blocks the navigation, and reports it through the optional onError prop:

tsx
const checkServer = async () => {
  const response = await fetch('/api/validate'); // may reject
  return response.ok;
};

function App() {
  const [error, setError] = useState<string | null>(null);

  return (
    <Stepper onComplete={handleComplete} onError={(err) => setError(String(err))}>
      <Step name="Server Check" canProceed={checkServer}>
        {() => (
          <Box flexDirection="column">
            <Text>Press Enter to validate.</Text>
            {error && <Text color="red">{error}</Text>}
          </Box>
        )}
      </Step>
    </Stepper>
  );
}

onError receives the thrown value as unknown. If you omit the prop, the error is logged with console.error instead. Either way the rejection is handled, so it never escapes as an unhandled rejection — which would otherwise terminate the host process.

The user stays on the current step after an error, so they can retry.

For a live version of this, the Validate step of examples/wizard.tsx arms a throwing validator when you press e, then routes the failure through onError without crashing the wizard.

Released under the MIT License.