Fix: Clack Not Working — Prompts Not Displaying, Spinners Stuck, or Cancel Not Handled
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 beautiful CLI prompts for Node.js scripts. Issues arise from:
- Clack writes to stdout/stderr — if your script runs in an environment without a TTY (piped output, CI, some IDE terminals), interactive prompts can’t read input. The process hangs waiting for input that never comes.
- Spinners must be explicitly stopped —
s.start()begins the animation. You must calls.stop()to end it. If an error throws between start and stop, the spinner runs forever and corrupts terminal output. - Cancellation returns a special symbol — when the user presses Ctrl+C or Escape, Clack returns
Symbol(clack:cancel). If you don’t check for it withisCancel(), your code tries to use the symbol as a string, causing crashes. intro()andoutro()are decorative wrappers — they frame the output but aren’t required. However, callingoutro()withoutintro()or nesting prompts incorrectly breaks the visual formatting.
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’s false, prompts can’t 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.
For related CLI tool issues, see Fix: esbuild Not Working and Fix: Bun 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.