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 -> javascriptreact
  • TSX title=Button.tsx -> typescriptreact
  • Dockerfile -> 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 = 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, 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 line

internalBlockMode 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 to borders: true, widthMode: "full". This is the normal markdown-in-a-box rendering.
  • "columns": borderless columns with a 2-column gap, defaults to widthMode: "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.

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.