{"id":35727104,"url":"https://github.com/archcorsair/ink-stepper","last_synced_at":"2026-01-20T17:35:01.388Z","repository":{"id":331289521,"uuid":"1126020496","full_name":"archcorsair/ink-stepper","owner":"archcorsair","description":"Step-by-step wizard component for Ink terminal applications.","archived":false,"fork":false,"pushed_at":"2026-01-03T00:02:31.000Z","size":96,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-05T16:48:31.582Z","etag":null,"topics":["ink","step-by-step","stepper","terminal","tui","wizard"],"latest_commit_sha":null,"homepage":"https://archcorsair.github.io/ink-stepper/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/archcorsair.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-31T22:20:34.000Z","updated_at":"2026-01-03T00:02:34.000Z","dependencies_parsed_at":"2026-01-06T05:05:05.785Z","dependency_job_id":null,"html_url":"https://github.com/archcorsair/ink-stepper","commit_stats":null,"previous_names":["archcorsair/ink-stepper"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/archcorsair/ink-stepper","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archcorsair%2Fink-stepper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archcorsair%2Fink-stepper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archcorsair%2Fink-stepper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archcorsair%2Fink-stepper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/archcorsair","download_url":"https://codeload.github.com/archcorsair/ink-stepper/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archcorsair%2Fink-stepper/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28234543,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2026-01-07T02:00:05.975Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ink","step-by-step","stepper","terminal","tui","wizard"],"created_at":"2026-01-06T09:13:25.974Z","updated_at":"2026-01-07T10:00:50.880Z","avatar_url":"https://github.com/archcorsair.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ink-stepper\n\nStep-by-step wizard component for [Ink](https://github.com/vadimdemedes/ink) terminal applications.\n\n```\n━━━━━ ✓ ━━━━━━━━━━ ✓ ━━━━━━━━━━●━━━━━━━━━━○━━━━━\n    Theme       Directory     Review      Done\n\n┌─────────────────────────────────────────────────┐\n│                                                 │\n│  Review your selections:                        │\n│                                                 │\n│    Theme: Dark                                  │\n│    Directory: ~/projects                        │\n│                                                 │\n│  Press Enter to continue, Escape to go back     │\n│                                                 │\n└─────────────────────────────────────────────────┘\n```\n\n## Installation\n\n```bash\n# npm\nnpm install ink-stepper\n\n# jsr\nnpx jsr add @archcorsair/ink-stepper\n\n# pnpm\npnpm add ink-stepper\n\n# bun\nbun add ink-stepper\n```\n\n## Usage\n\nFull documentation available here: https://archcorsair.github.io/ink-stepper/\n\n```tsx\nimport { Stepper, Step } from \"ink-stepper\";\nimport { Text } from \"ink\";\n\nfunction App() {\n  return (\n    \u003cStepper onComplete={() =\u003e process.exit(0)} onCancel={() =\u003e process.exit(1)}\u003e\n      \u003cStep name=\"Theme\"\u003e\n        \u003cThemeSelector /\u003e\n      \u003c/Step\u003e\n      \u003cStep name=\"Directory\" canProceed={pathIsValid}\u003e\n        {({ goNext, goBack }) =\u003e (\n          \u003cPathInput onConfirm={goNext} onBack={goBack} /\u003e\n        )}\n      \u003c/Step\u003e\n      \u003cStep name=\"Review\"\u003e\n        {({ goBack, isLast }) =\u003e (\n          \u003cReview onBack={goBack} showFinish={isLast} /\u003e\n        )}\n      \u003c/Step\u003e\n    \u003c/Stepper\u003e\n  );\n}\n```\n\n## API\n\n### `\u003cStepper\u003e`\n\nMain container component that orchestrates step navigation.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | required | Step elements |\n| `onComplete` | `() =\u003e void` | required | Called when advancing past the last step |\n| `onCancel` | `() =\u003e void` | - | Called when canceling (Escape on first step or `cancel()`) |\n| `onStepChange` | `(step: number) =\u003e void` | - | Called when current step changes (zero-based index) |\n| `onEnterStep` | `(step: number) =\u003e void` | - | Called after entering a step |\n| `onExitStep` | `(step: number) =\u003e void \\| boolean \\| Promise\u003cboolean\u003e` | - | Called before leaving a step (return `false` to cancel) |\n| `step` | `number` | - | Controlled step index (zero-based) |\n| `keyboardNav` | `boolean` | `true` | Enable Enter/Escape navigation |\n| `showProgress` | `boolean` | `true` | Show the progress bar |\n| `renderProgress` | `(ctx: ProgressContext) =\u003e ReactNode` | - | Custom progress bar renderer |\n| `markers` | `StepperMarkers` | - | Custom progress bar markers |\n\n### `\u003cStep\u003e`\n\nMarker component for defining individual steps.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `name` | `string` | required | Display name in progress bar |\n| `canProceed` | `boolean \\| (() =\u003e boolean \\| Promise\u003cboolean\u003e)` | `true` | Whether navigation to next step is allowed (supports async) |\n| `children` | `ReactNode \\| (ctx: StepContext) =\u003e ReactNode` | required | Step content |\n\n### StepContext\n\nContext passed to step content when using the render function pattern:\n\n```tsx\ninterface StepContext {\n  goNext: () =\u003e void;           // Navigate to next step (respects canProceed)\n  goBack: () =\u003e void;           // Navigate to previous step\n  goTo: (step: number) =\u003e void; // Jump to specific step (zero-based)\n  cancel: () =\u003e void;           // Cancel the wizard\n  currentStep: number;          // Current step index (zero-based)\n  totalSteps: number;           // Total number of steps\n  isFirst: boolean;             // Whether this is the first step\n  isLast: boolean;              // Whether this is the last step\n  isValidating: boolean;        // Whether async validation is in progress\n}\n```\n\n### ProgressContext\n\nContext passed to custom progress bar renderer:\n\n```tsx\ninterface ProgressContext {\n  currentStep: number;\n  steps: Array\u003c{\n    name: string;\n    completed: boolean;\n    current: boolean;\n  }\u003e;\n}\n```\n\n## Keyboard Navigation\n\nBy default, keyboard navigation is enabled:\n- **Enter** - Advance to next step (if `canProceed` is true)\n- **Escape** - Go back (or cancel if on first step)\n\nDisable with `keyboardNav={false}`.\n\n## Validation\n\n### Synchronous Validation\n\nControl navigation with the `canProceed` prop:\n\n```tsx\nfunction App() {\n  const [isValid, setIsValid] = useState(false);\n\n  return (\n    \u003cStepper onComplete={handleComplete}\u003e\n      \u003cStep name=\"Input\" canProceed={isValid}\u003e\n        {({ goNext }) =\u003e (\n          \u003cTextInput\n            onChange={(value) =\u003e setIsValid(value.length \u003e 0)}\n            onSubmit={goNext}\n          /\u003e\n        )}\n      \u003c/Step\u003e\n    \u003c/Stepper\u003e\n  );\n}\n```\n\n### Async Validation\n\n`canProceed` supports async functions for server-side validation:\n\n```tsx\nfunction App() {\n  const validateEmail = async () =\u003e {\n    const response = await fetch(`/api/validate?email=${email}`);\n    return response.ok;\n  };\n\n  return (\n    \u003cStepper onComplete={handleComplete}\u003e\n      \u003cStep name=\"Email\" canProceed={validateEmail}\u003e\n        {({ goNext, isValidating }) =\u003e (\n          \u003cBox flexDirection=\"column\"\u003e\n            \u003cTextInput value={email} onChange={setEmail} /\u003e\n            {isValidating \u0026\u0026 \u003cText color=\"yellow\"\u003eValidating...\u003c/Text\u003e}\n            \u003cButton onPress={goNext} disabled={isValidating}\u003e\n              Continue\n            \u003c/Button\u003e\n          \u003c/Box\u003e\n        )}\n      \u003c/Step\u003e\n    \u003c/Stepper\u003e\n  );\n}\n```\n\nThe `isValidating` flag in StepContext is `true` while async validation is running, allowing you to show loading states.\n\n## Lifecycle Hooks\n\n### onEnterStep / onExitStep\n\nExecute logic when entering or leaving steps:\n\n```tsx\n\u003cStepper\n  onComplete={handleComplete}\n  onEnterStep={(step) =\u003e {\n    analytics.track(`entered_step_${step}`);\n  }}\n  onExitStep={async (step) =\u003e {\n    // Save draft before leaving\n    await saveDraft(step);\n    return true; // Allow navigation\n  }}\n\u003e\n  ...\n\u003c/Stepper\u003e\n```\n\n`onExitStep` can return `false` (sync or async) to cancel navigation:\n\n```tsx\n\u003cStepper\n  onComplete={handleComplete}\n  onExitStep={(step) =\u003e {\n    if (hasUnsavedChanges) {\n      return confirm(\"Discard changes?\");\n    }\n    return true;\n  }}\n\u003e\n  ...\n\u003c/Stepper\u003e\n```\n\n## Input Coordination\n\nWhen steps contain interactive inputs (TextInput, Select, etc.), use `useStepperInput` to prevent keyboard conflicts:\n\n```tsx\nimport { useStepperInput } from \"ink-stepper\";\n\nfunction EmailInput() {\n  const { disableNavigation, enableNavigation } = useStepperInput();\n  const [value, setValue] = useState(\"\");\n\n  return (\n    \u003cTextInput\n      value={value}\n      onChange={setValue}\n      onFocus={disableNavigation}  // Disable Enter/Escape handling\n      onBlur={enableNavigation}    // Re-enable when done\n    /\u003e\n  );\n}\n```\n\nThis prevents Enter from advancing the step while the user is typing.\n\n## Controlled Mode\n\nFor external state management, use the `step` prop:\n\n```tsx\nfunction App() {\n  const [currentStep, setCurrentStep] = useState(0);\n\n  return (\n    \u003cStepper\n      step={currentStep}\n      onStepChange={setCurrentStep}\n      onComplete={handleComplete}\n    \u003e\n      \u003cStep name=\"One\"\u003e...\u003c/Step\u003e\n      \u003cStep name=\"Two\"\u003e...\u003c/Step\u003e\n    \u003c/Stepper\u003e\n  );\n}\n```\n\n## Wrapped \u0026 Nested Steps\n\nSteps can be wrapped in custom components, fragments, or conditional logic:\n\n```tsx\nconst StepGroup = ({ children }) =\u003e \u003c\u003e{children}\u003c/\u003e;\n\n\u003cStepper onComplete={handleComplete}\u003e\n  \u003cStepGroup\u003e\n    \u003cStep name=\"Wrapped\"\u003e\n      \u003cText\u003eThis works!\u003c/Text\u003e\n    \u003c/Step\u003e\n  \u003c/StepGroup\u003e\n  {showOptional \u0026\u0026 (\n    \u003cStep name=\"Optional\"\u003e\n      \u003cText\u003eConditional step\u003c/Text\u003e\n    \u003c/Step\u003e\n  )}\n\u003c/Stepper\u003e\n```\n\n## Custom Progress Bar\n\n### Custom Markers\n\nCustomize the progress bar markers without replacing the entire component:\n\n```tsx\n\u003cStepper\n  onComplete={handleComplete}\n  markers={{ completed: \"[X]\", current: \"[\u003e]\", pending: \"[ ]\" }}\n\u003e\n  ...\n\u003c/Stepper\u003e\n```\n\nDefault markers: `✓` (completed), `●` (current), `○` (pending)\n\n### Custom Renderer\n\nFull control over progress bar rendering:\n\n```tsx\n\u003cStepper\n  onComplete={handleComplete}\n  renderProgress={({ currentStep, steps }) =\u003e (\n    \u003cText\u003e\n      Step {currentStep + 1} of {steps.length}: {steps[currentStep].name}\n    \u003c/Text\u003e\n  )}\n\u003e\n  ...\n\u003c/Stepper\u003e\n```\n\n## Advanced: useStepperContext\n\nFor advanced use cases, access the full stepper context:\n\n```tsx\nimport { useStepperContext } from \"ink-stepper\";\n\nfunction CustomStepContent() {\n  const { stepContext, currentStepId } = useStepperContext();\n\n  return (\n    \u003cBox\u003e\n      \u003cText\u003eStep {stepContext.currentStep + 1}\u003c/Text\u003e\n      \u003cButton onPress={stepContext.goNext}\u003eNext\u003c/Button\u003e\n    \u003c/Box\u003e\n  );\n}\n```\n\n## Exports\n\n```tsx\n// Components\nexport { Stepper, Step } from \"ink-stepper\";\n\n// Hooks\nexport { useStepperContext, useStepperInput } from \"ink-stepper\";\n\n// Types\nexport type {\n  StepperProps,\n  StepProps,\n  StepContext,\n  ProgressContext,\n  StepperMarkers,\n  StepperContextValue,\n  RegisteredStep,\n  UseStepperInputReturn,\n} from \"ink-stepper\";\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farchcorsair%2Fink-stepper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farchcorsair%2Fink-stepper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farchcorsair%2Fink-stepper/lists"}