These docs describe the main branch, including unreleased changes. Read the 0.6.0 docs.

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") },
  },
  renderer.nativeScene,
)

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#

Markdown normalizes each code fence info string to a file type before Tree-sitter highlighting:

  • tsx -> typescriptreact
  • .jsx -> javascriptreact
  • TSX title=Button.tsx -> typescriptreact
  • Dockerfile -> dockerfile

Markdown uses infoStringToFiletype() for this step. To add or change mappings at runtime, edit the maps that it reads:

import { extensionToFiletype, basenameToFiletype } from "@opentui/core"

extensionToFiletype.set("templ", "html")
basenameToFiletype.set("mytoolrc", "yaml")

Concealment#

Set conceal: true to hide Markdown markers, such as backticks and emphasis markers:

const markdown = new MarkdownRenderable(renderer, {
  content: "**bold** and `code`",
  syntaxStyle,
  conceal: true,
})

concealCode controls concealment inside fenced code blocks separately. Its default is false.

Streaming updates#

Use streaming mode when content arrives in chunks. Keep streaming: true while you append chunks. When the content is complete, set markdown.streaming = false to finish parsing the trailing block. Tables show trailing partial rows, with missing cells shown empty.

const markdown = new MarkdownRenderable(renderer, {
  content: "",
  syntaxStyle,
  streaming: true,
})

markdown.content += "# Live log\n"
markdown.content += "- line 1\n"
markdown.streaming = false

Stable 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, set internalBlockMode: "top-level". The renderable then 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. In the default mode, it is always 0. 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") },
  },
  renderer.nativeScene,
)

const md = new MarkdownRenderable(renderer, {
  content: "",
  syntaxStyle,
  streaming: true,
  internalBlockMode: "top-level",
})

md.content = "# Title\n\nPara 1"
// md._stableBlockCount is 0. While streaming, the newest parsed tokens stay unstable.

md.content = "# Title\n\nPara 1\n\nPara 2"
// Still 0.

md.content = "# Title\n\nPara 1\n\nPara 2\n\nPara 3"
// md._stableBlockCount is 2: "# Title" and "Para 1" are stable.

internalBlockMode is an internal, experimental flag for the built-in scrollback streaming demo. This status does not apply to Markdown or to its other options. For normal rendering, keep the default value, "coalesced". This value combines sibling blocks into larger render blocks and keeps the existing layout.

Markdown tables#

Use tableOptions to set the style, sizing, wrapping, borders, and selection of Markdown tables.

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. The preset sets the defaults for borders, outerBorder, and widthMode:

  • "grid": a boxed table with visible borders. Defaults to borders: true and widthMode: "full". This is the normal table rendering.
  • "columns": borderless columns with a 2-column gap. Defaults to borders: false and widthMode: "content". Use it for append-only output where a full-width grid is too heavy.

This Name/Status sample shows both presets:

Grid:

Columns:

If you do not pass style, it is "columns" when internalBlockMode is "top-level", and "grid" otherwise. Individual fields, such as borders: true, override the preset defaults.

Custom node rendering#

Use renderNode to replace the renderable for a block. Return undefined or null to use the default renderable. Call context.defaultRender() to create the default renderable yourself:

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 to replace only specific fenced-code languages. It matches each key against the normalized info string. A tsx fence matches the key typescriptreact. A custom name such as taskflow matches itself.

import {
  BoxRenderable,
  MarkdownRenderable,
  SyntaxStyle,
  TextRenderable,
  createMarkdownCodeBlockRenderer,
  type CliRenderer,
  type MarkdownCodeBlockRenderer,
} from "@opentui/core"

const syntaxStyle = SyntaxStyle.fromStyles({ default: {} }, renderer.nativeScene)

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, also used by inner code blocks
bg ColorInput - Base background color, also used by 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 renderer for each Markdown block

Markdown rebuilds its blocks at once when you set content. It applies syntaxStyle, fg, bg, conceal, and concealCode changes later, in its lifecycle pass (onLifecyclePass). Core runs lifecycle passes after the frame callbacks and before layout, so the new styles appear in that frame. These five setters do not request a frame. If nothing else requests one, call markdown.requestRender().

If you assign onLifecyclePass on a Markdown renderable, even to null, Markdown no longer applies these changes by itself. Call refreshStyles() after each change. refreshStyles() rebuilds the blocks at once and requests a frame.

OpenTUI bundles a limited parser set. See the Tree-sitter reference before you highlight other fenced-code languages.

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.

Changes0.6.0, 0.5.12, 0.5.11
0.6.0
Added MarkdownRenderable.nativeIntegration.
Removed MarkdownRenderable.renderSelf.
0.5.12
core: MarkdownRenderable passes its fg to tables. Table text no longer always uses the #FFFFFF default, and a later change to fg recolors existing tables. (#1503)
0.5.11
core: Markdown named links and bare links are clickable in terminals that support OSC 8, and the link destination is not shown. Other terminals show the visible URL. Links stay intact across formatting, streaming, wrapping, and late terminal capability detection. (#1442)
Added MarkdownRenderable.destroySelf.