prompts
prompts asks a terminal user questions and hands you back an object of answers. You describe each question as a plain object with a type, a name, and a message, pass one or an array of them to the prompts() function, and await the result. It covers twelve input types: text, password, invisible, number, confirm, list, toggle, select, multiselect, autocompleteMultiselect, autocomplete, and date. Every property can also be a function evaluated just before the question is asked, which is how conditional and dependent questions work: return a falsy type and the question is skipped. It has exactly two dependencies, kleur for color and sisteransi for cursor control, and it became the default scaffolding prompt library because it is small and the API fits in one page.
Still a pleasant API and still tiny, but it has not shipped a release in nearly five years while sitting on 115 open issues, so you are adopting a frozen dependency. Fine for an internal script, harder to justify for a CLI you intend to support.
Use it if
- You are writing a scaffolding or init command and need five or six questions answered without adding a framework
- You want conditional questions driven by earlier answers, which the function-valued type and message properties handle without any branching code around the call
- You need the prompt flow to be testable: prompts.inject() feeds canned answers and prompts.override() pre-answers from parsed CLI flags, both built in
- Install size matters to you. Two dependencies and about 19 KB gzipped is a fraction of what the Inquirer family pulls in
- You want a maintained package. The last npm publish was 2.4.2 in October 2021, the repo was last pushed May 2025, and 115 issues are open on top of open PRs; nothing is being fixed, it is just widely installed
- You want TypeScript types in the box. There are none, so you install @types/prompts separately and it is community-maintained on DefinitelyTyped, last published November 2023
- You are writing native ESM. The package is CommonJS with no exports map and no ESM build, so a default import works through interop but named imports do not, and bundlers cannot tree-shake the twelve prompt types you did not use
- You want cancellation to be loud. Ctrl+C resolves the promise with whatever answers were collected so far instead of rejecting, so a script that forgets onCancel keeps running with undefined values and a zero exit code
- You are building a polished modern CLI. @clack/prompts has better visual grouping and cancel handling, and @inquirer/prompts is the actively developed rewrite of the library this one was reacting to
Setup reality
npm install prompts and you are running; there is no config, no peer dependency, and no build step. The gaps show up afterward. Types are a separate npm install --save-dev @types/prompts. package.json declares engines node >= 6 and the entry file branches to a transpiled dist build for anything below Node 8.6, while the README says Node 14 and above, so treat the engines field as historical rather than accurate. It writes to process.stdout and reads process.stdin by default, which means it hangs or misbehaves in a non-TTY context such as a CI job or a piped shell, and you have to guard on process.stdin.isTTY yourself. There is no ESM entry point, so in a type: module project you use a default import and cannot destructure named exports from it.
Patterns
Ask one questionsingle-prompt
const prompts = require('prompts');
const { projectName } = await prompts({
type: 'text',
name: 'projectName',
message: 'Project name?',
initial: 'my-app',
});The name property is the key you read off the response object, so it must be unique across a chain or a later answer silently overwrites an earlier one.
Exit properly when the user presses Ctrl+Chandle-cancel
const prompts = require('prompts');
const onCancel = () => {
console.log('Aborted.');
process.exit(1);
};
const answers = await prompts(questions, { onCancel });Without onCancel, Ctrl+C or Esc resolves the promise with only the answers collected so far and exit code 0, so your script happily continues with undefined values. Wire this up before anything else.
Ask a list of questions in orderprompt-chain
const questions = [
{ type: 'text', name: 'username', message: 'GitHub username?' },
{ type: 'password', name: 'token', message: 'Personal access token?' },
{ type: 'confirm', name: 'save', message: 'Save to keychain?', initial: true },
];
const { username, token, save } = await prompts(questions, { onCancel });Questions run sequentially and each one sees the answers so far, which is what makes function-valued properties useful; there is no parallel mode.
Skip a question based on a previous answerconditional-question
const questions = [
{ type: 'confirm', name: 'useTypescript', message: 'Use TypeScript?' },
{
type: prev => (prev ? 'select' : null),
name: 'strictness',
message: 'tsconfig strictness',
choices: [
{ title: 'strict', value: 'strict' },
{ title: 'loose', value: 'loose' },
],
},
];Returning a falsy type skips the question entirely, and no key is set on the response object, so destructure with a default rather than assuming the property exists.
Reject bad input before moving onvalidate-input
{
type: 'text',
name: 'email',
message: 'Work email?',
validate: value =>
/.+@.+\..+/.test(value) ? true : 'That does not look like an email',
}Return true to accept, a string to show that message, or false for a generic error. validate can be async, which is how you check name availability against an API before continuing.
Single choice from a listselect-from-choices
{
type: 'select',
name: 'pm',
message: 'Package manager',
choices: [
{ title: 'npm', value: 'npm' },
{ title: 'pnpm', value: 'pnpm', description: 'fast, disk efficient' },
{ title: 'yarn', value: 'yarn', disabled: true },
],
initial: 1,
}initial is a zero-based index into choices, not a value, which trips people up when the list is generated. Omitting value on a choice makes the answer the title string.
Pick several options at oncemultiselect
{
type: 'multiselect',
name: 'features',
message: 'Add features',
choices: [
{ title: 'ESLint', value: 'eslint', selected: true },
{ title: 'Prettier', value: 'prettier' },
{ title: 'Vitest', value: 'vitest' },
],
hint: 'space to select, enter to submit',
instructions: false,
}The answer is always an array, empty when nothing was picked, so there is no undefined case to handle. Set instructions: false to suppress the multi-line help block, which otherwise dominates a short list.
Transform the value before it reaches your codeformat-answer
{
type: 'text',
name: 'slug',
message: 'URL slug',
format: val => val.trim().toLowerCase().replace(/\s+/g, '-'),
}format runs after validate, so validate sees the raw input and your program sees the formatted value; putting normalization in validate instead is a common cause of confusing error messages.
Drive the prompt flow from a testinject-answers-in-tests
const prompts = require('prompts');
prompts.inject(['my-app', ['eslint', 'vitest'], true]);
const answers = await runInitCommand();
expect(answers.projectName).toBe('my-app');Injected values are consumed in order and removed from the queue, and an Error instance simulates the user cancelling. The README marks this as testing-only, so do not build production behavior on it.
Let CLI flags pre-answer questionsoverride-from-flags
const prompts = require('prompts');
const argv = require('minimist')(process.argv.slice(2));
prompts.override(argv);
// --projectName=my-app now skips that question entirely
const answers = await prompts(questions, { onCancel });override matches on the question name, so your flag names and prompt names have to agree. This is the clean way to make an interactive command scriptable without a second code path.
Filter a large list as the user typesautocomplete-async
{
type: 'autocomplete',
name: 'pkg',
message: 'Search npm',
choices: [],
suggest: async input => {
const hits = await searchRegistry(input);
return hits.map(h => ({ title: h.name, value: h.name }));
},
limit: 10,
}suggest is called on every keystroke, so debounce or cache inside it if it hits the network; the built-in default only does a case-insensitive title match against the static choices array.
Do not prompt when there is no terminalguard-non-tty
if (!process.stdin.isTTY) {
console.error('Non-interactive shell: pass --projectName and --pm as flags.');
process.exit(1);
}
const answers = await prompts(questions, { onCancel });The library reads process.stdin directly with no interactive-environment check, so in CI or behind a pipe it either hangs until the job times out or takes garbage as input. This guard has to be yours.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @inquirer/prompts | npm | You want the actively maintained option with per-prompt packages, real TypeScript types, and ESM plus CJS builds |
| @clack/prompts | npm | You want a modern-looking flow with grouped steps, spinners, and cancellation that is hard to ignore |
| enquirer | npm | You need prompt types this library lacks, such as forms, surveys, sorting, and scale questions |