Markdown
Markdown renders document structure and can use Tree-sitter highlighting in fenced code blocks. Use Code when the full content is source code.
Availability#
| Field | Availability |
|---|---|
| Package | @opentui/core |
| Core renderable | MarkdownRenderable |
| React | <markdown> (automatic) |
| Solid | <markdown> (automatic) |
| Status | Built in |
Basic usage#
Renderable API#
import { MarkdownRenderable, SyntaxStyle, RGBA, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
"markup.heading.1": { fg: RGBA.fromHex("#58A6FF"), bold: true },
"markup.list": { fg: RGBA.fromHex("#FF7B72") },
"markup.raw": { fg: RGBA.fromHex("#A5D6FF") },
default: { fg: RGBA.fromHex("#E6EDF3") },
})
const markdown = new MarkdownRenderable(renderer, {
id: "readme",
width: 60,
content: "# Hello\n\n- One\n- Two\n\n```ts\nconst x = 1\n```",
syntaxStyle,
})
renderer.root.add(markdown)Fenced language normalization#
Code fence info strings are normalized before Tree-sitter highlighting.
tsx->typescriptreact.jsx->javascriptreactTSX title=Button.tsx->typescriptreactDockerfile->dockerfile
Normalization uses infoStringToFiletype(). You can extend or override mappings at runtime:
import { extensionToFiletype, basenameToFiletype } from "@opentui/core"
extensionToFiletype.set("templ", "html")
basenameToFiletype.set("mytoolrc", "yaml")Concealment#
Hide markdown markers (backticks, emphasis markers, etc.) when conceal is true:
const markdown = new MarkdownRenderable(renderer, {
content: "**bold** and `code`",
syntaxStyle,
conceal: true,
})Use concealCode to control concealment inside fenced code blocks independently (false by default).
Streaming updates#
Enable streaming mode for incremental updates. Keep it true while appending chunks, then set markdown.streaming = false when complete to finalize trailing block parsing. Tables include trailing partial rows, with missing cells rendered empty.
const markdown = new MarkdownRenderable(renderer, {
content: "",
syntaxStyle,
streaming: true,
})
markdown.content += "# Live log\n"
markdown.content += "- line 1\n"
markdown.streaming = falseStable block prefix#
parseMarkdownIncremental reports how many parsed tokens at the head of the stream are unlikely to change after an append. The renderable exposes this value as a count of top-level render blocks. You can commit those blocks while the unstable tail changes.
To use it, render with internalBlockMode: "top-level". The renderable keeps each top-level markdown block as a separate child renderable. These blocks include headings, paragraphs, lists, tables, and fenced code. markdown._stableBlockCount gives the current stable prefix length. This matches the shape that ScrollbackSurface expects for row-by-row commits.
import { MarkdownRenderable, RGBA, SyntaxStyle } from "@opentui/core"
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex("#E6EDF3") },
})
const md = new MarkdownRenderable(renderer, {
content: "",
syntaxStyle,
streaming: true,
internalBlockMode: "top-level",
})
md.content = "# Title\n\nPara 1"
// md._stableBlockCount might be 1 here. "# Title" is settled, but "Para 1" could still grow.
md.content = "# Title\n\nPara 1\n\nPara 2"
// md._stableBlockCount rises as earlier blocks are sealed by a blank lineinternalBlockMode is an internal, experimental flag that powers the built-in scrollback streaming demo. This status does not apply to Markdown or its other public options. For normal rendering, keep the default value of "coalesced". This value folds sibling blocks together and preserves the existing layout.
Markdown tables#
Markdown tables support tableOptions for style, sizing, wrapping, borders, and selection.
const markdown = new MarkdownRenderable(renderer, {
content: "| Service | Status |\n| --- | --- |\n| api | ok |",
syntaxStyle,
tableOptions: {
style: "grid",
widthMode: "full",
columnFitter: "balanced",
wrapMode: "word",
cellPadding: 1,
cellPaddingX: 2,
cellPaddingY: 0,
borders: true,
outerBorder: true,
borderStyle: "rounded",
borderColor: "#6b7280",
selectable: true,
},
})tableOptions#
| Option | Type | Default | Description |
|---|---|---|---|
style |
"grid" | "columns" |
depends on block mode | Visual preset (see Table styles) |
widthMode |
"content" | "full" |
depends on style | "full" expands columns to fill available width |
columnFitter |
"proportional" | "balanced" |
"proportional" |
How columns shrink when space is constrained |
wrapMode |
"none" | "char" | "word" |
"word" |
Wrapping mode inside each table cell |
cellPadding |
number |
0 |
Padding on all sides of each cell |
cellPaddingX |
number |
cellPadding |
Horizontal padding on each side |
cellPaddingY |
number |
cellPadding |
Vertical padding on each side |
borders |
boolean |
depends on style | Enable inner and outer borders |
outerBorder |
boolean |
borders |
Override outer border visibility |
borderStyle |
BorderStyle |
"single" |
Table border character set |
borderColor |
ColorInput |
conceal fg or #888888 |
Border color for markdown tables |
selectable |
boolean |
true |
Enable table cell text selection |
Table styles#
tableOptions.style picks a preset that tunes the defaults for borders, outerBorder, and widthMode together:
"grid": boxed table with visible borders. Defaults toborders: true,widthMode: "full". This is the normal markdown-in-a-box rendering."columns": borderless columns with a 2-column gap, defaults towidthMode: "content". Useful for append-only output where a full-width grid feels heavy.
A separate Name/Status sample compares both presets:
Grid:
Columns:
If you do not pass style, it defaults to "columns" when internalBlockMode is "top-level", and "grid" otherwise. You can still override individual fields (for example, borders: true) to pull toward a different look.
Custom node rendering#
Override rendering for a token and fall back to default rendering:
const markdown = new MarkdownRenderable(renderer, {
content: "# Title\n\nHello",
syntaxStyle,
renderNode: (token, context) => {
if (token.type === "heading") {
return context.defaultRender()
}
return undefined
},
})Custom fenced-code languages#
Use createMarkdownCodeBlockRenderer when you only want to replace specific fenced-code languages. The language key is matched against the normalized fence info string, so a tsx fence maps to typescriptreact, and custom DSL names like taskflow can be matched directly.
import {
BoxRenderable,
MarkdownRenderable,
SyntaxStyle,
TextRenderable,
createMarkdownCodeBlockRenderer,
type CliRenderer,
type MarkdownCodeBlockRenderer,
} from "@opentui/core"
const syntaxStyle = SyntaxStyle.fromStyles({ default: {} })
const renderTaskFlow =
(renderer: CliRenderer): MarkdownCodeBlockRenderer =>
(token) => {
const steps = token.text
.split("\n")
.filter((line) => line.startsWith("step "))
.map((line) => line.slice("step ".length))
const card = new BoxRenderable(renderer, {
border: true,
borderStyle: "rounded",
borderColor: "#38BDF8",
paddingX: 1,
flexDirection: "column",
width: "100%",
})
for (const step of steps) {
card.add(new TextRenderable(renderer, { content: `- ${step}`, width: "100%" }))
}
return card
}
const markdown = new MarkdownRenderable(renderer, {
content: "```taskflow\nstep Parse markdown done\nstep Render widget active\n```",
syntaxStyle,
renderNode: createMarkdownCodeBlockRenderer({
taskflow: renderTaskFlow(renderer),
}),
})Properties#
| Property | Type | Default | Description |
|---|---|---|---|
content |
string |
"" |
Markdown source |
syntaxStyle |
SyntaxStyle |
required | Style definitions for tokens |
fg |
ColorInput |
- | Base foreground color (flows into inner code blocks) |
bg |
ColorInput |
- | Base background color (flows into inner code blocks) |
conceal |
boolean |
true |
Hide markdown markers in markdown text |
concealCode |
boolean |
false |
Hide markers inside fenced code blocks |
streaming |
boolean |
false |
Incremental mode. Set false to finalize |
tableOptions |
MarkdownTableOptions |
- | Options for markdown table rendering |
internalBlockMode |
"coalesced" | "top-level" |
"coalesced" |
Experimental: expose top-level blocks as separate renderables |
treeSitterClient |
TreeSitterClient |
- | Custom Tree-sitter client for code blocks |
renderNode |
(token: Token, context: RenderNodeContext) => Renderable | null | undefined |
- | Custom render hook per markdown block |
OpenTUI bundles a limited parser set. See the Tree-sitter reference before you highlight other fenced-code languages.
Related components#
Use Code for source-only content. Add a Line number gutter to a compatible code or editor renderable. Use Diff for patches. Use TextTable for table data that does not start as Markdown.