npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@humanspeak/svelte-markdown

v1.5.2

Published

Markdown and HTML renderer for Svelte 5 — built for rendering streaming AI agent output from Claude Code, ChatGPT, and agentic workflows. XSS-safe defaults, streaming-aware sanitization, token caching, TypeScript types, and Svelte 5 runes.

Downloads

80,599

Readme

@humanspeak/svelte-markdown

A powerful, customizable markdown renderer for Svelte with TypeScript support. Built as a successor to the original svelte-markdown package by Pablo Berganza, now maintained and enhanced by Humanspeak, Inc.

NPM version Build Status Coverage Status License Downloads CodeQL Install size Code Style: Trunk TypeScript Types Maintenance

Features

  • 🔒 Secure HTML parsing via HTMLParser2 with built-in XSS defaults (protocol allowlist, on* handler stripping)
  • 🚀 Full markdown syntax support through Marked
  • 💪 Complete TypeScript support with strict typing
  • 🔄 Svelte 5 runes compatibility
  • ✂️ Inline snippet overrides — customize renderers without separate files
  • 🎨 Customizable component rendering system
  • ♿ WCAG 2.1 accessibility compliance
  • 🎯 GitHub-style slug generation for headers
  • 🧪 Comprehensive test coverage (vitest and playwright)
  • 🧩 First-class marked extensions support via extensions prop (e.g., KaTeX math, alerts)
  • ⚡ Intelligent token caching (50-200x faster re-renders)
  • 📡 LLM streaming mode with incremental rendering (~1.6ms avg per update)
  • 🖼️ Smart image lazy loading with fade-in animation

Installation

npm i -S @humanspeak/svelte-markdown

Or with your preferred package manager:

pnpm add @humanspeak/svelte-markdown
yarn add @humanspeak/svelte-markdown

Basic Usage

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'

    const source = `
# This is a header

This is a paragraph with **bold** and <em>mixed HTML</em>.

* List item with \`inline code\`
* And a [link](https://svelte.dev)
  * With nested items
  * Supporting full markdown
`
</script>

<SvelteMarkdown {source} />

Rendering AI Agent Output

Modern AI coding agents — Claude Code, Codex, agentic workflows — increasingly emit HTML alongside markdown for richer output (design mockups, dashboards, reports, interactive artifacts). @humanspeak/svelte-markdown is built for this:

  • Mixed markdown + HTML in a single source — agents can interleave standard markdown with rich HTML (tables, SVG, custom elements) without a second renderer
  • XSS defaults on by defaultjavascript: URLs and on* handlers stripped from agent output before render, no opt-in required (see Security)
  • Streaming-aware sanitization — when streaming is enabled, each token is sanitized as it's emitted; mid-tag partials buffer until well-formed, so progressive HTML from an LLM renders without flicker
  • Custom HTML tag support — route semantic markup like <tool-call>, <thinking>, or your own design-system tags to your own components via renderers.html (see Custom HTML Tags)
<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { StreamingChunk } from '@humanspeak/svelte-markdown'

    let markdown: { writeChunk: (chunk: StreamingChunk) => void } | undefined

    async function streamFromAgent(response: Response) {
        const reader = response.body!.getReader()
        const decoder = new TextDecoder()
        while (true) {
            const { done, value } = await reader.read()
            if (done) break
            markdown?.writeChunk(decoder.decode(value, { stream: true }))
        }
    }
</script>

<SvelteMarkdown bind:this={markdown} source="" streaming />

For background on why HTML has become a common agent output format, see Thariq's post: Using Claude Code: The Unreasonable Effectiveness of HTML. For the full streaming API (offset chunks, reset, websocket patterns), see LLM Streaming below.

TypeScript Support

The package is written in TypeScript and includes full type definitions:

import type {
    Renderers,
    Token,
    TokensList,
    SvelteMarkdownOptions,
    MarkedExtension
} from '@humanspeak/svelte-markdown'

Exports for programmatic overrides

You can import renderer maps and helper keys to selectively override behavior.

import SvelteMarkdown, {
    // Maps
    defaultRenderers, // markdown renderer map
    Html, // HTML renderer map

    // Keys
    rendererKeys, // markdown renderer keys (excludes 'html')
    htmlRendererKeys, // HTML renderer tag names

    // Utility components
    Unsupported, // markdown-level unsupported fallback
    UnsupportedHTML // HTML-level unsupported fallback
} from '@humanspeak/svelte-markdown'

