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
<r-markdown copy highlight content="# Hello ..."></r-markdown>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:
const el = document.createElement('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');
container.append(el);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.
<r-markdown caret content="Half-typed *emphasis*, `inline code`, and **bold that is still arriving"></r-markdown>- caret:
caretshows 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-incompletemeanwhile.
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).
<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/downloadare forwarded).$$…$$,\[…\]and```math→ block<r-math>;\(…\)→ inline. Single-dollar$…$is **opt-in** viainline-mathbecause it is ambiguous with currency.
API Reference
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
content |
string |
— | Markdown 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. |
caret |
boolean / 'circle' |
off | Blinking caret after the last block (▋, or ● with circle). |
copy |
boolean | off | Copy button on code blocks (forwarded to embedded <r-mermaid>). |
download |
boolean | off | Download button on code blocks (code.<ext> by language). |
line-numbers |
boolean | off | Line numbers in code blocks. |
highlight |
boolean / "light dark" theme names |
off | Syntax highlighting via shiki. Bare → github-light github-dark; one name → both; two names → light / dark. |
inline-math |
boolean | off | Treat $…$ as inline math (\(…\) always is). |
link-target |
string |
'_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). |
sheet |
string |
— | Extra CSS injected into the shadow root. |
label-* |
string |
English | Override 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).
| Event | detail |
Fired 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
| Part | Description |
|---|---|
markdown |
The outer wrapper. |
body |
The block container. |
block |
Each rendered block. |
code |
A code-block container. |
code-header |
The language / actions bar of a code block. |
code-lang |
The language label. |
code-actions |
The action-button group. |
button |
Each copy / download button. |
table |
The horizontally scrolling table wrapper. |
error |
The error box (on render failure). |
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]: urlin 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, sonpm i ranuialready brings it; nothing extra to add. - Standalone IIFE:
dist/iife/markdown.iife.jshas 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.