Skip to content

Markdown

Render Markdown — including token-by-token AI output — as a framework-agnostic web component. <r-markdown> is modelled after Vercel's Streamdown: while text streams in it closes half-typed **bold, `code, links and $$ math on the fly, splits the document into blocks and re-renders only the block that changed, so a long answer never re-parses from the top on every token.

Fenced ```mermaid blocks become <r-mermaid>, math becomes <r-math>, and code can be highlighted with shiki — every one of these is lazy-loaded the first time the content needs it. Output is sanitized with DOMPurify.

Use when you display Markdown you don't fully control — chat replies, LLM streams, user comments, docs — and want streaming, code/diagram/math support and safe HTML without wiring a parser, sanitizer and highlighter yourself.

Quick Start

html
<r-markdown copy highlight content="# Hello ..."></r-markdown>
js
import 'ranui'; // or the standalone entry:
import 'ranui/markdown';

The source is read from the content property (preferred — not reflected, so streaming a long answer doesn't churn the DOM), the content attribute, or the element's text content:

js
const el = document.querySelector('r-markdown');
el.setAttribute('caret', ''); // show a blinking caret while streaming
for await (const chunk of stream) {
  el.content += chunk; // only the last block re-renders
}
el.removeAttribute('caret');

Streaming

mode="streaming" (the default) runs the text through remend first — the incomplete-markdown terminator extracted from Streamdown — so a half-received **bold renders as bold instead of literal asterisks, [text](https://exa shows as plain text until the URL closes, - doesn't turn the previous paragraph into a heading, and so on. Set mode="static" for finished documents to skip that pass and render in one piece.

html
<r-markdown caret content="Half-typed *emphasis*, `inline code`, and **bold that is still arriving"></r-markdown>
  • caretcaret shows a blinking , caret="circle" a , after the last block. It hides automatically while a code fence is still open or the last block is a table.
  • incomplete code fences stay plain (no highlighting flash, no half-rendered diagram) until the closing fence arrives; the container carries data-incomplete meanwhile.

Code blocks

Every code block gets a header with the language and, opt-in, a copy / download button. Add highlight to syntax-highlight with shiki (lazy-loaded; languages load on demand; github-light / github-dark by default, following the page theme).

html
<r-markdown copy download line-numbers highlight></r-markdown>
<!-- pick themes: light dark -->
<r-markdown highlight="vitesse-light vitesse-dark"></r-markdown>

Mermaid & math

  • ```mermaid<r-mermaid> (with fullscreen; copy / download are forwarded).
  • $$…$$, \[…\] and ```math → block <r-math>; \(…\) → inline. Single-dollar $…$ is opt-in via inline-math because it is ambiguous with currency.

API Reference

Attributes

AttributeTypeDefaultDescription
contentstringMarkdown source. The content property takes precedence and is not reflected; falls back to the element's text content.
mode'streaming' | 'static''streaming'streaming closes incomplete markdown and diffs by block; static renders the whole text as-is in one pass.
caretboolean / 'circle'offBlinking caret after the last block (, or with circle).
copybooleanoffCopy button on code blocks (forwarded to embedded <r-mermaid>).
downloadbooleanoffDownload button on code blocks (code.<ext> by language).
line-numbersbooleanoffLine numbers in code blocks.
highlightboolean / "light dark" theme namesoffSyntax highlighting via shiki. Bare → github-light github-dark; one name → both; two names → light / dark.
inline-mathbooleanoffTreat $…$ as inline math (\(…\) always is).
link-targetstring'_blank'target for external links (rel="noopener noreferrer" added). _self leaves links untouched. In-page #anchors never get it.
theme'auto' | 'light' | 'dark''auto'Highlight / diagram theme. auto follows the page (.dark, [data-ran-theme], else prefers-color-scheme).
sheetstringExtra CSS injected into the shadow root.
label-*stringEnglishOverride control labels: label-copy, label-download.

Property aliases: content, mode, caret, copyable, downloadable, lineNumbers, highlight, inlineMath, linkTarget, theme, sheet.

Events

All events bubble and cross the shadow boundary (composed).

EventdetailFired when
render{ blocks: number, changed: number }a render pass changed at least one block
copied{ kind: 'code', language, code }a code block was copied
download{ kind: 'code', language, filename }a code block was downloaded
error{ message: string }parsing/rendering failed (also shown in-place)

CSS Parts

PartDescription
markdownThe outer wrapper.
bodyThe block container.
blockEach rendered block.
codeA code-block container.
code-headerThe language / actions bar of a code block.
code-langThe language label.
code-actionsThe action-button group.
buttonEach copy / download button.
tableThe horizontally scrolling table wrapper.
errorThe error box (on render failure).
css
r-markdown::part(code) {
  border-radius: 8px;
}

CSS Variables

Override on the element (each falls back to a semantic token, then a literal): --ran-markdown-color, --ran-markdown-font-size, --ran-markdown-line-height, --ran-markdown-gap, --ran-markdown-heading-color, --ran-markdown-link-color, --ran-markdown-inline-code-bg, --ran-markdown-code-bg, --ran-markdown-code-border, --ran-markdown-code-radius, --ran-markdown-code-font-size, --ran-markdown-mono-font, --ran-markdown-blockquote-border, --ran-markdown-table-border, --ran-markdown-table-header-bg, --ran-markdown-caret, --ran-markdown-caret-color, --ran-markdown-button-color, --ran-markdown-error-color.

Notes

  • Lazy-loaded: the parser chunk (marked + DOMPurify + remend) loads on first render; shiki, mermaid and Temml each load only when the content uses them. Apps that never render markdown pay nothing.
  • Sanitized: raw HTML in the markdown goes through DOMPurify — scripts, event handlers, javascript: URLs, <style>, forms and iframes are removed. Task-list checkboxes survive.
  • Block diffing keys blocks by position, so DOM state inside untouched blocks (an open fullscreen diagram, a scrolled table) survives streaming updates. The document is lexed once and each block renders from its own tokens, so a link reference definition resolves across blocks ([text][id] in one block, [id]: url in another).
  • GFM footnotes ([^1]) are not supported — marked has no footnote tokenizer, so the markers render as literal text.
  • shiki resolves from your own install. The ES build leaves import('shiki') bare, so your bundler code-splits it and downloads only the grammars your code fences use. shiki is a regular dependency of ranui, so npm i ranui already brings it — nothing extra to add.
  • Standalone IIFE: dist/iife/markdown.iife.js has no resolver, so it inlines mermaid, Temml and shiki's web language bundle (~50 common languages) instead. Prefer the ES entry (ranui/markdown) for full language coverage and a smaller download.

Released under the MIT License.