inquirer
Inquirer is the library that draws the interactive questions in Node command line tools: the arrow-key list, the space-bar checkbox, the masked password field, the y/N confirm. You describe a session as an array of question objects, each with a type, a name, and a message, hand the array to inquirer.prompt(), and get back a promise resolving to an answers object keyed by those names. Questions can react to earlier answers through when, default, and choices callbacks, and can reject bad input through validate. Since version 9 the package is a compatibility layer over the newer @inquirer/* packages, which the same author now develops instead.
Keep it if you already have a large declarative question array and no appetite for a rewrite; it still works and still gets fixes. For new tools, install @inquirer/prompts instead, exactly as this package's own README asks you to.
Use it if
- You maintain an existing tool built on inquirer 8 or 9 and want current dependencies without rewriting a hundred-question array
- Your flow is genuinely declarative: one array of questions where later ones appear or disappear based on when callbacks against the answers hash, which the newer API deliberately dropped
- You rely on a third-party prompt type registered by name through registerPrompt, such as autocomplete or a file picker
- You feed questions from an RxJS-compatible observable because the next question depends on work happening while the user answers
- You are starting something new: the README's first paragraph says this is the legacy version, that it is not actively developed, and points you at @inquirer/prompts, which is smaller and gets the actual work
- Your tool is CommonJS: the package is ESM only, so require('inquirer') fails and you either convert the project, use a dynamic import(), or stay pinned on inquirer 8
- Your CI or users are on an older Node: the engines field is ^20.17.0 || ^22.13.0 || >=23.5.0, so Node 22.0 through 22.12 is excluded and installs there warn or fail on engine checks
- You want a light dependency: around 194 KB gzipped and six direct dependencies to ask one yes-or-no question, where @inquirer/confirm on its own is a rounding error next to that
- You need to run unattended: prompt() requires a real TTY and rejects otherwise, so every question needs a matching CLI flag or environment variable before the tool is usable in CI
Setup reality
npm install inquirer is quick, but two things stop the first import. The package is ESM only, so your package.json needs type: module or you call it through a dynamic import() from CommonJS. The engine range is unusually narrow, ^20.17.0 || ^22.13.0 || >=23.5.0, and a CI image pinned to Node 22.11 will complain. Types are bundled, and @types/node is an optional peer dependency rather than a hard one. Separator lives on the default export, not as a named export, so import inquirer from 'inquirer' and use inquirer.Separator. After that the friction moves to terminals: the README lists nodemon printing arrow-key garbage unless you pass --no-stdin, grunt-exec needing stdio inherit, and Windows network streams hanging the process. Plan the non-TTY path early, because a prompt in a CI script fails loudly.
Patterns
Ask a set of questions and read the answersbasic-question-array
import inquirer from 'inquirer';
const answers = await inquirer.prompt([
{ type: 'input', name: 'name', message: 'Project name:' },
{ type: 'confirm', name: 'typescript', message: 'Use TypeScript?', default: true },
]);
console.log(answers.name, answers.typescript);The name field becomes the key in the answers object, and a name containing dots creates a nested path. Top-level await needs an ESM entry point, which this package requires anyway.
Offer a single choice from a listlist-prompt
const { manager } = await inquirer.prompt([{
type: 'list',
name: 'manager',
message: 'Package manager:',
choices: [
{ name: 'npm (default)', value: 'npm' },
{ name: 'pnpm', value: 'pnpm', short: 'pnpm' },
],
default: 'npm',
}]);name is what the user sees, value is what lands in the answers, and short is what stays on screen after selection. default takes the value or the index, not the display name.
Multi-select with grouped optionscheckbox-with-separator
const { features } = await inquirer.prompt([{
type: 'checkbox',
name: 'features',
message: 'Include:',
choices: [
new inquirer.Separator('-- Testing --'),
{ name: 'Vitest', value: 'vitest', checked: true },
new inquirer.Separator('-- Linting --'),
{ name: 'ESLint', value: 'eslint' },
],
}]);Separator hangs off the default export, so a named import of it fails. Checkbox answers come back as an array that is empty when the user selects nothing, so validate if at least one is required.
Skip questions based on earlier answersconditional-questions
await inquirer.prompt([
{ type: 'confirm', name: 'deploy', message: 'Configure deployment?', default: false },
{
type: 'input',
name: 'host',
message: 'Deploy host:',
when: (answers) => answers.deploy,
},
]);This branching is the main reason to stay on this package: @inquirer/prompts has no when, so the same flow becomes explicit if statements between await calls. A skipped question leaves its key undefined rather than null.
Reject bad input and normalise good inputvalidate-and-filter
{
type: 'input',
name: 'port',
message: 'Port:',
validate: (value) =>
/^\d+$/.test(value) ? true : 'Port must be a number',
filter: (value) => Number(value),
}validate returns true to accept or a string to display as the error; returning false gives a generic message. filter runs after validate and its result is what gets stored, so validate always sees the raw string.
Validate against something slowasync-validate
{
type: 'input',
name: 'slug',
message: 'npm package name:',
async validate(value) {
const res = await fetch(`https://registry.npmjs.org/${value}`);
return res.status === 404 ? true : 'That name is taken';
},
}Returning a promise is the supported modern form; the older this.async() callback still works but cannot be used from an arrow function because it needs the prompt as this.
Build choices from earlier answersdynamic-choices
{
type: 'list',
name: 'branch',
message: 'Branch:',
choices: async (answers) =>
(await listBranches(answers.repo)).map((b) => ({ name: b, value: b })),
pageSize: 15,
}choices, default, and message can all be functions of the answers so far. pageSize controls how many rows render before the list scrolls; the default is small and long lists feel cramped without it.
Collect a secret without echoing itpassword-prompt
const { token } = await inquirer.prompt([{
type: 'password',
name: 'token',
message: 'API token:',
mask: '*',
}]);Without an explicit mask the input is not hidden, which the README calls out. Do not put the secret in a default value either: defaults are printed to the terminal.
Survive a run with no terminalnon-interactive-fallback
async function askOrFlag(flagValue, question) {
if (flagValue !== undefined) return flagValue;
if (!process.stdin.isTTY) {
throw new Error(`Missing --${question.name} in non-interactive mode`);
}
const answers = await inquirer.prompt([question]);
return answers[question.name];
}prompt() rejects when stdin is not a TTY, and the README says the rejection carries an isTtyError property. Checking process.stdin.isTTY yourself gives a clearer error, since a closed or piped stdin can also surface as a cancelled prompt.
Pass answers you already haveprefilled-answers
// second argument seeds the answers hash; matching questions are skipped
const answers = await inquirer.prompt(questions, {
name: argv.name,
typescript: argv.ts,
});This is the cleanest way to wire CLI flags into a question array. Set askAnswered: true on an individual question if you want it asked even when a value was supplied.
Register a custom prompt without global side effectsisolated-prompt-module
import inquirer from 'inquirer';
import autocomplete from 'inquirer-autocomplete-prompt';
const prompt = inquirer.createPromptModule();
prompt.registerPrompt('autocomplete', autocomplete);
const answers = await prompt([{ type: 'autocomplete', name: 'pkg', source: search }]);Calling inquirer.registerPrompt directly mutates shared state, so two libraries in the same process can clobber each other's prompt types. Older plugins were written against inquirer 8 internals and may not run on v14.
Move a question over to @inquirer/promptsmigrate-to-new-api
// before
const { ok } = await inquirer.prompt([
{ type: 'confirm', name: 'ok', message: 'Continue?' },
]);
// after
import { confirm } from '@inquirer/prompts';
const ok = await confirm({ message: 'Continue?' });Both packages run side by side, so you can convert one question at a time. The new API returns the value directly instead of an answers object, and drops when, filter, and the answers hash, so conditional flows become plain control flow.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @inquirer/prompts | npm | Anything new: same author, same look, individually importable prompts, and the package this README redirects you to |
| @clack/prompts | npm | You want a tighter, more polished default look with grouped flows and built-in cancel handling |
| enquirer | npm | You want a wide set of prompt types in one dependency and are fine with a CommonJS-friendly package |