Yugen UI

StreamingMarkdown

StreamingMarkdown renders an AI's Markdown answer as tokens arrive, without flashes of raw syntax, HTML injection or a full re-render on every word.

A built-in parser (parseMarkdown, also exported for testing) builds React elements directly: paragraphs, headings (shifted by headingOffset, never an h1), bold, italic, strikethrough and inline code, links (http, https and mailto only; http and https links open in a new tab with rel noreferrer noopener), nested lists and task lists, blockquotes, GFM tables with alignment, horizontal rules, and code blocks rendered with CodeBlock. While streaming, an unclosed fence shows as a code block in progress, and an unfinished **, link or [1] doesn't show raw syntax. A caret is shown (static with reduced motion) and aria-busy is set. Blocks that have settled keep the same object, so React.memo skips re-rendering them.

Import

Install from npm, copy the source into your project with @yugen-ui/cli or the shadcn CLI, or import directly when working in this repo.

TSX
// npm install yugen-uiimport { StreamingMarkdown, parseMarkdown } from "yugen-ui";

Variants

Each tile is a standalone example: hover to copy, or press Code to see the source.

To do
  • Done:
    Add color tokens
  • Done:
    Check contrast
  • Not done:
    Write the docs

Unsafe links like click here render as plain text.

Task list & unsafe links

A - [x] item gets an icon plus the hidden text “Done:”. A javascript: link is removed and only its label remains.

Note: the colorBrandBackground token follows data-brand.

  • Light: Brand80
  • Dark: Brand70

Small size

size="small" uses caption1, for notes, reasoning blocks and cards in a panel.

Usage

Do
  • Pass the whole growing string and keep streaming on until the last token arrives. The component handles unfinished syntax.
  • Set headingOffset to match the heading level of the surrounding page (default 2: # → h3).
  • Inside a streaming MessageBubble, set caret={false} so there aren't two carets.
Don't
  • Don't build HTML strings yourself or use dangerouslySetInnerHTML for model answers.
  • Don't turn on announceWhenDone inside a MessageScroller. The answer would be read twice.
  • Don't expect full CommonMark: raw HTML, images, reference links and indented code aren't supported.

Props

Prop · TypeDefaultDescription
childrenstringDefault: —Markdown content.
streamingbooleanDefault: falseTolerates unfinished syntax, shows the caret, sets aria-busy.
headingOffsetnumberDefault: 2Added to the heading level; never produces an h1.
citationsCitationSource[]Default: —Sources for [1] / [^1], rendered as Citation chips. The chip's preview card loads on demand, right after the first chip appears.
breaksbooleanDefault: trueTurns single line breaks into <br>.
caretbooleanDefault: trueCaret while streaming.
size"small" | "medium"Default: "medium"caption1 or body1.
announceWhenDonebooleanDefault: falseReads the plain text through a live region when the stream ends.
parseMarkdown(text, { streaming })MdRootDefault: —The parser on its own; sanitizeHref and markdownToPlainText are exported too.

Messages

Text built into the component: labels, aria-labels, screen reader announcements.

English by default. Change it with the messages prop of YugenProvider, by the namespace below; messagesVi has the Vietnamese set. See Interface language.

messages.streamingMarkdown

Key · TypeDefault (English)messagesVi
newTabstring

Chữ ẩn sau link mở tab mới.

Default (English)"(opens in new tab)"messagesVi"(mở tab mới)"
taskDonestring

Chữ ẩn trước mục danh sách việc đã xong / chưa xong.

Default (English)"Done:"messagesVi"Đã xong:"
taskTodostringDefault (English)"Not done:"messagesVi"Chưa xong:"
tablestring

Tên vùng cuộn của bảng.

Default (English)"Table"messagesVi"Bảng"