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.
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.
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:
isValidating(from context) becomestrue.- Navigation is locked —
goNext(),goBack()andgoTo()are no-ops, as are the Enter/Escape keys. - If the promise resolves to
true, the stepper advances. - 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:
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.