Render Markdown in the Terminal for Node CLIs and Coding Agents
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.
Print a Markdown file
Install the package, and shiki for code highlighting:
npm install @comark/ansi shikiThis script reads a file and prints it with highlighted code and math:
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:
widthsets the width of horizontal rules and code block headers. The default is80.colorsturns ANSI escape codes on or off. It isfalsewhen theNO_COLORenvironment variable is set.writerreceives the output string, for example to write tostderr.
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:
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 passedShow 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:
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
NO_COLOR environment variable, or pass colors: false. The output then contains no escape codes.renderAnsiFromDocument(). It renders without a parse step.h1 or a, in components. See overriding terminal output.Next steps
CMS and runtime content
Parse Markdown with components at runtime, store the JSON document, and render it in Vue, React, Svelte, Angular, or HTML with no build step.
Emails and RSS
Turn Markdown with components into HTML strings for emails and RSS feeds with @comark/html, including inline-styled components and CDATA output.