mrkeyoor.com_
Sat 08 Aug 22:52 UTC
npmCLI & Toolingupdated 08 Aug 2026

nanospinner

Nanospinner is a small Node.js terminal status spinner for command-line programs. You create one spinner, start it while an asynchronous job runs, then replace the animation with a success, error, warning, info, or custom final line. It ships CommonJS code plus TypeScript declarations and depends only on picocolors. Its deliberately narrow API covers one active spinner, custom frames, colors, timing, text updates, and output streams, without the task orchestration or multi-spinner display found in larger CLI libraries.

Verdict

A good fit for one simple spinner when tiny scope matters and its signal and TTY choices match your CLI. Install Ora or a task runner when output coordination, multiple tasks, or finer terminal behavior matters more than minimalism.

API stability4/5The 1.x surface is small and easy to understand: createSpinner returns methods for starting, stopping, updating, rendering, and choosing a final status. Releases 1.2.1 and 1.2.2 repaired an accidentally missing Spinner type export and stop text behavior after 1.2.0, showing that compatibility bugs are fixed, but also that even this narrow API saw a TypeScript regression during a minor release.
Docs3/5The README lists every public method and gives short examples for start, stop, success, warning, error, info, update, clear, and reset. It does not document isSpinning, write, render, or loop even though they are public in the declaration file, and it omits consequential behavior found in the implementation, including process signal handlers, stdout-based TTY detection, non-TTY output, and the ignored stream field in update().
Maintenance3/5Version 1.2.2 was published in December 2024 after fixes to the exported Spinner type, isSpinning, and final stop text, so the project is not abandoned or archived. The repository has not received a source push since December 10, 2024, npm lists one maintainer, and the repository currently reports four open issues and pull requests. That is acceptable for a tiny stable utility, but it leaves little redundancy if terminal behavior needs a fix.
Ecosystem3/5The package recorded 2,961,500 downloads for the week ending August 6, 2026 and supports both require() and ESM named-import interoperability, with TypeScript declarations included. Its ecosystem is intentionally narrow: one runtime dependency, no plugin API, no integrations, and no multi-spinner facility. Most surrounding CLI tooling targets broader alternatives such as Ora, so adoption does not translate into a large extension community.

Use it if

  • You need one compact status spinner around a command-line task and do not need nested or concurrent task rendering
  • Your project consumes CommonJS, or uses Node ESM interoperability, and wants bundled TypeScript declarations
  • You want to choose custom frames, timing, color, text, and the output stream without bringing in a full prompt framework
  • You want non-TTY runs to produce a single plain status line instead of an animation loop
Skip it if

Setup reality

Installation is just npm install nanospinner. There are no peer dependencies, native extensions, credentials, or configuration files; picocolors is the sole runtime dependency, and the package includes declaration files. The surprises are runtime behavior rather than installation. Output defaults to process.stderr, but version 1.2.2 computes its TTY flag once from stdout file descriptor 1. Redirecting stdout can therefore disable animation even when stderr is attached to a terminal, and selecting process.stdout or another stream does not recalculate that decision. In a non-TTY or CI process, start() renders one hyphen-prefixed line and does not schedule more frames, which is useful for logs but may not match snapshot expectations. While active, the spinner adds SIGINT and SIGTERM listeners that stop it and call process.exit; terminal applications with their own shutdown flow need to stop the spinner before taking over signal handling. Choose the stream when calling createSpinner because update() accepts a stream in its TypeScript option shape but the 1.2.2 implementation does not apply that field. Also note that clear() directly writes ANSI cursor controls even outside a TTY, reset() cancels the timer but does not remove signal listeners, and calling stop() with no argument clears the animation without printing a final message. Prefer success(), error(), warn(), info(), or stop('message') on every completion path so the timer and signal listeners are cleaned up.

Patterns

Wrap one asynchronous taskshow-basic-spinner

import { createSpinner } from 'nanospinner'

const spinner = createSpinner('Downloading release').start()
await downloadRelease()
spinner.success({ text: 'Release downloaded' })

Call a finishing method on every path. It clears the timer, removes the package's signal listeners, restores the cursor in a TTY, and prints the final line.

Finish with success or errorreport-task-failure

const spinner = createSpinner('Publishing').start()

