@firekid/ink
v0.1.1
Published
Beautiful terminal output for Node.js. Colors, spinners, boxes, tables, progress bars and banners. Zero dependencies.
Downloads
8
Maintainers
Readme
@firekid/ink
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-table3Every 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/inkColors — 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 falseColor 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
