DEV Community

Wilson Xu
Wilson Xu

Posted on

@clack/prompts: The Modern Alternative to Inquirer.js

@clack/prompts: The Modern Alternative to Inquirer.js

If you have built a CLI tool in Node.js, you have almost certainly used Inquirer.js. It has been the default interactive prompt library for over a decade. But defaults change. A newer library called @clack/prompts has arrived, and it is making Inquirer feel like jQuery in a React world.

This article covers why @clack/prompts is gaining traction, how its API works, and how to migrate from Inquirer without pain.

Why Developers Are Switching

Inquirer.js works. It has for years. But it carries baggage. The v9+ rewrite moved to ESM-only with a modular @inquirer/prompts package, which fractured the ecosystem. Plugin compatibility broke. TypeScript support was bolted on. Styling requires manual ANSI escape codes or third-party libraries like chalk.

@clack/prompts takes a different approach. It ships beautiful, opinionated styling out of the box. No configuration, no theming layer, no plugins. You get polished terminal UI from the first line of code.

Here is what you see when you run a basic @clack/prompts flow:

 +-- Create your project
 |
 o  What is your project name?
 |  my-awesome-app
 |
 o  Pick a framework
 |  React
 |
 o  Install dependencies?
 |  Yes
 |
 +-- You're all set!
Enter fullscreen mode Exit fullscreen mode

That vertical line, the step indicators, the grouping -- all automatic. With Inquirer, you would need ora, chalk, boxen, and manual formatting to get anywhere close.

The numbers back it up. @clack/prompts is roughly 4KB gzipped. Inquirer.js (even the modular @inquirer/prompts) pulls in significantly more. For CLI tools where startup time matters, that difference is real.

Getting Started

Install it:

npm install @clack/prompts
Enter fullscreen mode Exit fullscreen mode

That is it. One package. No peer dependencies.

import { intro, outro, text, select, confirm, spinner } from '@clack/prompts';
Enter fullscreen mode Exit fullscreen mode

Every prompt function is a named export. No class instantiation, no prompt registration, no configuration objects. Just functions.

Prompt Types

text

Collects free-form string input.

const name = await text({
  message: 'What is your project name?',
  placeholder: 'my-app',
  defaultValue: 'my-app',
  validate(value) {
    if (!value) return 'Name is required';
    if (value.includes(' ')) return 'No spaces allowed';
  },
});
Enter fullscreen mode Exit fullscreen mode

The validate function returns a string on error, or nothing on success. Simple and predictable.

select

Single-choice selection from a list.

const framework = await select({
  message: 'Pick a framework',
  options: [
    { value: 'react', label: 'React', hint: 'recommended' },
    { value: 'vue', label: 'Vue' },
    { value: 'svelte', label: 'Svelte' },
  ],
});
Enter fullscreen mode Exit fullscreen mode

The hint property adds subtle context text next to the option. No equivalent exists in Inquirer without custom rendering.

multiselect

Multiple-choice selection with spacebar toggling.

const features = await multiselect({
  message: 'Select features',
  options: [
    { value: 'typescript', label: 'TypeScript' },
    { value: 'eslint', label: 'ESLint' },
    { value: 'prettier', label: 'Prettier' },
  ],
  required: true,
});
Enter fullscreen mode Exit fullscreen mode

Setting required: true prevents the user from continuing without selecting at least one option.

confirm

Yes/no boolean prompt.

const shouldInstall = await confirm({
  message: 'Install dependencies?',
});
Enter fullscreen mode Exit fullscreen mode

Returns true or false. Not an object. Not a wrapped value. A boolean.

spinner

Shows a loading indicator while async work happens.

const s = spinner();
s.start('Installing dependencies');
await installDeps();
s.stop('Dependencies installed');
Enter fullscreen mode Exit fullscreen mode

The spinner is a separate utility, not a prompt. It does not block input. It just shows progress. Compare this to Inquirer, where you would reach for ora as a separate dependency.

Group Prompts for Wizard Flows

This is where @clack/prompts really separates itself. The group function lets you chain prompts into a single object, with each step able to reference previous answers.

import { group, text, select, confirm } from '@clack/prompts';

const project = await group(
  {
    name: () =>
      text({
        message: 'Project name?',
        placeholder: 'my-app',
      }),
    framework: () =>
      select({
        message: 'Framework?',
        options: [
          { value: 'react', label: 'React' },
          { value: 'vue', label: 'Vue' },
        ],
      }),
    typescript: ({ results }) =>
      confirm({
        message: `Use TypeScript with ${results.framework}?`,
      }),
  },
  {
    onCancel: () => {
      process.exit(0);
    },
  }
);

console.log(project.name);      // string
console.log(project.framework);  // 'react' | 'vue'
console.log(project.typescript); // boolean
Enter fullscreen mode Exit fullscreen mode

The results parameter gives you access to all previous answers. The return value is a fully typed object. In TypeScript, project.framework is inferred as 'react' | 'vue', not string.

With Inquirer, achieving this requires manually chaining .then() calls or using when conditionals inside a flat array of question objects. The group API is declarative and composable.

Cancel Handling

Every prompt in @clack/prompts returns a special symbol when the user presses Ctrl+C. You check for it with isCancel:

