Interactive Prompts (Inquirer)
Not every input fits on the command line. Scaffolding tools (npm init, create-vite) and setup wizards ask questions interactively — text prompts, yes/no confirms, single- and multi-select menus, masked password entry. Inquirer is the most popular library for this in Node, providing a rich set of prompt types with arrow-key navigation, validation, and conditional questions. This page covers the common prompt types, validation and defaults, conditional flows, and — crucially — when interactive prompts help versus when they break automation, plus how to combine them with flags so your tool works both ways.
Asking questions
Bash
npm install @inquirer/prompts
JS
import { input, confirm, select, checkbox, password } from '@inquirer/prompts'
const name = await input({ message: 'Project name:', default: 'my-app' })
const useTs = await confirm({ message: 'Use TypeScript?', default: true })
const manager = await select({
message: 'Package manager:',
choices: [
{ name: 'npm', value: 'npm' },
{ name: 'pnpm', value: 'pnpm' },
{ name: 'yarn', value: 'yarn' },
],
})
const features = await checkbox({
message: 'Add features:',
choices: [
{ name: 'ESLint', value: 'eslint' },
{ name: 'Prettier', value: 'prettier' },
{ name: 'Vitest', value: 'vitest' },
],
})
const token = await password({ message: 'API token:', mask: '*' })Each prompt is an async function returning the answer — text, confirm, select, checkbox, and password cover most needs
The modern `@inquirer/prompts` package exposes each prompt type as its own **async function** that resolves to the user's answer, so a wizard reads as a straightforward sequence of `await`s. The core types cover almost everything: **`input`** for free text (with an optional `default`), **`confirm`** for yes/no booleans, **`select`** for choosing *one* option from an arrow-key menu, **`checkbox`** for selecting *several*, and **`password`** for masked secret entry. Choices can be simple strings or `{ name, value }` objects (display label vs returned value). Because each prompt is just an awaited function, you compose, branch, and loop over them with ordinary JavaScript — no special flow DSL needed.
Validation and defaults
JS
const email = await input({
message: 'Email:',
validate: (value) =>
/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value) || 'Please enter a valid email',
// return true to accept, or a STRING error message to reject & re-ask
})
const port = await input({
message: 'Port:',
default: '3000',
validate: (v) => (Number(v) > 0 && Number(v) < 65536) || 'Port must be 1–65535',
})A `validate` function returns `true` to accept or an error string to reject and re-prompt — input is checked inline
Each prompt accepts a **`validate`** function that runs on the entered value: return `true` to accept it, or return a **string** to reject it — the string is shown as an inline error and the user is asked again, so they can't proceed with bad input. This keeps validation right next to the question and gives immediate, friendly feedback (far better than accepting garbage and failing later). Combine it with **`default`** values so pressing Enter takes a sensible option, and with `transformer`/filter hooks to normalize input (trim, lowercase) before it's returned. Good prompts validate eagerly and default generously — the user should be able to breeze through a wizard by hitting Enter and only stop where a real decision is required.
Conditional and dynamic questions
JS
// Because prompts are just awaited functions, branching is plain JS — no special API:
const useDb = await confirm({ message: 'Add a database?', default: false })
if (useDb) {
const engine = await select({
message: 'Which database?',
choices: [
{ name: 'PostgreSQL', value: 'postgres' },
{ name: 'MongoDB', value: 'mongo' },
],
})
const url = await input({ message: `${engine} connection URL:` })
// ...only asked when the user opted into a database
}Conditional flows are just `if`/`for` around `await` — ask follow-up questions only when earlier answers warrant them
A good wizard doesn't ask irrelevant questions. Because each prompt is an awaited function returning a value, **conditional flows are ordinary control flow**: wrap follow-up prompts in an `if` based on a previous answer, loop with `for`/`while` to collect a variable number of items, or compute a later prompt's `choices` from earlier answers. (Older Inquirer used a declarative `when` callback for this; the modern async API makes it plain JavaScript.) This keeps the experience focused — a user who answers "no database" never sees the connection-URL question — and keeps your code readable as a normal sequence of steps rather than a config object you have to mentally execute.
When prompts help — and when they break things
Interactive prompts hang in CI and non-TTY environments — always provide a non-interactive path via flags
Interactive prompts are wonderful for humans at a terminal and **disastrous for automation**. A script, CI pipeline, Docker build, or piped invocation has no interactive terminal (no TTY) and no human to answer — so a prompt either **hangs forever** waiting for input or crashes, breaking the build. The rule: prompts must be **optional, not mandatory**. Detect whether you're attached to an interactive terminal with `process.stdout.isTTY` (and check for a `--yes`/`--ci` flag), and when you're *not* interactive, take values from [command-line flags](/nodejs/commander) and defaults instead of prompting — or fail with a clear "missing required option" message. The best CLIs work **both ways**: `create-app --name foo --template ts` runs fully non-interactively, while bare `create-app` falls back to a friendly wizard. Never make a prompt the *only* way to provide a value.
Combining flags and prompts
JS
// Pattern: a flag value wins; prompt ONLY for what's missing AND only if interactive.
async function resolveName(flagValue) {
if (flagValue) return flagValue // provided non-interactively → use it
if (!process.stdout.isTTY) {
console.error('Error: --name is required in non-interactive mode')
process.exit(1) // fail loudly, don't hang
}
return input({ message: 'Project name:' }) // interactive → ask
}
const name = await resolveName(options.name) // options from CommanderLet flags override prompts: use a provided flag, otherwise prompt if interactive, otherwise fail — best of both worlds
The production pattern marries [Commander](/nodejs/commander) flags with Inquirer prompts so one tool serves both humans and scripts. For each value: if a **flag** supplied it, use that and skip the question; if it's missing *and* you're in an **interactive** terminal, **prompt** for it; if it's missing and you're **non-interactive**, **fail fast** with a clear error (never hang). This gives a smooth wizard for first-time human users (`create-app` → answer questions) and full scriptability for everyone else (`create-app --name x --template ts --yes`). A `--yes`/`-y` flag that accepts all defaults is a common nicety. Designing for both modes from the start is what lets your CLI be both friendly *and* automatable.
Next
Get your finished tool into users' hands: [Publishing a CLI Tool](/nodejs/publishing-cli).