CLI tool
sindresorhus/ora avatar
sindresorhus/ora

ora: a terminal spinner for Node.js CLIs that knows when to stay quiet

Elegant terminal spinner

9,757 stars294 forksJavaScriptMIT

At a glance

What is it?
ora is an ESM-only terminal spinner for Node.js 20 and later, published under MIT. Its real design decision is not the animation: it is the detection of TTY, CI and non-interactive output that decides whether anything is drawn at all.
Who is it for?
Adopt ora when you are writing a Node.js 20 or later CLI in ESM and you want spinner states that map to success, failure, warning and info without hand-rolling escape codes. Do not adopt it for a browser bundle, a CommonJS-only toolchain, or a script whose only output is piped into another process, since the spinner is skipped there anyway.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 11 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What ora actually solves for a Node.js CLI author

A command line tool that talks to a network or a disk spends most of its wall clock time waiting. Printing nothing during that wait looks like a hang. Printing a line per step floods the scrollback. ora sits in the middle: it owns one line of the terminal, redraws a frame on an interval, and then replaces that line with a final symbol and message when the work ends.

The audience is narrow and specific. ora is a Node.js package, published as ESM only, with engines set to node >=20 in package.json. It is aimed at people writing interactive command line tools in JavaScript or TypeScript, not at people writing shell scripts or Python programs. The README's own example is a single import and a start call, which is the whole surface most users touch.

The part that is easy to miss is that ora is not only an animation library. It is a small state machine over terminal output, with terminal methods for the four outcomes a CLI usually needs: succeed, fail, warn and info. Those methods stop the spinner, swap the frame for a symbol from log-symbols, and persist the text so it survives in the scrollback. That is the feature most people actually want, and it is the reason a hand-rolled setInterval over process.stderr tends to be replaced by this package.

How the spinner decides whether to render at all

The default stream is process.stderr, not stdout. The README documents this explicitly, along with the note that you can point it at process.stdout instead. The default matters: a program that pipes its real output somewhere else can still show progress on the terminal without contaminating the pipe.

Enablement is computed from the environment. According to the README, the spinner is enabled when the stream is inside a TTY context (not spawned or piped) and not in a CI environment, unless isEnabled is set explicitly. So in a GitHub Actions log, ora draws nothing animated. The README is careful about what that means: isEnabled: false does not suppress output entirely, it stops the spinner, the colors and the other ANSI escape codes, and still logs text. isSilent is the stronger switch, and it suppresses all output and forces isEnabled to false.

The frame data comes from cli-spinners, which the README exposes through the spinner option. The default is 'dots', and spinners carry their own recommended interval, so the interval option is rarely needed. A custom spinner is an object with a frames array and an optional interval. There is a platform carve-out worth knowing before you pick an animation: on Windows, except for Windows Terminal, ora always uses the line spinner, because the README states the Windows command line does not have proper Unicode support. The dependency list in package.json shows how the environment checks are assembled: is-interactive, is-unicode-supported, cli-cursor, string-width and stdin-discarder each handle one piece rather than one large terminal abstraction.

Installing ora and running a first spinner

The install step is one npm command. The README gives it without a version pin.

bash
npm install ora

Because package.json declares "type": "module" and the exports map only exposes index.js and index.d.ts, you import it rather than require it. The README's usage example creates a spinner with text, starts it, and mutates the instance one second later.

js
import ora from 'ora';

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

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

Run that file with Node 20 or later and you should see a cyan dots animation on stderr whose label turns yellow and changes text after a second. Nothing stops it, so add a terminal state yourself. The four outcome methods are the idiomatic ending.

js
import ora from 'ora';

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

spinner.succeed();

If you would rather not wire the try/catch by hand, the package exports oraPromise, which the README describes as starting a spinner for a promise or promise-returning function, calling succeed on fulfillment and fail on rejection, and returning the promise. The README's example is a single awaited call, and the options extend the normal option set with successText and failText, each of which can be a string or a function of the result or the error.

The discardStdin trade-off and other limits worth knowing

The option with the sharpest edge is discardStdin, which defaults to true. The README explains why it exists: while the spinner runs, stdin input other than Ctrl+C is discarded if stdin is a TTY, which prevents the spinner from twitching on input, prevents broken lines on Enter, and prevents input buffering during the animation.

The cost is stated plainly in the README. discardStdin puts stdin into raw mode, and in raw mode Ctrl+C no longer generates SIGINT from the terminal. ora re-emits Ctrl+C from stdin input, but if your code blocks the event loop with synchronous work, Ctrl+C handling is delayed until that blocking work ends. The README's suggested remedies are async APIs, a worker thread or a child process, or setting discardStdin to false. This is a real failure mode for a CLI that does a long synchronous parse or a tight computation loop: the user presses Ctrl+C, nothing happens, and the process keeps running. The README also notes that discardStdin has no effect on Windows, because there is no good way to implement stdin discarding there.