// Example: override a subset
const customRenderers = {
    ...defaultRenderers,
    link: CustomLink,
    html: {
        ...Html,
        span: CustomSpan
    }
}

// Optional: iterate keys when building overrides dynamically
for (const key of rendererKeys) {
    // if (key === 'paragraph') customRenderers.paragraph = MyParagraph
}
for (const tag of htmlRendererKeys) {
    // if (tag === 'div') customRenderers.html.div = MyDiv
}

Notes

  • rendererKeys intentionally excludes html. Use htmlRendererKeys for HTML tag overrides.
  • Unsupported and UnsupportedHTML are available if you want a pass-through fallback strategy.

Helper utilities for allow/deny strategies

These helpers make it easy to either allow only a subset or exclude only a subset of renderers without writing huge maps by hand.

  • HTML helpers
    • buildUnsupportedHTML(): returns a map where every HTML tag uses UnsupportedHTML.
    • allowHtmlOnly(allowed): enable only the provided tags; others use UnsupportedHTML.
      • Accepts tag names like 'strong' or tuples like ['div', MyDiv] to plug in custom components.
    • excludeHtmlOnly(excluded, overrides?): disable only the listed tags (mapped to UnsupportedHTML), with optional overrides for non-excluded tags using tuples.
  • Markdown helpers (non-HTML)
    • buildUnsupportedRenderers(): returns a map where all markdown renderers (except html) use Unsupported.
    • allowRenderersOnly(allowed): enable only the provided markdown renderer keys; others use Unsupported.
      • Accepts keys like 'paragraph' or tuples like ['paragraph', MyParagraph] to plug in custom components.
    • excludeRenderersOnly(excluded, overrides?): disable only the listed markdown renderer keys, with optional overrides for non-excluded keys using tuples.

HTML helpers in context

The HTML helpers return an HtmlRenderers map to be used inside the html key of the overall renderers map. They do not replace the entire renderers object by themselves.

Basic: keep markdown defaults, allow only a few HTML tags (others become UnsupportedHTML):

import SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'

const renderers = {
    ...defaultRenderers, // keep markdown defaults
    html: allowHtmlOnly(['strong', 'em', 'a']) // restrict HTML
}

Allow a custom component for one tag while allowing others with defaults:

import SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'

const renderers = {
    ...defaultRenderers,
    html: allowHtmlOnly([['div', MyDiv], 'a'])
}

Exclude just a few HTML tags; keep all other HTML tags as defaults:

import SvelteMarkdown, { defaultRenderers, excludeHtmlOnly } from '@humanspeak/svelte-markdown'

const renderers = {
    ...defaultRenderers,
    html: excludeHtmlOnly(['span', 'iframe'])
}

// Or exclude 'span', but override 'a' to CustomA
const renderersWithOverride = {
    ...defaultRenderers,
    html: excludeHtmlOnly(['span'], [['a', CustomA]])
}

Disable all HTML quickly (markdown defaults unchanged):

import SvelteMarkdown, { defaultRenderers, buildUnsupportedHTML } from '@humanspeak/svelte-markdown'

const renderers = {
    ...defaultRenderers,
    html: buildUnsupportedHTML()
}

Markdown-only (non-HTML) scenarios

Allow only paragraph and link with defaults, disable others:

import { allowRenderersOnly } from '@humanspeak/svelte-markdown'

const md = allowRenderersOnly(['paragraph', 'link'])

Exclude just link; keep others as defaults:

import { excludeRenderersOnly } from '@humanspeak/svelte-markdown'

const md = excludeRenderersOnly(['link'])

Disable all markdown renderers (except html) quickly:

import { buildUnsupportedRenderers } from '@humanspeak/svelte-markdown'

const md = buildUnsupportedRenderers()

Combine HTML and Markdown helpers

You can combine both maps in renderers for SvelteMarkdown.

<script lang="ts">
    import SvelteMarkdown, { allowRenderersOnly, allowHtmlOnly } from '@humanspeak/svelte-markdown'

    const renderers = {
        // Only allow a minimal markdown set
        ...allowRenderersOnly(['paragraph', 'link']),

        // Configure HTML separately (only strong/em/a)
        html: allowHtmlOnly(['strong', 'em', 'a'])
    }

    const source = `# Title\n\nThis has <strong>HTML</strong> and [a link](https://example.com).`
