Ora Spinners
Add progress indicators to CLI tools using ora and nanospinner for async operation feedback
You are an expert in adding progress indicators to CLI applications using ora and nanospinner. ## Key Points - Use `spinner.succeed()`, `.fail()`, `.warn()`, and `.info()` to terminate spinners with clear final states rather than just calling `.stop()`, which leaves no visual indicator of the outcome. - Update `spinner.text` during long operations to show progress — a static "Loading..." for 30 seconds feels broken, but "Loading... (142/500 files)" communicates progress. - Choose nanospinner for tools where dependency count matters (e.g., `create-*` scaffolders that users install globally) and ora for feature-rich applications. - Ora v7+ is ESM-only — use `ora@6` if your project requires CommonJS, or migrate to `"type": "module"` in `package.json`. ## Quick Example ```bash # Full-featured npm install ora # Lightweight alternative npm install nanospinner ```
skilldb get cli-development-skills/Ora SpinnersFull skill: 212 linesProgress Indicators (ora, nanospinner) — CLI Development
You are an expert in adding progress indicators to CLI applications using ora and nanospinner.
Overview
Progress indicators give users feedback during long-running operations. Ora is the standard choice with rich customization (spinner styles, colors, prefixes). Nanospinner is a lightweight alternative at 1/14th the size. Both show an animated spinner with a status message that can be updated, succeeded, or failed.
Setup & Configuration
Install your preferred library:
# Full-featured
npm install ora
# Lightweight alternative
npm install nanospinner
Basic ora usage:
import ora from 'ora';
const spinner = ora('Loading configuration').start();
try {
const config = await loadConfig();
spinner.succeed('Configuration loaded');
} catch (err) {
spinner.fail('Failed to load configuration');
process.exit(1);
}
Basic nanospinner usage:
import { createSpinner } from 'nanospinner';
const spinner = createSpinner('Loading configuration').start();
try {
const config = await loadConfig();
spinner.success({ text: 'Configuration loaded' });
} catch (err) {
spinner.error({ text: 'Failed to load configuration' });
process.exit(1);
}
Core Patterns
Sequential operations with status updates
import ora from 'ora';
async function deploy(env: string) {
const spinner = ora({ text: 'Starting deployment', color: 'cyan' }).start();
spinner.text = 'Running tests';
await runTests();
spinner.text = 'Building application';
await build();
spinner.text = `Deploying to ${env}`;
await deployTo(env);
spinner.text = 'Running health checks';
await healthCheck();
spinner.succeed(`Deployed to ${env} successfully`);
}
Multiple independent spinners
import ora from 'ora';
async function parallelTasks() {
const tasks = [
{ name: 'Database migration', fn: runMigrations },
{ name: 'Asset compilation', fn: compileAssets },
{ name: 'Cache warmup', fn: warmCache },
];
// Run sequentially with individual spinners
for (const task of tasks) {
const spinner = ora(task.name).start();
try {
await task.fn();
spinner.succeed();
} catch (err) {
spinner.fail(`${task.name}: ${err.message}`);
}
}
}
Custom spinner styles
import ora from 'ora';
// Use a different built-in spinner
const spinner = ora({
text: 'Processing',
spinner: 'dots12', // dots, line, star, hamburger, bouncingBar, etc.
color: 'magenta',
prefixText: '[build]',
}).start();
// Warning state (yellow, special symbol)
spinner.warn('Completed with warnings');
// Info state
spinner.info('Using cached results');
Wrapping a promise
import ora from 'ora';
// ora can wrap a promise directly
const result = await ora.promise(fetchData(), {
text: 'Fetching data',
successText: 'Data fetched',
failText: 'Failed to fetch data',
});
Non-TTY fallback
import ora from 'ora';
// ora automatically degrades in non-TTY (pipes, CI)
// It logs the text without animation
// You can also detect and handle manually:
const spinner = ora({
text: 'Processing',
isSilent: !process.stderr.isTTY,
}).start();
Progress with percentage
import ora from 'ora';
const spinner = ora('Downloading').start();
async function downloadWithProgress(url: string) {
const response = await fetch(url);
const total = Number(response.headers.get('content-length'));
let downloaded = 0;
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
downloaded += value.length;
const pct = Math.round((downloaded / total) * 100);
spinner.text = `Downloading... ${pct}%`;
}
spinner.succeed('Download complete');
}
Best Practices
- Use
spinner.succeed(),.fail(),.warn(), and.info()to terminate spinners with clear final states rather than just calling.stop(), which leaves no visual indicator of the outcome. - Update
spinner.textduring long operations to show progress — a static "Loading..." for 30 seconds feels broken, but "Loading... (142/500 files)" communicates progress. - Choose nanospinner for tools where dependency count matters (e.g.,
create-*scaffolders that users install globally) and ora for feature-rich applications.
Core Philosophy
Progress indicators exist to maintain user trust during operations they cannot observe. A spinning indicator says "I am working" — and updating its text to say "Connecting to database..." or "Processing 142/500 files..." transforms vague trust into informed confidence. The difference between a good CLI and a frustrating one is often just the quality of its progress feedback.
Always terminate spinners with an explicit outcome. Calling .stop() leaves a blank line that tells the user nothing. Calling .succeed(), .fail(), .warn(), or .info() leaves a permanent line in the terminal with a clear visual indicator of what happened. Users scrolling through their terminal history should be able to reconstruct the sequence of events from the spinner outcomes alone.
Choose the right weight for your context. Ora is feature-rich and appropriate for application-level CLIs where you want custom spinner styles, color control, and promise wrapping. Nanospinner is 14x smaller and appropriate for scaffolding tools, libraries, and anything distributed via npx where install size matters. Both achieve the same core goal — the difference is in the dependency footprint.
Anti-Patterns
-
Leaving spinners running after completion — forgetting to call
.succeed()or.fail()leaves an animated spinner on screen indefinitely, confusing users about whether the operation is still in progress. -
Showing a static message for long operations — displaying "Loading..." for 30 seconds with no progress updates feels broken; update
spinner.textperiodically to show what is happening and how far along it is. -
Writing to stdout while a spinner is active —
console.logcalls during spinner animation cause visual glitching and garbled output; usespinner.info()to log a line cleanly or clear/re-render the spinner around log statements. -
Using ora in non-TTY environments without fallback — spinners in CI logs or piped output produce streams of ANSI escape codes; detect non-TTY with
process.stderr.isTTYand fall back to simple log lines. -
Nesting spinners — running multiple concurrent ora instances produces overlapping animations and cursor conflicts; use sequential spinners or a multi-line progress display like
listr2for parallel tasks.
Common Pitfalls
- Writing to stdout with
console.logwhile a spinner is active causes visual glitching — usespinner.clear()before logging, thenspinner.render()to resume, or useora's built-inspinner.info()to log a line cleanly. - Ora v7+ is ESM-only — use
ora@6if your project requires CommonJS, or migrate to"type": "module"inpackage.json.
Install this skill directly: skilldb add cli-development-skills
Related Skills
Chalk Picocolors
Style terminal output with chalk and picocolors for colored, formatted CLI text
Clack Prompts
Build beautiful interactive CLI prompts using @clack/prompts with minimal boilerplate
CLI Distribution
Package and distribute CLI tools via npm/npx, standalone binaries with pkg, and Homebrew taps
CLI Testing
Test CLI applications using subprocess execution, mock filesystems, and snapshot testing
Commander Js
Build structured CLI applications with Commander.js including commands, options, and argument parsing
Ink React CLI
Build rich interactive terminal UIs using Ink, a React renderer for the command line