Fix: Clack Not Working — Prompts Not Displaying, Spinners Stuck, or Cancel Not Handled
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix @clack/prompts issues — interactive CLI prompts, spinners, multi-select, confirm dialogs, grouped tasks, cancellation handling, and building CLI tools with beautiful output.
The Problem
Clack prompts don’t display any output:
import { text } from '@clack/prompts';
const name = await text({ message: 'What is your name?' });
// Terminal shows nothing — hangs indefinitelyOr the spinner starts but never stops:
import { spinner } from '@clack/prompts';
const s = spinner();
s.start('Loading...');
// Spinner runs foreverOr cancellation crashes the program:
const value = await text({ message: 'Enter value:' });
// User presses Ctrl+C
// UnhandledPromiseRejection: Symbol(clack:cancel)Why This Happens
Clack (@clack/prompts) provides terminal prompts for Node.js scripts using a render loop that owns the cursor and a few lines of output. Most failures come from the fact that Clack is not a passive printer — it requires a real interactive terminal session, expects you to participate in its lifecycle, and treats cancellation as data rather than as an exception.
Common causes:
- Clack writes to stdout/stderr and reads from stdin — if your script runs in an environment without a TTY (piped output, CI, some IDE output panels, Docker without
-it), interactive prompts cannot read input. The process hangs waiting for input that never comes.process.stdin.isTTYis the canonical check. - Spinners must be explicitly stopped —
s.start()begins the animation and takes over a line of output. You must calls.stop()to end it. If an error throws between start and stop, the spinner runs forever and corrupts terminal output because the render loop never releases the cursor. - Cancellation returns a special symbol — when the user presses Ctrl+C or Escape, Clack returns
Symbol(clack:cancel). If you do not check for it withisCancel(), your code tries to use the symbol as a string, causingTypeError: Cannot convert a Symbol value to a string, or it falls through to logic that assumes a valid answer. intro()andoutro()are decorative wrappers — they frame the output but are not required. However, callingoutro()withoutintro()or nesting prompts incorrectly breaks the visual formatting because the box-drawing characters expect a matching open and close.- Node version matters —
@clack/promptstargets modern Node. On Node 16 you can hit ESM import errors because the package ships as ESM. Use Node 18+ or run via a loader liketsx.
Version History That Changes the Failure Mode
The shape of the bug you hit depends heavily on which @clack/prompts version you installed and which alternative CLI library you migrated from. The API has been mostly additive, but the supporting primitives changed significantly.
@clack/promptsv0.6 (March 2023) — original public release. Providedtext,confirm,select,multiselect,password,spinner,intro,outro,cancel, andisCancel. If you are on this version,group()does not exist and grouped cancellation must be handled per prompt.@clack/promptsv0.7 (mid 2023) — addedgroupMultiselect()for selecting items grouped under headers, and refined theselectoptionhintrendering. Code that usesgroupMultiselectwill not type-check on v0.6.@clack/promptsv0.8 (late 2023) — reworked spinner output to be more resilient to non-TTY environments. Earlier versions wrote raw ANSI escape codes even when piped, producing garbage in log files. v0.8 detects non-TTY and degrades gracefully. If your CI logs show[2K[1Gsequences, upgrade.@clack/promptsv0.9 (2024) — added thetasks()API for running a sequence of named async steps with per-step spinners. Replaces the common pattern of manually starting and stopping aspinnerfor each step. Prior to v0.9, you had to roll this yourself.@clack/core0.3 (2024) — the lower-level primitive package, used directly only if you build custom prompts. Stream handling changed in 0.3, so custom prompts written against 0.2 may not detect Ctrl+C correctly.- Ecosystem comparison — Inquirer.js (v9, ESM-only since 2022) is the older alternative with a heavier rendering layer. Enquirer ships smaller but has a less polished default theme. Prompts (
promptson npm) is the minimal option used bycreate-vite. Clack sits between Enquirer and Inquirer — opinionated visuals, small surface, ESM-only.
If you are on Node 16, none of these recent versions install cleanly. Upgrade Node before debugging anything else.
Fix 1: Build a Complete CLI Flow
npm install @clack/prompts#!/usr/bin/env node
// cli.ts
import {
intro,
outro,
text,
select,
multiselect,
confirm,
spinner,
note,
cancel,
isCancel,
log,
group,
} from '@clack/prompts';
import color from 'picocolors';
async function main() {
intro(color.inverse(' My CLI Tool '));
// Text input
const name = await text({
message: 'What is your project name?',
placeholder: 'my-awesome-project',
validate(value) {
if (!value) return 'Name is required';
if (!/^[a-z0-9-]+$/.test(value)) return 'Only lowercase letters, numbers, and hyphens';
if (value.length < 3) return 'At least 3 characters';
},
});
// ALWAYS check for cancellation after each prompt
if (isCancel(name)) {
cancel('Operation cancelled.');
process.exit(0);
}
// Select (single choice)
const framework = await select({
message: 'Pick a framework:',
options: [
{ value: 'next', label: 'Next.js', hint: 'React framework' },
{ value: 'svelte', label: 'SvelteKit', hint: 'Svelte framework' },
{ value: 'astro', label: 'Astro', hint: 'Content-focused' },
{ value: 'remix', label: 'Remix', hint: 'Full-stack React' },
],
});
if (isCancel(framework)) {
cancel('Operation cancelled.');
process.exit(0);
}
// Multi-select
const features = await multiselect({
message: 'Select features:',
options: [
{ value: 'typescript', label: 'TypeScript' },
{ value: 'eslint', label: 'ESLint' },
{ value: 'prettier', label: 'Prettier' },
{ value: 'tailwind', label: 'Tailwind CSS' },
{ value: 'testing', label: 'Testing (Vitest)' },
],
required: true, // At least one must be selected
});
if (isCancel(features)) {
cancel('Operation cancelled.');
process.exit(0);
}
// Confirm
const shouldInstall = await confirm({
message: 'Install dependencies?',
initialValue: true,
});
if (isCancel(shouldInstall)) {
cancel('Operation cancelled.');
process.exit(0);
}
// Spinner for async operations
const s = spinner();
s.start('Creating project...');
try {
await createProject(name, framework, features);
s.stop('Project created');
} catch (error) {
s.stop('Failed to create project');
log.error(String(error));
process.exit(1);
}
if (shouldInstall) {
s.start('Installing dependencies...');
try {
await installDeps(name);
s.stop('Dependencies installed');
} catch (error) {
s.stop('Install failed');
log.warn('You can install manually with: npm install');
}
}
// Summary note
note(
`cd ${name}\nnpm run dev`,
'Next steps'
);
outro(color.green('Done! Happy coding.'));
}
main().catch(console.error);Fix 2: Group Prompts Together
import { group, intro, outro, isCancel, cancel } from '@clack/prompts';
async function main() {
intro('Project Setup');
// group() collects all prompts into an object
const project = await group(
{
name: () => text({
message: 'Project name?',
placeholder: 'my-project',
validate: (v) => !v ? 'Required' : undefined,
}),
type: () => select({
message: 'Project type?',
options: [
{ value: 'app', label: 'Application' },
{ value: 'lib', label: 'Library' },
{ value: 'monorepo', label: 'Monorepo' },
],
}),
git: () => confirm({
message: 'Initialize git?',
initialValue: true,
}),
},
{
// Handle cancellation for all prompts in the group
onCancel: () => {
cancel('Setup cancelled.');
process.exit(0);
},
},
);
// project = { name: 'my-project', type: 'app', git: true }
console.log(project);
outro('Setup complete!');
}Fix 3: Logging and Output
import { log, note, intro, outro } from '@clack/prompts';
import color from 'picocolors';
intro('Build Report');
// Structured log messages
log.info('Starting build...');
log.success('TypeScript compiled successfully');
log.warn('3 deprecation warnings found');
log.error('Failed to minify CSS');
log.message('Plain message without icon');
// Step indicator
log.step('Step 1: Compile');
log.step('Step 2: Bundle');
log.step('Step 3: Optimize');
// Note box — highlighted information
note(
'Build output: 245KB (gzipped: 82KB)\nChunks: 3\nAssets: 12',
'Build Summary',
);
// Colored output with picocolors
log.info(`Found ${color.bold('42')} files to process`);
log.success(`Deployed to ${color.cyan(color.underline('https://myapp.com'))}`);
log.warn(`${color.yellow('Warning:')} Node.js 18 is end-of-life`);
outro('Build complete');Fix 4: Password and Secret Input
import { password, text } from '@clack/prompts';
// Password input — characters are masked
const secret = await password({
message: 'Enter your API key:',
validate(value) {
if (!value) return 'API key is required';
if (!value.startsWith('sk_')) return 'API key must start with sk_';
},
});
// Path input with default
const outputDir = await text({
message: 'Output directory?',
placeholder: './dist',
defaultValue: './dist',
});Fix 5: Handle Non-TTY Environments
import { text, select, isCancel } from '@clack/prompts';
async function main() {
// Check if running in interactive mode
if (!process.stdin.isTTY) {
// Non-interactive — use defaults or CLI args
console.log('Running in non-interactive mode');
const name = process.argv[2] || 'default-project';
await createProject(name, 'next', ['typescript']);
return;
}
// Interactive mode — show prompts
const name = await text({ message: 'Project name?' });
if (isCancel(name)) {
process.exit(0);
}
// ...
}
// Or accept CLI flags as overrides
import { parseArgs } from 'node:util';
const { values } = parseArgs({
options: {
name: { type: 'string', short: 'n' },
framework: { type: 'string', short: 'f' },
yes: { type: 'boolean', short: 'y' }, // Skip prompts
},
});
async function main() {
// Skip prompts if --yes flag is passed
const name = values.name || await text({ message: 'Project name?' });
const framework = values.framework || await select({
message: 'Framework?',
options: [/* ... */],
});
}Fix 6: Build a Full CLI Tool
// bin/cli.ts — entry point
#!/usr/bin/env node
import { intro, outro, spinner, log, confirm, isCancel, cancel } from '@clack/prompts';
import color from 'picocolors';
import { execSync } from 'child_process';
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'init':
await initProject();
break;
case 'deploy':
await deploy();
break;
case 'help':
default:
showHelp();
}
async function deploy() {
intro(color.bgCyan(' Deploy '));
const proceed = await confirm({
message: `Deploy to ${color.bold('production')}?`,
});
if (isCancel(proceed) || !proceed) {
cancel('Deploy cancelled.');
process.exit(0);
}
const s = spinner();
s.start('Running tests...');
try {
execSync('npm test', { stdio: 'pipe' });
s.stop(color.green('Tests passed'));
} catch {
s.stop(color.red('Tests failed'));
log.error('Fix failing tests before deploying');
process.exit(1);
}
s.start('Building...');
execSync('npm run build', { stdio: 'pipe' });
s.stop(color.green('Build complete'));
s.start('Deploying...');
execSync('npx wrangler deploy', { stdio: 'pipe' });
s.stop(color.green('Deployed!'));
outro(color.green('Successfully deployed to production'));
}
function showHelp() {
console.log(`
${color.bold('my-cli')} - Project management tool
${color.bold('Commands:')}
init Create a new project
deploy Deploy to production
help Show this help message
`);
}// package.json — make it executable
{
"name": "my-cli",
"bin": {
"my-cli": "./dist/cli.js"
},
"scripts": {
"build": "tsup src/cli.ts --format esm",
"dev": "tsx src/cli.ts"
}
}Still Not Working?
Prompts hang without showing anything — the script is likely running in a non-TTY environment (piped output, some CI runners, IDE output panels). Check process.stdin.isTTY — if it is false, prompts cannot read input. Provide fallback behavior or require the --yes flag for non-interactive mode.
Spinner corrupts terminal output — if an error throws between s.start() and s.stop(), the spinner keeps running. Always wrap spinner operations in try/catch and call s.stop() in both the success and error paths. The stop message can indicate success or failure.
isCancel() doesn’t catch Ctrl+C — isCancel() checks for the cancel symbol returned by Clack prompts. Ctrl+C during a prompt returns this symbol. But Ctrl+C outside a prompt sends SIGINT to the process. Handle both: check isCancel() after each prompt, and add process.on('SIGINT', () => process.exit(0)) for graceful global exit.
Multi-select returns empty array — if required: false (default), the user can submit without selecting anything. Set required: true to enforce at least one selection. The user presses Space to toggle items and Enter to confirm.
ERR_REQUIRE_ESM when importing @clack/prompts — the package is ESM-only. You cannot require() it from CommonJS. Either convert your entry file to ESM by adding "type": "module" to package.json, run the script through tsx (which transparently loads ESM), or dynamically import it with await import('@clack/prompts') from an async function.
Output looks broken on Windows Terminal or older terminals — Clack uses Unicode box-drawing characters and ANSI escapes. On legacy Windows consoles without VT mode (cmd.exe pre-Windows 10 build 1607) the box characters render as ?. Run inside Windows Terminal, PowerShell 7, or WSL. For old terminals, force a plain rendering by setting FORCE_COLOR=0 and stripping the framing.
Spinner clears the success message immediately — on some terminals, s.stop('Done') writes the line and then the next console.log or intro redraws and clears it. Avoid mixing raw console.log between Clack calls. Use log.message, log.info, log.success, etc., which are aware of the render loop.
For related CLI tool issues, see Fix: esbuild Not Working, Fix: Bun Not Working, Fix: tsx Not Working, and Fix: tsup Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Sharp Not Working — Installation Failing, Image Not Processing, or Build Errors on Deploy
How to fix Sharp image processing issues — native binary installation, resize and convert operations, Next.js image optimization, Docker setup, serverless deployment, and common platform errors.
Fix: Shiki Not Working — No Syntax Highlighting, Wrong Theme, or Build Errors
How to fix Shiki syntax highlighter issues — basic setup, theme configuration, custom languages, transformer plugins, Next.js and Astro integration, and bundle size optimization.
Fix: Rspack Not Working — Build Failing, Loaders Not Applying, or Dev Server Not Starting
How to fix Rspack issues — configuration migration from webpack, loader compatibility, CSS extraction, module federation, React Fast Refresh, and build performance tuning.
Fix: Biome Not Working — Rules Not Applied, ESLint Config Not Migrated, or VSCode Extension Ignored
How to fix Biome linter/formatter issues — biome.json configuration, migrating from ESLint and Prettier, VSCode extension setup, CI integration, and rule override syntax.