</script>

<SvelteMarkdown {source} {renderers} />

Custom Renderer Example

Here's a complete example of a custom renderer with TypeScript support:

<script lang="ts">
    import type { Snippet } from 'svelte'

    interface Props {
        children?: Snippet
        href?: string
        title?: string
    }

    const { href = '', title = '', children }: Props = $props()
</script>

<a {href} {title} class="custom-link">
    {@render children?.()}
</a>

If you would like to extend other renderers please take a look inside the renderers folder for the default implentation of them. If you would like feature additions please feel free to open an issue!

Snippet Overrides (Svelte 5)

For simple tweaks — adding a class, changing an attribute, wrapping in a div — you can override renderers inline with Svelte 5 snippets instead of creating separate component files:

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'

    const source = '# Hello\n\nA paragraph with [a link](https://example.com).'
</script>

<SvelteMarkdown {source}>
    {#snippet paragraph({ children })}
        <p class="prose">{@render children?.()}</p>
    {/snippet}

    {#snippet heading({ depth, children })}
        {#if depth === 1}
            <h1 class="title">{@render children?.()}</h1>
        {:else}
            <h2>{@render children?.()}</h2>
        {/if}
    {/snippet}

    {#snippet link({ href, title, children })}
        <a {href} {title} target="_blank" rel="noopener noreferrer">
            {@render children?.()}
        </a>
    {/snippet}

    {#snippet code({ lang, text })}
        <pre class="highlight {lang}"><code>{text}</code></pre>
    {/snippet}
</SvelteMarkdown>

How it works

  • Container renderers (paragraph, heading, blockquote, list, etc.) receive a children snippet for nested content
  • Leaf renderers (code, image, hr, br) receive only data props — no children
  • Precedence: snippet > component renderer > default. If both a snippet and a renderers.paragraph component are provided, the snippet wins

HTML tag snippets

HTML tag snippets use an html_ prefix to avoid collisions with markdown renderer names:

<SvelteMarkdown {source}>
    {#snippet html_div({ attributes, children })}
        <div class="custom-wrapper" {...attributes}>{@render children?.()}</div>
    {/snippet}

    {#snippet html_a({ attributes, children })}
        <a {...attributes} target="_blank" rel="noopener noreferrer">
            {@render children?.()}
        </a>
    {/snippet}
</SvelteMarkdown>

All HTML snippets share a uniform props interface: { attributes?: Record<string, any>, children?: Snippet }.

Custom HTML Tags

You can render arbitrary (non-standard) HTML tags like <click>, <tooltip>, or any custom element by providing a renderer or snippet for the tag name. The parsing pipeline accepts any tag name — you just need to tell SvelteMarkdown how to render it.

Component renderer approach:

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import ClickButton from './ClickButton.svelte'

    const source = '<click>Click Me</click>'
    const renderers = { html: { click: ClickButton } }
</script>

<SvelteMarkdown {source} {renderers} />

Snippet override approach:

<SvelteMarkdown source={'<click data-action="submit">Click Me</click>'}>
    {#snippet html_click({ attributes, children })}
        <button {...attributes} class="custom-btn">{@render children?.()}</button>
    {/snippet}
</SvelteMarkdown>

Both approaches work for any tag name. Snippet overrides take precedence over component renderers when both are provided.

Marked Extensions

Use marked extensions via the extensions prop. SvelteMarkdown ships first-class extensions for KaTeX, Mermaid, GitHub-style alerts, and footnotes from the @humanspeak/svelte-markdown/extensions subpath — no third-party packages required. Third-party extensions still work too; the component handles registering tokenizers internally and you just provide renderers for the custom token types.

KaTeX Math Rendering

The package includes built-in markedKatex and KatexRenderer helpers. Install katex as an optional peer dependency and load its CSS:

npm install katex

Default delimiter set (mirrors KaTeX's own auto-render defaults):

| Delimiter pair | Level | displayMode | | -------------------------------------------------------------- | ------ | ------------- | | \(...\) | inline | false | | \[...\] (own-line) | block | true | | $$...$$ (own-line) | block | true | | \begin{equation}...\end{equation} and other AMS environments | block | true |

Single-dollar inline ($x^2$) is off by default — KaTeX itself excludes it from auto-render to avoid currency-string clashes like $5,000. Pass { singleDollarInline: true } to enable it; it uses a whitespace-bounded rule so currency strings still won't match.

Component renderer approach:

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import { markedKatex, KatexRenderer } from '@humanspeak/svelte-markdown/extensions'

    interface KatexRenderers extends Renderers {
        inlineKatex: RendererComponent
        blockKatex: RendererComponent
    }

    const renderers: Partial<KatexRenderers> = {
        inlineKatex: KatexRenderer,
        blockKatex: KatexRenderer
    }
</script>

<svelte:head>
    <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
        crossorigin="anonymous"
    />
</svelte:head>

<SvelteMarkdown
    source={`Euler's identity: \\(e^{i\\pi} + 1 = 0\\)`}
    extensions={[markedKatex()]}
    {renderers}
/>

KatexRenderer hardcodes throwOnError: false so a single malformed expression renders as a tinted error span instead of throwing — if you need stricter behavior, supply your own component for the inlineKatex / blockKatex keys.

Snippet override approach (no separate component file needed):

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import { markedKatex } from '@humanspeak/svelte-markdown/extensions'
    import katex from 'katex'
</script>

<svelte:head>
    <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
        crossorigin="anonymous"
    />
</svelte:head>

<SvelteMarkdown source={`Euler's identity: \\(e^{i\\pi} + 1 = 0\\)`} extensions={[markedKatex()]}>
    {#snippet inlineKatex(props)}
        {@html katex.renderToString(props.text, { throwOnError: false, displayMode: false })}
    {/snippet}
    {#snippet blockKatex(props)}
        {@html katex.renderToString(props.text, { throwOnError: false, displayMode: true })}
    {/snippet}
</SvelteMarkdown>

Mermaid Diagrams (Async Rendering)

The package includes built-in markedMermaid and MermaidRenderer helpers for Mermaid diagram support. Install mermaid as an optional peer dependency:

npm install mermaid

Then use the built-in helpers — no boilerplate needed:

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown/extensions'

    // markdown containing fenced mermaid code blocks
    let { source } = $props()

    interface MermaidRenderers extends Renderers {
        mermaid: RendererComponent
    }

    const renderers: Partial<MermaidRenderers> = {
        mermaid: MermaidRenderer
    }
</script>

<SvelteMarkdown {source} extensions={[markedMermaid()]} {renderers} />

markedMermaid() is a zero-dependency tokenizer that converts ```mermaid code blocks into custom tokens. MermaidRenderer lazy-loads mermaid in the browser, renders SVG asynchronously, and automatically re-renders when dark/light mode changes.

You can also use snippet overrides to wrap MermaidRenderer with custom markup:

<SvelteMarkdown source={markdown} extensions={[markedMermaid()]}>
    {#snippet mermaid(props)}
        <div class="my-diagram-wrapper">
            <MermaidRenderer text={props.text} />
        </div>
    {/snippet}
</SvelteMarkdown>

Since Mermaid rendering is async, the snippet delegates to MermaidRenderer rather than calling mermaid.render() directly. This pattern works for any async extension — keep the async logic in a component and use the snippet for layout customization.

GitHub Alerts

Built-in support for GitHub-style alerts/admonitions. Five alert types are supported: NOTE, TIP, IMPORTANT, WARNING, and CAUTION.

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import { markedAlert, AlertRenderer } from '@humanspeak/svelte-markdown/extensions'

    const source = `
> [!NOTE]
> Useful information that users should know.

> [!WARNING]
> Urgent info that needs immediate attention.
`

    interface AlertRenderers extends Renderers {
        alert: RendererComponent
    }

    const renderers: Partial<AlertRenderers> = {
        alert: AlertRenderer
    }
</script>

<SvelteMarkdown {source} extensions={[markedAlert()]} {renderers} />

AlertRenderer renders a <div class="markdown-alert markdown-alert-{type}"> with a title — no inline styles, so you can theme it with your own CSS. You can also use snippet overrides:

<SvelteMarkdown source={markdown} extensions={[markedAlert()]}>
    {#snippet alert(props)}
        <div class="my-alert my-alert-{props.alertType}">
            <strong>{props.alertType}</strong>
            <p>{props.text}</p>
        </div>
    {/snippet}
</SvelteMarkdown>

Footnotes

Built-in support for footnote references and definitions. Footnote references ([^id]) render as superscript links, and definitions ([^id]: content) render as a numbered list at the end of the document with back-links.

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
    import {
        markedFootnote,
        FootnoteRef,
        FootnoteSection
    } from '@humanspeak/svelte-markdown/extensions'

    const source = `
Here is a statement[^1] with a footnote.

Another claim[^note] that needs a source.

[^1]: This is the first footnote.
[^note]: This is a named footnote.
`

    interface FootnoteRenderers extends Renderers {
        footnoteRef: RendererComponent
        footnoteSection: RendererComponent
    }

    const renderers: Partial<FootnoteRenderers> = {
        footnoteRef: FootnoteRef,
        footnoteSection: FootnoteSection
    }
</script>

<SvelteMarkdown {source} extensions={[markedFootnote()]} {renderers} />

FootnoteRef renders <sup><a href="#fn-{id}">{id}</a></sup> and FootnoteSection renders an <ol> with bidirectional links (ref to definition and back). You can also use snippet overrides for custom rendering.

How It Works

Marked extensions define custom token types with a name property (e.g., inlineKatex, blockKatex, alert). When you pass extensions via the extensions prop, SvelteMarkdown automatically extracts these token type names and makes them available as both component renderer keys and snippet override names.

To find the token type names for any extension, check its source or documentation for the name field in its extensions array:

// Example: markedKatex (built-in) registers tokens named "inlineKatex" and "blockKatex"
// → use renderers={{ inlineKatex: ..., blockKatex: ... }}
// → or {#snippet inlineKatex(props)} and {#snippet blockKatex(props)}

// Example: a custom alert extension registers a token named "alert"
// → use renderers={{ alert: AlertComponent }}
// → or {#snippet alert(props)}

Each snippet/component receives the token's properties as props (e.g., text, displayMode for KaTeX; text, level for alerts).

See the full documentation and interactive demo.

TypeScript

All snippet prop types are exported for use in external components:

import type {
    ParagraphSnippetProps,
    HeadingSnippetProps,
    LinkSnippetProps,
    CodeSnippetProps,
    HtmlSnippetProps,
    SnippetOverrides,
    HtmlSnippetOverrides
} from '@humanspeak/svelte-markdown'

Advanced Features

Table Support with Mixed Content

The package excels at handling complex nested structures and mixed content:

| Type       | Content                                 |
| ---------- | --------------------------------------- |
| Nested     | <div>**bold** and _italic_</div>        |
| Mixed List | <ul><li>Item 1</li><li>Item 2</li></ul> |
| Code       | <code>`inline code`</code>              |

HTML in Markdown

Seamlessly mix HTML and Markdown:

<div style="color: blue">
  ### This is a Markdown heading inside HTML
  And here's some **bold** text too!
</div>

<details>
<summary>Click to expand</summary>

- This is a markdown list
- Inside an HTML details element
- Supporting **bold** and _italic_ text

</details>

Performance

Intelligent Token Caching

Parsed tokens are automatically cached using an LRU strategy, providing 50-200x faster re-renders for previously seen content (< 1ms vs 50-200ms). The cache uses FNV-1a hashing keyed on source + options, with LRU eviction (default 50 documents) and TTL expiration (default 5 minutes). No configuration required.

import { tokenCache, TokenCache } from '@humanspeak/svelte-markdown'

// Manual cache management
tokenCache.clearAllTokens()
tokenCache.deleteTokens(markdown, options)

// Custom cache instance
const myCache = new TokenCache({ maxSize: 100, ttl: 10 * 60 * 1000 })

Smart Image Lazy Loading

Images automatically lazy load using native loading="lazy" and IntersectionObserver prefetching, with a smooth fade-in animation and error state handling. To disable lazy loading, provide a custom Image renderer:

<!-- EagerImage.svelte -->
<script lang="ts">
    let { href = '', title = undefined, text = '' } = $props()
</script>

<img src={href} {title} alt={text} loading="eager" />
<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import EagerImage from './EagerImage.svelte'

    const renderers = { image: EagerImage }
</script>

<SvelteMarkdown source={markdown} {renderers} />

LLM Streaming

For real-time rendering of AI responses from ChatGPT, Claude, Gemini, and other LLMs, enable the streaming prop. This uses a smart diff algorithm that re-parses the full source for correctness but only updates changed DOM nodes, keeping render times constant regardless of document size.

The preferred API is now imperative: bind the component instance and call writeChunk() as chunks arrive. This avoids prop reactivity edge cases like identical consecutive string chunks being coalesced.

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import type { StreamingChunk } from '@humanspeak/svelte-markdown'

    let markdown:
        | {
              writeChunk: (chunk: StreamingChunk) => void
              resetStream: (nextSource?: string) => void
          }
        | undefined

    async function streamResponse() {
        const response = await fetch('/api/chat', { method: 'POST', body: '...' })
        const reader = response.body.getReader()
        const decoder = new TextDecoder()

        while (true) {
            const { done, value } = await reader.read()
            if (done) break
            markdown?.writeChunk(decoder.decode(value, { stream: true }))
        }
    }
</script>

<SvelteMarkdown bind:this={markdown} source="" streaming={true} />

For websocket-style offset patches, pass an object chunk instead:

markdown?.writeChunk({ value: 'world', offset: 6 })

Object chunks overwrite the internal buffer at offset. This is overwrite semantics, not insert semantics: the chunk replaces characters starting at that index and preserves any trailing content after the overwritten span.

If offset skips ahead, missing positions are padded with spaces. There is no delete or truncate behavior in offset mode.

Typical websocket-style usage can arrive out of order:

markdown?.writeChunk({ value: ' world', offset: 5 })
markdown?.writeChunk({ value: 'Hello', offset: 0 })

The internal buffer converges as later patches fill earlier gaps.

You can reset the internal streaming buffer at any time:

markdown?.resetStream('')
markdown?.resetStream('# Seeded response')

The first successful write after a reset locks the stream into one input mode:

  • string chunks: append mode
  • { value, offset } chunks: offset mode

Switching modes before resetStream() or a source prop reset logs a warning and drops the chunk. Offset chunks must use a non-negative safe integer offset.

Changing the source prop also resets the imperative buffer, seeds a new baseline value, and unlocks the input mode.

Appending directly to source is still supported:

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'

    let source = $state('')

    function onChunk(chunk: string) {
        source += chunk
    }
</script>

<SvelteMarkdown {source} streaming={true} />

Performance (measured at 100 characters/sec, character mode):

| Metric | Standard Mode | Streaming Mode | | -------------- | :-----------: | :------------: | | Average render | ~3.6ms | ~1.6ms | | Peak render | ~21ms | ~10ms | | Dropped frames | 0 | 0 |

When streaming is false (default), existing behavior is unchanged. The streaming prop skips cache lookups (always a miss during streaming) and uses in-place token array mutation so Svelte only re-renders components for tokens that actually changed.

Note: streaming is automatically disabled when async extensions (e.g., markedMermaid) are used. A console warning is logged in this case.

See the full streaming documentation and interactive demo.

Available Renderers

  • text - Text within other elements
  • paragraph - Paragraph (<p>)
  • em - Emphasis (<em>)
  • strong - Strong/bold (<strong>)
  • hr - Horizontal rule (<hr>)
  • blockquote - Block quote (<blockquote>)
  • del - Deleted/strike-through (<del>)
  • link - Link (<a>)
  • image - Image (<img>)
  • table - Table (<table>)
  • tablehead - Table head (<thead>)
  • tablebody - Table body (<tbody>)
  • tablerow - Table row (<tr>)
  • tablecell - Table cell (<td>/<th>)
  • list - List (<ul>/<ol>)
  • listitem - List item (<li>)
  • heading - Heading (<h1>-<h6>)
  • codespan - Inline code (<code>)
  • code - Block of code (<pre><code>)
  • html - HTML node
  • rawtext - All other text that is going to be included in an object above

Optional List Renderers

For fine-grained styling:

  • orderedlistitem - Items in ordered lists
  • unorderedlistitem - Items in unordered lists

HTML Renderers

The html renderer is special and can be configured separately to handle HTML elements:

| Element | Description | | -------- | -------------------- | | div | Division element | | span | Inline container | | table | HTML table structure | | thead | Table header group | | tbody | Table body group | | tr | Table row | | td | Table data cell | | th | Table header cell | | ul | Unordered list | | ol | Ordered list | | li | List item | | code | Code block | | em | Emphasized text | | strong | Strong text | | a | Anchor/link | | img | Image |

You can customize HTML rendering by providing your own components:

import type { HtmlRenderers } from '@humanspeak/svelte-markdown'

const customHtmlRenderers: Partial<HtmlRenderers> = {
    div: YourCustomDivComponent,
    span: YourCustomSpanComponent
}

Events

The component emits a parsed event when tokens are calculated:

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'

    const handleParsed = (tokens: Token[] | TokensList) => {
        console.log('Parsed tokens:', tokens)
    }
</script>

<SvelteMarkdown {source} parsed={handleParsed} />

Props

| Prop | Type | Description | | ------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------ | | source | string \| Token[] | Markdown content or pre-parsed tokens | | streaming | boolean | Enable incremental rendering for LLM streaming | | renderers | Partial<Renderers> | Custom component overrides | | options | SvelteMarkdownOptions | Marked parser configuration | | isInline | boolean | Toggle inline parsing mode | | extensions | MarkedExtension[] | Third-party marked extensions (e.g., KaTeX math) | | sanitizeUrl | SanitizeUrlFn | URL sanitizer applied before render. Defaults to defaultSanitizeUrl (http/https/mailto/tel/relative) | | sanitizeAttributes | SanitizeAttributesFn | Attribute sanitizer applied before render. Defaults to defaultSanitizeAttributes |

Security

This package takes a defense-in-depth approach to security. The defaults below are applied automatically in the Parser before tokens reach any renderer or snippet, so custom renderers cannot bypass them.

On by default:

  • Secure HTML parsing — All HTML is parsed through HTMLParser2's streaming parser rather than innerHTML, preventing script injection
  • URL protocol allowlist (defaultSanitizeUrl) — Markdown link/image URLs and the HTML attributes href, src, action, formaction, cite, data, and poster are restricted to http:, https:, mailto:, tel:, and relative URLs. javascript:, vbscript:, data:, and blob: URIs are blocked (including mixed-case and leading-whitespace variants).
  • Event handler stripping (defaultSanitizeAttributes) — All on* attributes (e.g. onclick, onerror, onload) are removed. The srcdoc attribute is also stripped to prevent iframe HTML injection.
  • No <script> or <style> renderers — Both tags fall through to UnsupportedHTML, which renders them as visible escaped text (e.g. <script>...</script>) rather than executing or applying them.

Configurable controls:

  • Custom sanitizers — Pass sanitizeUrl / sanitizeAttributes props to tighten or loosen the defaults. Use the exported unsanitizedUrl / unsanitizedAttributes passthroughs to disable sanitization entirely (only for trusted input).
  • Granular HTML control — Use allowHtmlOnly() / excludeHtmlOnly() to restrict which HTML tags are rendered (see Helper utilities). For example, excludeHtmlOnly(['iframe', 'form', 'embed']) if you don't want those.
  • Full HTML lockdown — Call buildUnsupportedHTML() to block all raw HTML rendering.
  • Markdown renderer control — Use allowRenderersOnly() / excludeRenderersOnly() to limit which markdown token types are rendered.

Known gaps (not handled by defaults):

  • Inline style="..." attributes are not sanitized. They pass through unchanged (only on* and srcdoc are stripped from attribute maps). Modern browsers don't execute JavaScript via CSS, but visual hijacking (e.g. display:none) and exfiltration via background-image URLs are possible.
  • iframe, form, embed are rendered by default. With on*/srcdoc stripped and src/action protocol-restricted, the worst exploits are blocked, but an iframe to an arbitrary http(s) URL is still possible. Use excludeHtmlOnly(['iframe', 'form', 'embed']) to remove them.
  • srcset and other less common URL attributes are not sanitized. Only the attributes listed above pass through sanitizeUrl. Provide a custom sanitizeAttributes if you need broader coverage.
  • No built-in DOM sanitizer — By design, the package does not bundle DOMPurify or similar. For untrusted input, layer a full sanitizer on top of the defaults above.

License

MIT © Humanspeak, Inc.

Credits

Made with ❤️ by Humanspeak