Render Markdown in the Terminal for Node CLIs and Coding Agents

Print Markdown as styled ANSI output in Node.js CLIs and coding agents, with highlighted code, tables, and alerts, using @comark/ansi.

Use @comark/ansi to turn Markdown into a string with ANSI styles. printAnsi() writes it to the terminal, and renderAnsi() returns it so you can write it yourself.

Coding agents and CLIs often print Markdown: release notes, help text, or model answers. Raw Markdown in a terminal shows ** and # characters. @comark/ansi styles headings, emphasis, lists, links, tables, GitHub alerts, and code blocks.

Install the package, and shiki for code highlighting:

Terminal
npm install @comark/ansi shiki

This script reads a file and prints it with highlighted code and math:

render.ts
import { readFile } from 'node:fs/promises'
import { printAnsi } from '@comark/ansi'
import shiki from '@comark/ansi/plugins/shiki'
import math, { Math } from '@comark/ansi/plugins/math'

const md = await readFile('source.md', 'utf-8')

await printAnsi(md, {
  plugins: [shiki(), math()],
  components: { Math },
})

Run it with node render.ts. The ANSI example contains a sample file with every supported element.

Control width, colors, and output

printAnsi() writes to stdout by default. These options change the output:

  • width sets the width of horizontal rules and code block headers. The default is 80.
  • colors turns ANSI escape codes on or off. It is false when the NO_COLOR environment variable is set.
  • writer receives the output string, for example to write to stderr.
cli.ts
import { createAnsiPrinter } from '@comark/ansi'
import shiki from '@comark/ansi/plugins/shiki'

const print = createAnsiPrinter({
  plugins: [shiki()],
  width: process.stdout.columns ?? 80,
  writer: output => process.stderr.write(output),
})

await print('> [!WARNING]\n> This command deletes the build cache.')
await print('| Step | Status |\n| --- | --- |\n| Build | Done |')

createAnsiPrinter() and createAnsiRenderer() set up the parser once. Use them when you print more than one document.

Render custom components

Map a component tag to a function that returns a string. The function receives the node and a render() helper for its children:

cli.ts
import { renderAnsi } from '@comark/ansi'

const output = await renderAnsi('::badge{type="success"}\nBuild passed\n::', {
  components: {
    badge: async ([, attrs, ...children], { render }) =>
      `[${String(attrs.type).toUpperCase()}] ${await render(children)}`,
  },
})
// [SUCCESS] Build passed

Show streaming model output

@comark/ansi renders complete strings, and it has no streaming option. Because autoClose closes unfinished syntax, you can render the partial text at any time.

The following code is a pattern, not a Comark API. It re-renders the accumulated text after each chunk, and redraws the screen:

agent.ts
import { streamText } from 'ai'
import { createAnsiRenderer } from '@comark/ansi'

const render = createAnsiRenderer({ width: process.stdout.columns ?? 80 })
const result = streamText({ model: 'anthropic/claude-sonnet-4.6', prompt: 'Explain git rebase.' })

let text = ''
for await (const chunk of result.textStream) {
  text += chunk
  // Clear the screen, move the cursor to the top, then print the new frame
  process.stdout.write('\x1b[2J\x1b[H' + await render(text))
}

A full redraw replaces the scrollback on each frame. For long answers, print the final output once when the stream ends.

FAQ

Next steps

© 2026 Vercel, Inc.