try {
  await publish()
  spinner.success('Published')
} catch (error) {
  spinner.error('Publish failed')
  throw error
}

error() only changes the terminal status. It does not throw, set process.exitCode, or preserve the caught error for you.

Change progress text while runningupdate-spinner-text

const spinner = createSpinner('Reading manifest').start()

const manifest = await readManifest()
spinner.update(`Installing ${manifest.dependencies.length} packages`)
await installDependencies(manifest)
spinner.success('Dependencies installed')

update() changes future renders. Pass the output stream to createSpinner instead; version 1.2.2 ignores the stream field when supplied to update().

Use custom frames and timingcustomize-animation

const spinner = createSpinner('Waiting for worker', {
  frames: ['.', '..', '...'],
  interval: 120,
  color: 'cyan',
}).start()

await waitForWorker()
spinner.success('Worker ready')

An empty frames array falls back to the built-in frames, and an interval of 0 falls back to 50 ms because the implementation uses truthy defaults.

Write spinner output to stdoutchoose-output-stream

const spinner = createSpinner('Generating report', {
  stream: process.stdout,
}).start()

await generateReport()
spinner.success('Report generated')

The default stream is stderr. TTY capability is still calculated globally from stdout when the module loads, regardless of the stream selected here.

Show a warning resultfinish-with-warning

const spinner = createSpinner('Checking configuration').start()
const missing = await findOptionalSettings()

if (missing.length) {
  spinner.warn(`Missing optional settings: ${missing.join(', ')}`)
} else {
  spinner.success('Configuration complete')
}

warn() stops the spinner and uses the built-in yellow warning symbol; it does not write to a separate warning channel.

End with an informational statusfinish-with-info

const spinner = createSpinner('Looking for updates').start()
const update = await findUpdate()

update
  ? spinner.info(`Version ${update.version} is available`)
  : spinner.success('Already up to date')

info() is a terminal presentation method, not a logger. The final line goes to the spinner's configured stream.

Stop with a custom mark and coloruse-custom-final-mark

const spinner = createSpinner('Skipping cached files').start()
await inspectCache()
spinner.stop({
  text: 'Cache is current',
  mark: '=',
  color: 'gray',
})

stop() with no argument clears the active output but prints no final line. Pass a string or object when the result should remain visible.

Avoid finishing an inactive spinnercheck-spinner-state

const spinner = createSpinner('Connecting').start()

try {
  await connect()
  if (spinner.isSpinning()) spinner.success('Connected')
} finally {
  if (spinner.isSpinning()) spinner.stop('Connection cancelled')
}

isSpinning() reflects the package's internal flag. It becomes false after stop, success, error, warn, info, or reset.

Reuse a completed spinnerreuse-spinner-instance

const spinner = createSpinner('Step one').start()
await stepOne()
spinner.success('Step one done')

spinner.reset().start('Step two')
await stepTwo()
spinner.success('Step two done')

Finish the first run before reset. reset() clears the timer but does not itself remove SIGINT and SIGTERM listeners installed by start().

Load nanospinner from CommonJSuse-commonjs

const { createSpinner } = require('nanospinner')

const spinner = createSpinner('Building').start()
build().then(
  () => spinner.success('Built'),
  error => {
    spinner.error('Build failed')
    throw error
  },
)

The published entry point is CommonJS. ESM named imports work through Node interoperability, while require() is the package's native module shape.

Skip spinner creation in machine-readable modedisable-spinner-explicitly

const quiet = process.env.CI || process.argv.includes('--json')
const spinner = quiet ? null : createSpinner('Loading').start()

const data = await loadData()
spinner?.success('Loaded')
if (process.argv.includes('--json')) console.log(JSON.stringify(data))

Nanospinner has no enabled option. It emits one plain line outside a TTY, so bypass it explicitly when stdout must contain only JSON or another machine-readable format.

Alternatives

PackageRegistryPick it when
oranpmChoose it for a fuller spinner API, established terminal edge-case handling, prefixes, indentation, and broader presentation controls
yocto-spinnernpmChoose it when you want another tiny single-spinner package with a newer release line and an ESM-first API
cli-spinnersnpmChoose it when you only need a catalog of frame sequences and will own the rendering loop yourself