Skip to main content
urbicon ui
source

StreamingMarkdownexperimental

A markdown renderer for streaming LLM output. It parses a growing string incrementally, caches settled blocks, and applies a strict URL policy by default. Because it renders to real components instead of an HTML string, untrusted output cannot inject markup.

Playground

Rate limiting with a token bucket

A token bucket lets short bursts through while capping the long-run rate. The bucket refills at a steady pace, and each request spends one token.

  • Refill tokens on a timer, up to a maximum

  • Allow a request when a token is free, otherwise reject it

ts
if (tokens > 0) { tokens--; allow(); } else reject();
Size
<script lang="ts">
  import { StreamingMarkdown } from '@urbicon-ui/blocks';

  let content = $state(`## Rate limiting with a token bucket

A **token bucket** lets short bursts through while capping the long-run rate.
The bucket refills at a steady pace, and each request spends one token.

- Refill tokens on a timer, up to a maximum
- Allow a request when a token is free, otherwise reject it

\`\`\`ts
if (tokens > 0) { tokens--; allow(); } else reject();
\`\`\`
`);
</script>

<StreamingMarkdown
  {content}
  headingLevelStart={2}
  streaming
/>

01 Examples

Static markdown — tables and task lists

A settled answer renders GFM tables and task lists the same whether the text arrived all at once or streamed chunk by chunk. headingLevelStart continues the page outline: at 3, the answer's ## renders as an h4 under this example's h3 title.

Release checklist

StepOwnerStatus
FreezePlatformDone
Sign-offQAPending

Remaining work:

  • Bump the version

  • Regenerate the docs

  • Publish the tag

<script lang="ts">
  import { StreamingMarkdown } from '@urbicon-ui/blocks';

  // A settled answer (streaming={false}): GFM tables and task lists render the
  // same whether the text arrived all at once or chunk by chunk.
  const content = `## Release checklist

| Step     | Owner    | Status  |
| -------- | -------- | ------- |
| Freeze   | Platform | Done    |
| Sign-off | QA       | Pending |

Remaining work:

- [x] Bump the version
- [x] Regenerate the docs
- [ ] Publish the tag
`;
</script>

<StreamingMarkdown {content} headingLevelStart={3} />

Citations from sources

Ids in the sources prop activate the matching [n] markers in the text; each becomes a CitationChip whose popover shows the title, snippet, and a policy-checked link. Markers without a matching id stay plain text.

Transformers replaced recurrence with self-attention , and later work mapped how quality scales with model and dataset size . A bare marker like [3] with no matching source stays plain text.

<script lang="ts">
  import { StreamingMarkdown, type CitationSource } from '@urbicon-ui/blocks';

  // Ids activate the matching `[id]` markers in the text; the markers render as
  // CitationChip (1-based, in array order). `[3]` below has no source, so it
  // stays plain text — prose like "step [3]" is never mangled.
  const sources: CitationSource[] = [
    {
      id: '1',
      title: 'Attention Is All You Need',
      url: 'https://arxiv.org/abs/1706.03762',
      snippet: 'The Transformer, based solely on attention mechanisms.'
    },
    {
      id: '2',
      title: 'Scaling Laws for Neural Language Models',
      url: 'https://arxiv.org/abs/2001.08361',
      snippet: 'Quality improves smoothly with model size, dataset size, and compute.'
    }
  ];

  const content = `Transformers replaced recurrence with self-attention [1], and later
work mapped how quality scales with model and dataset size [2]. A bare marker
like [3] with no matching source stays plain text.`;
</script>

<StreamingMarkdown {content} {sources} headingLevelStart={3} />

Untrusted input stays inert

The renderer never emits an HTML string, so untrusted model output cannot inject markup. With the default URL policy, scheme-smuggled links become inert text, external images become an alt-text chip, and raw HTML stays literal text.

A normal link renders as a real link.

A scheme-smuggled link becomes inert text with a dotted underline — the URL never reaches the DOM.

External images are blocked and shown as an alt-text chip: remote tracker

