ora vs nanospinner vs cli-spinners: Terminal Spinners in Node.js (2026)
TL;DR
ora is the most popular terminal spinner — elegant API, promise support, multiple spinner styles, text updates, and TTY detection. nanospinner is the ultra-lightweight alternative — 15x smaller than ora, same core API, zero dependencies. cli-spinners is just the spinner animation data — 80+ spinner frame definitions, no rendering logic, used as a data source by ora and other tools. In 2026: ora for feature-rich CLI tools, nanospinner for size-conscious projects, cli-spinners if you're building your own renderer.
Key Takeaways
- ora: ~20M weekly downloads — elegant spinner with promise support, text updates, colors
- nanospinner: ~8M weekly downloads — 1 KB, zero deps, same core API as ora
- cli-spinners: ~15M weekly downloads — spinner animation data only, no rendering
- ora uses cli-spinners internally for its 80+ spinner styles
- nanospinner includes only one spinner style but is tiny (~1 KB vs ~15 KB)
- Both ora and nanospinner auto-detect non-TTY environments (CI, pipes)
ora
ora — elegant terminal spinner:
Basic usage
import ora from "ora"
const spinner = ora("Loading packages...").start()
// Simulate async work:
await fetchPackages()
spinner.succeed("Loaded 1,234 packages")
// ✔ Loaded 1,234 packages
Text updates
const spinner = ora("Fetching data...").start()
spinner.text = "Processing 500 packages..."
await processFirst500()
spinner.text = "Processing remaining packages..."
await processRemaining()
spinner.succeed("All 1,234 packages processed")
Promise integration
import ora from "ora"
// Wraps a promise — starts spinner, resolves = succeed, rejects = fail:
const packages = await ora.promise(
fetchPackages(),
{ text: "Fetching packages..." }
)
// Or with custom success/fail text:
await ora.promise(deployToProduction(), {
text: "Deploying to production...",
successText: "Deployed successfully!",
failText: "Deployment failed",
})
Spinner styles
import ora from "ora"
// Default (dots):
ora({ text: "Loading...", spinner: "dots" }).start()
// ⠋ Loading...
// Other styles:
ora({ text: "Loading...", spinner: "line" }).start()
// - Loading...
ora({ text: "Loading...", spinner: "bouncingBar" }).start()
// [ ▖] Loading...
ora({ text: "Loading...", spinner: "earth" }).start()
// 🌍 Loading...
// Custom frames:
ora({
text: "Building...",
spinner: {
interval: 100,
frames: ["◐", "◓", "◑", "◒"],
},
}).start()
Colors and prefixes
import ora from "ora"
const spinner = ora({
text: "Installing dependencies...",
color: "cyan",
prefixText: "[npm]",
}).start()
// Status methods:
spinner.succeed("Dependencies installed") // ✔ (green)
spinner.fail("Installation failed") // ✖ (red)
spinner.warn("Peer dependency warnings") // ⚠ (yellow)
spinner.info("Using cached packages") // ℹ (blue)
spinner.stop() // Stop without symbol
spinner.clear() // Clear spinner line
Non-TTY handling
import ora from "ora"
// ora auto-detects non-interactive environments:
// In CI/pipes: just prints text, no animation
// In TTY: full animated spinner
const spinner = ora({
text: "Building...",
isSilent: false, // Set true to suppress all output
discardStdin: true, // Don't buffer stdin while spinning
}).start()
// Force non-spinner mode:
const spinner2 = ora({
text: "Building...",
isEnabled: false, // Disable spinner animation
}).start()
nanospinner
nanospinner — ultra-lightweight spinner:
Basic usage
import { createSpinner } from "nanospinner"
const spinner = createSpinner("Loading packages...").start()
await fetchPackages()
spinner.success({ text: "Loaded 1,234 packages" })
// ✔ Loaded 1,234 packages
Status methods
import { createSpinner } from "nanospinner"
const spinner = createSpinner("Processing...").start()
// Update text:
spinner.update({ text: "Processing 500 of 1,234..." })
// Final status:
spinner.success({ text: "Done!" }) // ✔ Done!
spinner.error({ text: "Failed!" }) // ✖ Failed!
spinner.warn({ text: "Warning!" }) // ⚠ Warning!
spinner.stop() // Stop without symbol
spinner.clear() // Clear line
spinner.reset() // Reset to initial state
Custom mark and color
import { createSpinner } from "nanospinner"
const spinner = createSpinner("Deploying...", {
color: "cyan",
mark: "🚀", // Custom success mark
}).start()
await deploy()
spinner.success() // 🚀 Deploying...
Why nanospinner
nanospinner vs ora:
Size: ~1 KB vs ~15 KB
Dependencies: 0 vs 5+ (cli-spinners, chalk, etc.)
API: Nearly identical (start, success, error, warn, stop)
Missing from nanospinner:
❌ Promise wrapper (ora.promise)
❌ 80+ spinner styles (nanospinner has 1 style)
❌ prefixText / suffixText
❌ indent option
nanospinner is best when:
✅ Bundle size matters (CLIs distributed via npm)
✅ Simple spinner needs (start → status → done)
✅ Zero dependency preference
cli-spinners
cli-spinners — spinner animation data:
What it provides
import spinners from "cli-spinners"
// Just data — no rendering:
console.log(spinners.dots)
// { interval: 80, frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] }
console.log(spinners.line)
// { interval: 130, frames: ["-", "\\", "|", "/"] }
console.log(spinners.bouncingBar)
// { interval: 80, frames: ["[ ]", "[ =]", "[ ==]", "[ ===]", ...] }
// Available spinners:
console.log(Object.keys(spinners).length)
// → 80+ different spinner animations
Build your own spinner
import spinners from "cli-spinners"
function createSimpleSpinner(text: string, style = "dots") {
const { frames, interval } = spinners[style]
let i = 0
let timer: NodeJS.Timeout
return {
start() {
timer = setInterval(() => {
process.stdout.write(`\r${frames[i++ % frames.length]} ${text}`)
}, interval)
},
stop(finalText?: string) {
clearInterval(timer)
process.stdout.write(`\r✔ ${finalText ?? text}\n`)
},
}
}
// Usage:
const spinner = createSimpleSpinner("Building...", "earth")
spinner.start()
await build()
spinner.stop("Built successfully!")
Available spinner styles
Popular styles from cli-spinners:
dots ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏
line -\|/
star ✶✸✹✺✹✷
earth 🌍🌎🌏
moon 🌑🌒🌓🌔🌕🌖🌗🌘
runner 🚶🏃
arrow ←↖↑↗→↘↓↙
clock 🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛
bounce ⠁⠂⠄⠂
arc ◜◠◝◞◡◟
toggle ⊶⊷
See full list: https://jsfiddle.net/sindresorhus/2eLtsbey/embedded/result/
Practical Patterns
Sequential tasks
import ora from "ora"
async function buildProject() {
const spinner = ora()
spinner.start("Linting...")
await lint()
spinner.succeed("Lint passed")
spinner.start("Running tests...")
await test()
spinner.succeed("Tests passed (42 tests)")
spinner.start("Building...")
await build()
spinner.succeed("Built in 2.3s")
spinner.start("Deploying...")
await deploy()
spinner.succeed("Deployed to production")
}
With @clack/prompts
import * as p from "@clack/prompts"
// @clack/prompts has its own spinner — better for interactive CLIs:
const s = p.spinner()
s.start("Installing dependencies...")
await installDeps()
s.stop("Dependencies installed")
s.start("Building project...")
await build()
s.stop("Built successfully")
Conditional spinners (CI detection)
import ora from "ora"
function createLogger() {
const isCI = process.env.CI || !process.stdout.isTTY
if (isCI) {
// In CI — just log text, no spinners:
return {
start: (text: string) => console.log(`→ ${text}`),
succeed: (text: string) => console.log(`✓ ${text}`),
fail: (text: string) => console.error(`✗ ${text}`),
}
}
// Interactive terminal — use ora:
const spinner = ora()
return {
start: (text: string) => spinner.start(text),
succeed: (text: string) => spinner.succeed(text),
fail: (text: string) => spinner.fail(text),
}
}
Feature Comparison
| Feature | ora | nanospinner | cli-spinners |
|---|---|---|---|
| Rendering | ✅ | ✅ | ❌ (data only) |
| Spinner styles | 80+ | 1 | 80+ (data) |
| Promise wrapper | ✅ | ❌ | ❌ |
| Text updates | ✅ | ✅ | N/A |
| Colors | ✅ (chalk) | ✅ (built-in) | N/A |
| Non-TTY detection | ✅ | ✅ | N/A |
| Dependencies | 5+ | 0 | 0 |
| Size | ~15 KB | ~1 KB | ~3 KB |
| Weekly downloads | ~20M | ~8M | ~15M |
When to Use Each
Use ora if:
- Building a polished CLI tool with multiple spinner styles
- Need promise wrapping (
ora.promise) - Want prefix/suffix text, indent, and custom symbols
- Don't mind the ~15 KB size
Use nanospinner if:
- Want the smallest spinner library (~1 KB)
- Simple spinner needs (start → update → done)
- Zero dependency preference
- Building size-sensitive CLI tools
Use cli-spinners if:
- Building your own spinner renderer
- Need the raw animation frame data
- Custom rendering requirements (concurrent spinners, custom layout)
- Already using a different rendering library
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on ora v8.x, nanospinner v1.x, and cli-spinners v3.x.