mrkeyoor.com_
Thu 06 Aug 00:58 UTC
npmCLI & Toolingupdated 05 Aug 2026

ora

Ora renders an animated spinner in the terminal while your Node CLI does async work. You create an instance with a message, call .start(), and end it with .succeed(), .fail(), .warn(), or .info(), which swap the spinner for a colored symbol and keep the final line in the scrollback. It picks sensible spinners per platform, writes to stderr by default so stdout stays pipeable, disables itself in CI and non-TTY contexts, and swallows stray keystrokes so typing does not shred the animation. oraPromise() wraps a whole promise in that lifecycle in one call.

Verdict

The default spinner for Node CLIs, and the edge-case handling (CI detection, stdin discarding, stderr default, log-while-spinning) is what you are actually paying the eight dependencies for. Skip it only if you need multiple spinners, a progress bar, or CommonJS.

API stability5/5start/stop/succeed/fail has been the API for a decade; recent majors changed the Node floor (v9 needs >=20) and packaging (ESM only since v6), not the methods.
Docs4/5The README documents every option and instance member with types and defaults, plus an FAQ covering the real gotchas (frozen spinners, Windows, worker threads); there is no separate site, which is fine at this size.
Maintenance4/5Pushed June 2026 with a completely clean tracker (0 open issues) and sindresorhus as long-term maintainer; releases are steady, though it is one volunteer rather than a team.
Ecosystem5/5Roughly 85M weekly downloads, ports to Python, Rust, Go, and Swift listed in the README, and it composes with the same author's cli-spinners, chalk, and log-update packages.

Use it if

  • Your CLI has waits longer than a second (network calls, installs, builds) and you want users to see progress plus a persisted success or failure line
  • You want TTY, CI, and piped-output detection handled for you: ora degrades to plain text automatically instead of dumping escape codes into logs
  • You wrap promises a lot: oraPromise(action, {successText, failText}) covers the start/succeed/fail dance in one call
  • You are already in the sindresorhus dependency universe (chalk, log-symbols, cli-spinners), so ora adds little new surface
Skip it if

Setup reality

npm install ora and one import is genuinely all of it: no config, types bundled, no native builds. The friction is environmental. Pure ESM since v6 means CommonJS projects get ERR_REQUIRE_ESM and either migrate or stay on ora 5. The default discardStdin puts stdin into raw mode, so Ctrl+C stops generating SIGINT from the terminal; ora re-emits it, but if you block the event loop with synchronous work the Ctrl+C is delayed until the work finishes. Windows terminals mostly fall back to the plain line spinner because Unicode detection there is best-effort, and spinners do not animate at all in Worker threads or non-interactive environments, which regularly surprises people testing in CI.

Patterns

Start a spinner and update it livebasic-spinner

import ora from 'ora';

const spinner = ora('Loading unicorns').start();

setTimeout(() => {
  spinner.color = 'yellow';
  spinner.text = 'Loading rainbows';
}, 1000);

Pure ESM since v6: require('ora') throws ERR_REQUIRE_ESM. text, color, spinner, and indent are live setters, so you mutate the running instance instead of restarting it.

End with a persisted symbol linesucceed-fail-outcomes

import ora from 'ora';

const spinner = ora('Deploying').start();

try {
  await deploy();
  spinner.succeed('Deployed');
} catch (error) {
  spinner.fail(`Deploy failed: ${error.message}`);
  process.exitCode = 1;
}

succeed/fail/warn/info stop the spinner and persist a green check, red cross, yellow warning, or blue info symbol; .stop() just clears the line and persists nothing.

Wrap a promise with oraPromisewrap-promise

import {oraPromise} from 'ora';

const user = await oraPromise(fetchUser(id), {
  text: 'Fetching user',
  successText: result => `Fetched ${result.name}`,
  failText: error => `Fetch failed: ${error.message}`
});

Resolves or rejects with the original promise result, calling succeed/fail for you. successText and failText accept functions that receive the result or error.

Pick a named spinner or define framescustom-spinner-frames

import ora from 'ora';

const named = ora({text: 'Thinking', spinner: 'moon'}).start();

const custom = ora({
  text: 'Working',
  spinner: {frames: ['-', '+', '-'], interval: 80}
});

Names come from cli-spinners. On Windows outside Windows Terminal ora falls back to the line spinner because Unicode detection is unreliable; set spinner explicitly if you know the terminal can handle it.

Log messages while the spinner runslog-while-spinning

import ora from 'ora';

const spinner = ora('Processing...').start();

console.log('Step 1 complete');
console.error('warning: cache miss');

spinner.succeed('Done!');

Ora intercepts writes to the same stream, clears itself, prints your line, and re-renders below. This works for both stdout and stderr; you do not need a special log method.

Persist a custom symbol and textstop-and-persist

import ora from 'ora';

const spinner = ora('Downloading').start();

spinner.stopAndPersist({
  symbol: '#',
  text: 'Download queued'
});

Use this when none of succeed/fail/warn/info fits; the default symbol is a single space, which visually aligns the persisted text with symbol lines above it.

Send the spinner to stdoutstdout-instead-of-stderr

import ora from 'ora';

const spinner = ora({
  text: 'Building',
  stream: process.stdout
}).start();

Default is stderr, which keeps stdout clean for pipeable program output. Only switch if the spinner itself is the product; enabled-detection then follows stdout's TTY state instead.

Silence output for tests and quiet modesilence-in-tests

import ora from 'ora';

const spinner = ora({
  text: 'Syncing',
  isSilent: process.env.NODE_ENV === 'test'
}).start();

spinner.succeed();

isSilent suppresses everything including the persisted final line. isEnabled: false is different: it still prints the text as plain lines, just without animation and colors.

Add dynamic prefix or suffix textprefix-suffix-text

import ora from 'ora';

let done = 0;
const spinner = ora({
  text: 'Fetching pages',
  suffixText: () => `[${done}/10]`
}).start();

// later, as work completes
done += 1;

prefixText and suffixText accept functions re-evaluated each frame, which is the cheap way to get a live counter without a progress bar library.

Control stdin discarding and Ctrl+C behaviorctrl-c-and-stdin

import ora from 'ora';

const spinner = ora({
  text: 'Long task',
  discardStdin: false
}).start();

Default discardStdin: true puts stdin into raw mode so Enter presses do not break the animation, but Ctrl+C is then re-emitted by ora instead of arriving as terminal SIGINT; blocking the event loop delays it. Disable it if your CLI reads stdin or you see interrupt weirdness.

Drive one spinner from a worker threadworker-thread-control

// main.js
import {Worker} from 'node:worker_threads';
import ora from 'ora';

const spinner = ora().start();
const worker = new Worker('./worker.js');

worker.on('message', message => {
  if (message.type === 'ora:text') spinner.text = message.text;
  if (message.type === 'ora:succeed') spinner.succeed(message.text);
});

Spinners do not animate inside Worker threads because they are not interactive; keep ora in the main thread and message it, which also keeps Ctrl+C responsive during CPU work.

Alternatives

PackageRegistryPick it when
yocto-spinnernpmYou want the same start/succeed/fail idea in a much smaller zero-frills package; ora's own README suggests it.
listr2npmYou have several tasks and want a live task list with nested spinners, output collapsing, and error states per task.
nanospinnernpmYou want one tiny dependency with a similar chainable API and do not need ora's detection edge cases.
cli-progressnpmYour work has measurable progress and users deserve a real bar with percentage and ETA instead of a spinner.