Skip to main content

ora vs nanospinner vs cli-spinners: Terminal Spinners in Node.js (2026)

·PkgPulse Team

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

Featureorananospinnercli-spinners
Rendering❌ (data only)
Spinner styles80+180+ (data)
Promise wrapper
Text updatesN/A
Colors✅ (chalk)✅ (built-in)N/A
Non-TTY detectionN/A
Dependencies5+00
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.

Compare CLI utilities and developer tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.