npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@firekid/ink

v0.1.1

Published

Beautiful terminal output for Node.js. Colors, spinners, boxes, tables, progress bars and banners. Zero dependencies.

Downloads

8

Readme

@firekid/ink

CI npm version npm downloads bundle size TypeScript License: MIT

Beautiful terminal output for Node.js. Colors, spinners, boxes, tables, progress bars, and banners — all in one package. Zero dependencies. Full TypeScript. ESM + CJS.


Why

Most Node.js projects end up installing four or five separate packages just to get basic terminal output looking good:

npm install chalk ora boxen cli-table3

Every one of those has different APIs, different release cycles, and different ESM/CJS situations. Some have dropped CommonJS entirely. Some haven't been touched in years.

@firekid/ink replaces all of them with one consistent API.


Installation

npm install @firekid/ink
yarn add @firekid/ink
pnpm add @firekid/ink

Colors — Two Ways

The tag template (recommended for full lines)

The c tag template reads like text. You can see exactly what the output will look like just by reading the code.

import { c } from '@firekid/ink'

console.log(c`<red>error</red>`)
console.log(c`<green bold>success</green>`)
console.log(c`<red bold underline>critical</red>`)
console.log(c`<hex:#ff6b6b>custom color</hex>`)
console.log(c`<bg:blue white>background</bg>`)

// mix styled and unstyled text in one line
console.log(c`Status: <green>ok</green> — Time: <yellow>234ms</yellow>`)

// interpolate values naturally
const file = 'app.js'
const line = 42
console.log(c`at <cyan>${file}</cyan>:<yellow>${line}</yellow>`)

Compare with chalk:

// chalk — hard to read, method chains everywhere
console.log(chalk.red.bold('[ERROR]') + ' ' + chalk.white(message) + ' ' + chalk.dim(file) + ':' + chalk.yellow(line))

// ink — reads like the output itself
console.log(c`<red bold>[ERROR]</red> ${message} <dim>${file}</dim>:<yellow>${line}</yellow>`)

Named functions (recommended for single values)

import { red, green, yellow, cyan, bold, dim } from '@firekid/ink'

console.log(red('error'))
console.log(green('success'))
console.log(bold('important'))
console.log(dim('secondary'))

When to Use Which

// one word or one value  →  named function
const level = { error: red, warn: yellow, info: green }
console.log(level[log.level](log.level.toUpperCase()))

// full line with mixed content  →  c tag template
console.log(c`<dim>[${timestamp}]</dim> <bold>${method}</bold> ${path} <green>${status}</green> <dim>${ms}ms</dim>`)

All Available Tags

Colors

<black>   <red>   <green>   <yellow>   <blue>   <magenta>   <cyan>   <white>   <gray>

Bright colors

<redBright>   <greenBright>   <yellowBright>   <blueBright>
<magentaBright>   <cyanBright>   <whiteBright>

Styles

<bold>   <dim>   <italic>   <underline>   <strikethrough>   <inverse>

Background colors

<bg:black>   <bg:red>   <bg:green>   <bg:yellow>
<bg:blue>   <bg:magenta>   <bg:cyan>   <bg:white>

Custom hex color

<hex:#ff6b6b>text</hex>
<hex:#00ff00>text</hex>

Combining modifiers

When you combine multiple modifiers in one tag, they are applied innermost-first. For <red bold>text</red>, bold wraps the text first, then red wraps that result. In practice this produces correct output for all standard combinations, but it is worth knowing if you are doing something unusual with conflicting modifiers.

c`<red bold underline>critical</red>`
c`<cyan italic>note</cyan>`
c`<bg:blue white bold>header</bg>`

Spinner

import { spin } from '@firekid/ink'

const spinner = spin('Loading users...')

const users = await fetchUsers()

spinner.succeed('Users loaded')
spinner.fail('Failed to load users')
spinner.warn('Loaded with warnings')
spinner.info('No users found')

// update the message while running
spinner.text('Still loading...')

// stop without a completion message
spinner.stop()

The spinner writes to stderr by default, so it does not pollute piped stdout output. In non-TTY environments (CI, piped output) it prints a static line instead of animating. SIGINT and SIGTERM are handled automatically — the spinner cleans up the cursor and exits gracefully.


Box

import { box } from '@firekid/ink'

console.log(box('Hello World'))
// ┌─────────────┐
// │ Hello World │
// └─────────────┘

console.log(box('Deployment complete', { title: 'Success', border: 'round' }))
// ╭─ Success ─────────────────╮
// │ Deployment complete       │
// ╰───────────────────────────╯

