opentui

Build terminal UIs with OpenTUI. Covers Core, frameworks, components, application APIs, testing, extensions, integrations, deployment, and public API lookup.

Install
npx skills add 'https://github.com/anomalyco/opentui/tree/main/packages/web/src/content'
Download bundle ↓
main · ac753b4Scanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

docs/components/markdown.mdx

docs/components/markdown.mdxBrowse 89 files
View on GitHub
← Back to SKILL.md

title: Markdown description: Render markdown with syntax-aware styling

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

FieldAvailability
Package@opentui/core
Core renderableMarkdownRenderable
React<markdown> (automatic)
Solid<markdown> (automatic)
StatusBuilt 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

OptionTypeDefaultDescription
style"grid" | "columns"depends on block modeVisual 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
cellPaddingnumber0Padding on all sides of each cell
cellPaddingXnumbercellPaddingHorizontal padding on each side
cellPaddingYnumbercellPaddingVertical padding on each side
bordersbooleandepends on styleEnable inner and outer borders
outerBorderbooleanbordersOverride outer border visibility
borderStyleBorderStyle"single"Table border character set
borderColorColorInputconceal fg or #888888Border color for markdown tables
selectablebooleantrueEnable 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:

┌────────┬────────┐
│ Name   │ Status │
├────────┼────────┤
│ api    │ ready  │
├────────┼────────┤
│ worker │ paused │
└────────┴────────┘

Columns:

Name    Status
api     ready
worker  paused

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

PropertyTypeDefaultDescription
contentstring""Markdown source
syntaxStyleSyntaxStylerequiredStyle definitions for tokens
fgColorInput-Base foreground color (flows into inner code blocks)
bgColorInput-Base background color (flows into inner code blocks)
concealbooleantrueHide markdown markers in markdown text
concealCodebooleanfalseHide markers inside fenced code blocks
streamingbooleanfalseIncremental mode. Set false to finalize
tableOptionsMarkdownTableOptions-Options for markdown table rendering
internalBlockMode"coalesced" | "top-level""coalesced"Experimental: expose top-level blocks as separate renderables
treeSitterClientTreeSitterClient-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.