nib-ink Svelte Themes

Nib Ink

Svelte 5 terminal UI renderer. Like Ink (React) but for Svelte.

nib-ink

Svelte 5 terminal UI renderer.
Like Ink (React), but for Svelte.

npm

Install · Quick Start · Components · Hooks · Docs


Build rich, interactive terminal apps using Svelte 5 components, reactive state, and flexbox layout powered by Yoga.

<script>
  import { Box, Text, onInput } from 'nib-ink'

  let count = $state(0)

  onInput((input, key) => {
    if (key.upArrow) count++
    if (key.downArrow) count--
    if (input === 'q') process.exit(0)
  })
</script>

<Box flexDirection="column" padding={1} borderStyle="single">
  <Text bold>Counter: <Text color="cyan">{count}</Text></Text>
  <Text dimColor>up/down to change, q to quit</Text>
</Box>

Why nib-ink?

Svelte 5's reactivity ($state, $derived, $effect) is a perfect fit for terminal UIs. No virtual DOM diffing, no hooks rules, no re-render headaches. Just write components and let the compiler handle the rest.

nib-ink replaces the browser DOM with a lightweight fake DOM (TermNode), feeds it through Yoga for flexbox layout, and renders ANSI escape sequences to stdout. Svelte never knows the difference.

Features

  • Svelte 5 reactivity ($state, $derived, $effect) works unchanged
  • Flexbox layout via Yoga (padding, margin, gap, grow/shrink, wrapping)
  • Box borders (single, double, round, bold, classic, and more)
  • Colors (named, hex, RGB), text styles (bold, italic, dim, underline, strikethrough)
  • Keyboard and mouse input handling
  • Focus management (tab/shift+tab cycling, click-to-focus)
  • Scrollable containers with mouse wheel and scrollbar
  • Text wrapping (wrap, truncate, truncate-start, truncate-middle)
  • Static output area for logs and append-only content
  • Theme system with presets and variants
  • renderToString() for headless rendering and snapshots
  • Test harness for component testing
  • Dirty-region diffing (only redraws changed cells, not full screen)

Install

bun add nib-ink svelte

Requires Bun >= 1.0. Uses Bun's plugin system for Svelte compilation.

Setup

nib-ink needs a Bun plugin to compile .svelte files at import time. Create two files in your project root:

// svelte-loader.ts
import { plugin } from "bun";

plugin({
  name: "svelte",
  setup(build) {
    build.onLoad({ filter: /\.svelte$/ }, async (args) => {
      const { compile } = await import("svelte/compiler");
      const source = await Bun.file(args.path).text();
      const result = compile(source, {
        generate: "client",
        dev: false,
        filename: args.path,
        css: "injected",
      });
      return { contents: result.js.code, loader: "js" };
    });
  },
});
# bunfig.toml
preload = ["./svelte-loader.ts"]

Quick start

Create a component:

<!-- App.svelte -->
<script>
  import { Box, Text } from 'nib-ink'
</script>

<Box borderStyle="round" padding={1}>
  <Text color="green" bold>Hello from nib-ink!</Text>
  <Text>Svelte 5 in the terminal</Text>
</Box>

Mount it:

// index.ts
import { render } from 'nib-ink'
import App from './App.svelte'

render(App)

Run with --conditions=browser (required for Svelte 5 lifecycle hooks):

bun --conditions=browser index.ts

Important: always use --conditions=browser when running nib-ink apps. Without it, Svelte 5 lifecycle hooks (onMount, $effect, etc.) won't work and you'll get cryptic errors.

hello example

Components

Component Description
Box Flexbox container with borders, padding, margin, scroll, absolute positioning
Text Styled text with colors, bold, italic, dim, underline, wrapping/truncation
Newline Blank line
Spacer Flexible whitespace (pushes siblings apart)
Static Render-once area for logs and append-only output
Transform Apply text transformations to children

Hooks

Hook Description
onInput(callback) Keyboard input handler
onMouse(callback) Mouse click, scroll, and drag handler
getApp() App lifecycle (exit)
getFocus(options, callback) Register as focusable
getFocusManager() Control focus programmatically
getStdout() / getStderr() Direct stream access
log(...args) Write above the TUI (static area)
flushSync(fn?) Force synchronous re-render

render() API

const instance = render(App, props?, options?)

Options: stdout, stdin, exitOnCtrlC (default true), fps (default 30).

Instance methods: unmount(), rerender(), waitUntilExit(), clear().

Headless rendering

import { renderToString } from 'nib-ink'

const output = await renderToString(App, { name: 'world' }, { columns: 80 })

Testing

import { createTestHarness } from 'nib-ink'

const t = await createTestHarness(Counter, { count: 0 })
console.log(t.lastFrame())  // ANSI-stripped text output

t.stdin.write('j')           // simulate keypress
t.unmount()

How it works

Svelte components
       |
       v
  Compiled JS (svelte/internal/client)
       |
       v
  Fake DOM (TermNode tree)
       |
       v
  Yoga layout (flexbox)
       |
       v
  ANSI renderer --> stdout

Svelte 5 components compile to JS that calls svelte/internal/client DOM functions. nib-ink shims globalThis.document and window with a fake DOM before Svelte loads. Svelte's reactivity works unchanged, only the rendering target is replaced.

Examples

dashboard example

colors example

21 runnable examples included:

bun run example:hello          # minimal hello world
bun run example:counter        # interactive counter
bun run example:todo           # todo list with keyboard nav
bun run example:dashboard      # real-time system dashboard
bun run example:table          # sortable process table
bun run example:spinner        # build pipeline with spinners
bun run example:text-input     # form with text fields
bun run example:focus-demo     # tab/click focus navigation
bun run example:scroll         # scrollable log viewer
bun run example:mouse          # mouse click canvas
bun run example:theme          # theme switching

See all examples in examples/ or the examples doc.

Documentation

Architecture internals: docs/internals/

License

MIT

Top categories

Loading Svelte Themes