import { text, isCancel, cancel } from '@clack/prompts';

const name = await text({ message: 'Project name?' });

if (isCancel(name)) {
  cancel('Operation cancelled.');
  process.exit(0);
}
Enter fullscreen mode Exit fullscreen mode

The cancel function prints a styled cancellation message. This pattern is explicit and impossible to miss. Inquirer handles cancellation through error events or rejected promises, which are easy to forget.

In a group, you handle cancellation once with the onCancel callback instead of checking after every prompt.

Inquirer.js vs @clack/prompts: Side by Side

API Style

Inquirer uses a configuration-driven approach. You define an array of question objects and pass them to a prompt() function. @clack/prompts uses individual function calls, each returning a value directly.

Inquirer:

const answers = await inquirer.prompt([
  { type: 'input', name: 'name', message: 'Name?' },
  { type: 'list', name: 'color', message: 'Color?', choices: ['red', 'blue'] },
]);
Enter fullscreen mode Exit fullscreen mode

@clack/prompts:

const name = await text({ message: 'Name?' });
const color = await select({
  message: 'Color?',
  options: [
    { value: 'red', label: 'Red' },
    { value: 'blue', label: 'Blue' },
  ],
});
Enter fullscreen mode Exit fullscreen mode

The difference is ergonomic. With @clack/prompts, each value is a standalone variable with a clear type. With Inquirer, everything lives on an answers object keyed by the name string.

Bundle Size

@clack/prompts ships at roughly 4KB gzipped. @inquirer/prompts (the modular v9+ package) is larger due to its rendering engine and plugin system. If you are building a CLI that needs to start fast, the size gap matters.

Styling

Inquirer renders prompts as plain text by default. Customization requires a theme object or manual ANSI codes. @clack/prompts renders a visually connected flow with step indicators, consistent spacing, and color -- all without configuration.

TypeScript

@clack/prompts is written in TypeScript and infers return types from your option values. Inquirer's TypeScript support has improved in v9 but still relies on generics that can be awkward.

Migration Guide: Inquirer to @clack/prompts

Migrating is straightforward because the prompt types map one-to-one:

Inquirer Type @clack/prompts Function
input text
list select
checkbox multiselect
confirm confirm
password password

Step 1: Replace import inquirer from 'inquirer' with individual imports from @clack/prompts.

Step 2: Convert your question arrays into sequential await calls or a group.

Step 3: Replace answers.propertyName access with direct variable usage.

Step 4: Add intro() at the start and outro() at the end for the connected visual flow.

Step 5: Add isCancel checks after each prompt (or use onCancel in a group).

Most migrations take under 30 minutes for a typical CLI tool.

Complete CLI Wizard Example

Here is a full project scaffolding wizard:

#!/usr/bin/env node
import {
  intro, outro, text, select, multiselect,
  confirm, spinner, isCancel, cancel, group, note,
} from '@clack/prompts';
import { setTimeout } from 'node:timers/promises';

intro('create-my-app');

const project = await group(
  {
    name: () =>
      text({
        message: 'Project name',
        placeholder: 'my-app',
        validate: (v) => (!v ? 'Required' : undefined),
      }),
    framework: () =>
      select({
        message: 'Framework',
        options: [
          { value: 'react', label: 'React', hint: 'recommended' },
          { value: 'vue', label: 'Vue' },
          { value: 'svelte', label: 'Svelte' },
        ],
      }),
    features: () =>
      multiselect({
        message: 'Additional features',
        options: [
          { value: 'ts', label: 'TypeScript' },
          { value: 'lint', label: 'ESLint + Prettier' },
          { value: 'test', label: 'Vitest' },
          { value: 'ci', label: 'GitHub Actions CI' },
        ],
        required: false,
      }),
    install: () =>
      confirm({
        message: 'Install dependencies now?',
        initialValue: true,
      }),
  },
  {
    onCancel: () => {
      cancel('Setup cancelled.');
      process.exit(0);
    },
  }
);

if (project.install) {
  const s = spinner();
  s.start('Installing dependencies');
  await setTimeout(2000); // simulate install
  s.stop('Dependencies installed');
}

note(
  `cd ${project.name}\nnpm run dev`,
  'Next steps'
);

outro('Happy coding!');
Enter fullscreen mode Exit fullscreen mode

Run this and you get a fully styled, connected wizard flow in your terminal. No chalk. No ora. No figlet banners. Just clean, modern CLI UX in under 50 lines of code.

When to Stick with Inquirer

Inquirer still wins if you need custom prompt types (like autocomplete or date pickers), deep plugin ecosystem support, or backward compatibility with CommonJS projects. If your CLI has been on Inquirer for years and works fine, migration is optional.

But for new projects, @clack/prompts is the better starting point. Less code, better defaults, smaller footprint.

Conclusion

@clack/prompts is not a drop-in replacement for Inquirer. It is a rethink of what CLI prompts should feel like. The API is simpler. The output is prettier. The bundle is smaller. And the group API solves the wizard-flow problem that Inquirer never addressed cleanly.

Next time you scaffold a CLI tool, give it a try. Your terminal will thank you.

Top comments (0)