Raw HTML stays literal text, never parsed: <img src=x onerror="alert(1)">

<script lang="ts">
  import { StreamingMarkdown } from '@urbicon-ui/blocks';

  // Untrusted model output. The strict-by-default URL policy neutralizes every
  // hostile shape without any extra configuration.
  const content = `A [normal link](https://ui.urbicon.de) renders as a real link.

A [scheme-smuggled link](javascript:alert(1)) becomes inert text with a dotted
underline — the URL never reaches the DOM.

External images are blocked and shown as an alt-text chip:
![remote tracker](https://evil.example/track.gif)

Raw HTML stays literal text, never parsed: <img src=x onerror="alert(1)">`;
</script>

<StreamingMarkdown {content} headingLevelStart={3} />

Custom node renderer

A renderers snippet replaces the built-in renderer for one node type. Use it for syntax highlighting, lightboxes, or router-aware links; here, a custom code-block presentation.

Fenced code flows through your own renderer:

ts custom renderer
export const answer = 42;
<script lang="ts">
  import { StreamingMarkdown } from '@urbicon-ui/blocks';

  const content = `Fenced code flows through your own renderer:

\`\`\`ts
export const answer = 42;
\`\`\`
`;
</script>

<!--
  A `renderers` snippet fully replaces the built-in renderer for one node type —
  the hook point for syntax highlighting, lightboxes, or router-aware links,
  without pulling any of those into the core. Here: a custom code presentation.
-->
{#snippet codeBlock({ code, lang }: { code: string; lang?: string; open?: boolean })}
  <div class="border-primary/40 rounded-contain mt-4 overflow-hidden border first:mt-0">
    <div class="bg-primary-subtle flex items-center justify-between px-3 py-1.5">
      <span class="text-primary-text font-mono text-xs">{lang ?? 'code'}</span>
      <span class="text-text-tertiary text-xs">custom renderer</span>
    </div>
    <pre class="text-text-primary overflow-x-auto px-3 py-2.5 font-mono text-sm">{code}</pre>
  </div>
{/snippet}

<StreamingMarkdown {content} renderers={{ codeBlock }} headingLevelStart={3} />

02 Customization

Every element maps to a named slot (paragraph, heading1–heading6, inlineCode, codeBlock, table, …). Restyle any of them via slotClasses, or register a reusable look as a preset on BlocksProvider. Use renderers only when you need to replace a whole node type (highlighting, custom links); use slotClasses for pure styling.

The urlPolicy is strict by default. Widen it narrowly: allow a specific image CDN via allowedImagePrefixes rather than a broad prefix. Keep the policy object referentially stable; a new reference re-parses the whole content.

03 Accessibility

Heading hierarchy

Markdown # maps to the DOM level set by headingLevelStart (deeper levels shift along, clamped at h6). Set it to the rank that continues the outline the message sits in — 3 under a chat panel's <h2>. No level keeps a message's headings out of the page outline; one set too deep leaves a gap in it instead. Visual sizing keeps following the author's level independently.

Streaming cursor

The pulsing cursor shown while streaming is true is decorative and carries aria-hidden="true", so screen readers announce only the text. Its pulse is gated on motion-safe:, so it holds still under prefers-reduced-motion.

Scrollable tables

Wide tables scroll inside a focusable region (tabindex="0") labelled by tableRegionLabel, so keyboard users can reach and scroll the overflow (WCAG 2.1.1). Horizontal scroll stays inside the block — never the page.

Blocked links and images

A policy-blocked link renders as inert text with a dotted underline as the "this was a link" cue; a blocked image becomes an alt-text chip. The blocked state is visible, so the reader can tell something was withheld.

04 API Reference

16 props
16 props 1 required
Prop
Type
Default
Description

05 Types

7 types
Name
Kind
Category
Used by
Description

06 Installation

Import

import { StreamingMarkdown } from '@urbicon-ui/blocks';
import type {
  StreamingMarkdownProps,
  MarkdownRenderers,
  MarkdownUrlPolicy,
  CitationSource
} from '@urbicon-ui/blocks';