// border options: 'single' | 'double' | 'round' | 'bold' | 'none'
// align options: 'left' | 'center' | 'right'
// color: any named color

console.log(box('All tests passed', {
  title: 'Results',
  border: 'double',
  align: 'center',
  color: 'green',
}))

Table

import { table } from '@firekid/ink'

console.log(table({
  head: ['Function', 'Status', 'Duration'],
  rows: [
    ['processUser',  'pass', '12ms'],
    ['fetchData',    'fail', '340ms'],
    ['validateForm', 'pass', '4ms'],
  ]
}))
// ┌───────────────┬────────┬──────────┐
// │ Function      │ Status │ Duration │
// ├───────────────┼────────┼──────────┤
// │ processUser   │ pass   │ 12ms     │
// ├───────────────┼────────┼──────────┤
// │ fetchData     │ fail   │ 340ms    │
// ├───────────────┼────────┼──────────┤
// │ validateForm  │ pass   │ 4ms      │
// └───────────────┴────────┴──────────┘

Progress Bar

import { progress } from '@firekid/ink'

const bar = progress({ total: 100 })

for (let i = 0; i <= 100; i++) {
  bar.tick()
  await sleep(50)
}

bar.done()

// update by ratio instead of ticking
bar.update(0.5)  // jump to 50%

Both tick() and update() automatically call done() when the bar reaches 100%. Any calls after done() are no-ops.


Banner

import { banner } from '@firekid/ink'

console.log(banner('@firekid'))
//  ██████  ███████ ██ ██████  ███████ ██  ██ ██ ██████
//  ██      ██      ██ ██   ██ ██      ██ ██  ██ ██   ██
//  ██████  ███████ ██ ██████  █████   ████   ██ ██   ██
//  ██          ██  ██ ██   ██ ██      ██ ██  ██ ██   ██
//  ██████  ███████ ██ ██   ██ ███████ ██  ██ ██ ██████

console.log(banner('ink', { color: 'cyan' }))

Note: banner() supports a maximum of 20 characters. Passing more will throw a RangeError. If you need to truncate intentionally, use text.slice(0, 20) before passing it in.

Supported characters: A–Z, 0–9, and @ / - . ! space. Lowercase input is automatically uppercased.


Utilities

import { strip, visibleLength, supportsColor } from '@firekid/ink'

// remove all ANSI codes from a string
strip('\x1b[31mhello\x1b[39m')
// 'hello'

// get the visible length of a string (ignores ANSI codes)
visibleLength(red('hello'))
// 5

// check if the current terminal supports colors
supportsColor()
// true or false

Color Detection

@firekid/ink respects the standard environment variables:

| Variable | Effect | |---|---| | NO_COLOR=1 | Disables all color output | | FORCE_COLOR=1 | Forces color output even in non-TTY environments |

When running in a non-TTY environment (piped output, CI, Docker logs), colors are automatically disabled unless FORCE_COLOR is set.


Default Export

All methods are available on the default export:

import ink from '@firekid/ink'

ink.c`<red>error</red>`
ink.red('error')
ink.bold('important')
ink.spin('Loading...')
ink.box('Hello', { border: 'round' })
ink.table({ head: ['A', 'B'], rows: [['1', '2']] })
ink.progress({ total: 100 })
ink.banner('ink')
ink.strip('\x1b[31mhello\x1b[39m')
ink.visibleLength(ink.red('hello'))
ink.supportsColor()

Replacing Other Packages

| Before | After | |---|---| | chalk.red.bold('error') | c\error`orred('error')| |ora('Loading').start()|spin('Loading')| |boxen('text', { title: 'T' })|box('text', { title: 'T' })| |new Table({ head: [...] })|table({ head: [...], rows: [...] })| |figlet.textSync('text')|banner('text')` |


Comparison

| Feature | ink | chalk | ora | boxen | |---|---|---|---|---| | Zero dependencies | yes | yes | no | no | | TypeScript built-in | yes | yes | yes | yes | | ESM + CJS | yes | ESM only (v5+) | yes | ESM only (v7+) | | Tag template API | yes | no | no | no | | Colors | yes | yes | no | partial | | Spinners | yes | no | yes | no | | Boxes | yes | no | no | yes | | Tables | yes | no | no | no | | Progress bars | yes | no | no | no | | Banners | yes | no | no | no | | NO_COLOR support | yes | yes | yes | yes |


Environment Support

| Runtime | Support | |---|---| | Node.js 18+ | yes | | Deno | yes | | Bun | yes |

Exports both ESM (import) and CommonJS (require).


License

MIT


Built by Firekid