Two smaller constraints. First, ora is ESM only, so a CommonJS codebase cannot require it without a loader or a build step. Second, the project points readers at yocto-spinner in the install section as a smaller alternative, which is a fair signal that ora is not trying to be the minimal option. If your tool already depends on Ink or another renderer that owns the terminal, adding ora means two things writing to the same stream.

ora against yocto-spinner and Ink

The README itself names yocto-spinner as the smaller alternative, by the same author. The difference in approach is scope. ora ships a broader option surface (prefixText, suffixText, indent, discardStdin, isSilent, a writable stream override, stopAndPersist with its own symbol and text overrides) and a longer dependency list, including chalk, cli-cursor, cli-spinners, is-interactive, is-unicode-supported, log-symbols, stdin-discarder and string-width. yocto-spinner exists for people who want the animation and not the surrounding policy, and the trade is that you give up the parts of the option set you would otherwise get for free.

A different comparison is Ink, which appears in the related searches around this package. Ink is a React renderer for the terminal: you describe a tree of components and Ink owns the layout and the redraw. ora owns exactly one line and knows nothing about layout. If your interface is a single progress line, ora is the smaller dependency and the smaller mental model. If you are building a multi-pane interface with live regions, a spinner library is the wrong layer, and you would be fighting it rather than using it. The same logic applies to Listr2, which composes task lists: ora is a single indicator, not a task runner.

Maintenance, licence and what upgrading costs

The repository is not archived, and the last push was on 2026-09-18. The recent release history shows v9.4.1 on 2026-06-22, v9.4.0 on 2026-04-22 and v9.3.0 on 2026-02-05, so the current line is receiving patch and minor releases rather than sitting still.

The licence is MIT, declared in package.json and in the license file at the repository root. MIT is permissive: it allows use, modification and redistribution provided the copyright notice and permission notice are kept. That is a description of the terms, not legal advice; if your organisation has a policy on third-party licences, the file to read is license in the repository root.

The upgrade cost is dominated by the ESM-only packaging and the Node floor. package.json sets engines to node >=20, and the exports map exposes only index.js and index.d.ts. A consumer on Node 18 cannot use the current line without changing runtimes, and a CommonJS consumer cannot require it. Within the 9.x line the option names documented in the README are the stable surface: text, prefixText, suffixText, spinner, color, hideCursor, indent, interval, stream, isEnabled, isSilent and discardStdin. The instance getters and setters for text, prefixText, suffixText, color, spinner and indent are what most code touches during an upgrade, and those have stayed in the same shape across the documented releases.

Editorial conclusion

Adopt ora when you are writing a Node.js 20 or later CLI in ESM and you want spinner states that map to success, failure, warning and info without hand-rolling escape codes. Do not adopt it for a browser bundle, a CommonJS-only toolchain, or a script whose only output is piped into another process, since the spinner is skipped there anyway. Before committing, check three things in your own code: that package.json has "type": "module", that Node is at least 20, and whether any long synchronous loop runs while the spinner is active, because with the default discardStdin the README states that Ctrl+C handling waits for that loop to end.

Frequently asked questions

How do I install ora and use it in a Node.js script?

Install it with npm install ora, then import it with a default import since the package is ESM only. The README's example creates a spinner with ora('Loading unicorns').start() and can change spinner.color and spinner.text on the running instance.

Why does the ora spinner not appear in my CI logs?

The README states that the spinner is enabled only when the stream is inside a TTY context and not in a CI environment, unless isEnabled is set explicitly. Setting isEnabled to false does not remove all output: it still logs text, just without the spinner, colors and other ANSI escape codes. Use isSilent to suppress everything.

Does ora work with CommonJS require()?

No. package.json declares "type": "module" and the exports map points only at index.js and index.d.ts, so the package is consumed through import. The same file sets engines to node >=20, so the runtime floor is Node.js 20.

Why is Ctrl+C unresponsive while an ora spinner is running?

The README explains that discardStdin, which defaults to true, puts stdin into raw mode, where Ctrl+C no longer generates SIGINT from the terminal. ora re-emits Ctrl+C from stdin input, but synchronous work that blocks the event loop delays that handling. The README suggests async APIs, a worker thread, a child process, or setting discardStdin to false.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. sindresorhus/ora on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sindresorhus-ora.svg)](https://hysenlabs.com/projects/sindresorhus-ora)