logo

SvelteMarkdown Component

The SvelteMarkdown component is the main entry point for rendering markdown content. It accepts a markdown string (or pre-parsed tokens), parses it, and renders the result through a composable system of Svelte renderer components.

<script>
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
</script>

<SvelteMarkdown source="# Hello World" />
<script>
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
</script>

<SvelteMarkdown source="# Hello World" />

Props

source

  • Type: string | Token[]
  • Default: []

The markdown content to render. Can be either a raw markdown string or an array of pre-parsed Marked tokens.

When a string is provided, it is parsed using Marked’s lexer (with built-in token caching for performance). When pre-parsed tokens are provided, parsing is skipped entirely.

When using the imperative streaming API (bind:this + writeChunk()), source must be a string. Token-array sources remain read-only.

<!-- String source -->
<SvelteMarkdown source="**Bold** and *italic*" />

<!-- Pre-parsed tokens -->
<SvelteMarkdown source={myTokens} />
<!-- String source -->
<SvelteMarkdown source="**Bold** and *italic*" />

<!-- Pre-parsed tokens -->
<SvelteMarkdown source={myTokens} />

renderers

  • Type: Partial<Renderers>
  • Default: {}

An object mapping renderer keys to custom Svelte components. Any keys you provide will override the corresponding default renderer. Keys you omit will use the built-in defaults.

The renderers object supports two categories of keys:

  1. Markdown renderer keys (e.g., heading, paragraph, link, code) — see Markdown Renderers
  2. html — an object mapping HTML tag names to components — see HTML Renderers
<script>
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import CustomHeading from './CustomHeading.svelte'
    import CustomLink from './CustomLink.svelte'

    const source = '# Title\n\n[Click here](https://example.com)'
</script>

<SvelteMarkdown
    {source}
    renderers={{
        heading: CustomHeading,
        link: CustomLink
    }}
/>
<script>
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import CustomHeading from './CustomHeading.svelte'
    import CustomLink from './CustomLink.svelte'

    const source = '# Title\n\n[Click here](https://example.com)'
</script>

<SvelteMarkdown
    {source}
    renderers={{
        heading: CustomHeading,
        link: CustomLink
    }}
/>

To override HTML tag renderers within markdown, use the nested html key:

<SvelteMarkdown
    {source}
    renderers={{
        html: {
            div: CustomDiv,
            span: CustomSpan
        }
    }}
/>
<SvelteMarkdown
    {source}
    renderers={{
        html: {
            div: CustomDiv,
            span: CustomSpan
        }
    }}
/>

When you provide a partial html object, it is merged with the defaults — your overrides replace only the tags you specify, and all other HTML tags continue using their built-in renderers.

options

  • Type: Partial<SvelteMarkdownOptions>
  • Default: {}

Configuration options for the markdown parser. These are merged with the default options.

<SvelteMarkdown
    source={markdown}
    options={{
        headerIds: true,
        headerPrefix: 'section-',
        gfm: true,
        breaks: true
    }}
/>
<SvelteMarkdown
    source={markdown}
    options={{
        headerIds: true,
        headerPrefix: 'section-',
        gfm: true,
        breaks: true
    }}
/>

Available Options

OptionTypeDefaultDescription
headerIdsbooleantrueGenerate id attributes on heading elements using github-slugger
headerPrefixstring''String to prepend to generated heading IDs
gfmbooleantrueEnable GitHub Flavored Markdown (tables, strikethrough, task lists, etc.)
breaksbooleanfalseConvert single newlines in paragraphs to <br> elements

The options object also passes through Marked’s own configuration properties such as async, pedantic, silent, tokenizer, and walkTokens.

isInline

  • Type: boolean
  • Default: false

When set to true, the markdown is parsed as inline content. This means block-level elements like headings, paragraphs, lists, and blockquotes are not produced. Only inline elements such as bold, italic, links, code spans, and images are rendered.

This is useful when you want to embed markdown within a <span> or other inline HTML context.

<p>
    Status: <SvelteMarkdown source="**Active** since *2024*" isInline={true} />
</p>
<p>
    Status: <SvelteMarkdown source="**Active** since *2024*" isInline={true} />
</p>

streaming

  • Type: boolean
  • Default: false

Enables optimized rendering for LLM streaming scenarios. When true, the component diffs parsed tokens against the previous result and only updates changed DOM nodes, keeping render cost proportional to the change rather than the full document size.

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

streaming also enables the component instance methods described below.

Note: streaming is automatically disabled when async extensions are used. A console warning is logged in this case.

See LLM Streaming for usage patterns and performance data.

Instance Methods

When you bind the component instance, SvelteMarkdown exposes imperative helpers for streaming writers:

writeChunk(chunk)

  • Type: (chunk: StreamingChunk) => void

Accepts either a string append chunk or an offset patch object:

<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

    function onChunk(chunk: StreamingChunk) {
        markdown?.writeChunk(chunk)
    }
</script>

<SvelteMarkdown bind:this={markdown} source="" streaming={true} />
<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

    function onChunk(chunk: StreamingChunk) {
        markdown?.writeChunk(chunk)
    }
</script>

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

Rules:

  • String chunks append to the internal buffer.
  • Object chunks use { value, offset } overwrite semantics, not insert semantics.
  • Offset writes preserve trailing content after the overwritten span.
  • If offset skips ahead, the gap is padded with spaces.
  • Offset mode does not support delete or truncate semantics.
  • The first successful write locks the stream into append mode or offset mode until reset.
  • Offset chunks require a non-negative safe integer offset.
  • Calls are ignored with a warning when streaming={false}, async extensions are active, or source is a token array.

resetStream(nextSource?)

  • Type: (nextSource?: string) => void

Clears the internal streaming buffer and resets mode locking. Pass a string to seed the next stream:

markdown?.resetStream()
markdown?.resetStream('# Seed')
markdown?.resetStream()
markdown?.resetStream('# Seed')

Changing the source prop also resets the imperative buffer and becomes the new baseline.

parsed

  • Type: (tokens: Token[] | TokensList) => void
  • Default: () => {}

A callback function that is invoked with the parsed token array whenever the source is parsed. This is useful for inspecting the token tree, building a table of contents, or performing other analysis on the parsed output.

<script>
    import SvelteMarkdown from '@humanspeak/svelte-markdown'

    let headings = $state([])

    function handleParsed(tokens) {
        headings = tokens
            .filter(t => t.type === 'heading')
            .map(t => ({ depth: t.depth, text: t.text }))
    }
</script>

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

<nav>
    {#each headings as h}
        <a href="#{h.text}">{h.text} (h{h.depth})</a>
    {/each}
</nav>
<script>
    import SvelteMarkdown from '@humanspeak/svelte-markdown'

    let headings = $state([])

    function handleParsed(tokens) {
        headings = tokens
            .filter(t => t.type === 'heading')
            .map(t => ({ depth: t.depth, text: t.text }))
    }
</script>

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

<nav>
    {#each headings as h}
        <a href="#{h.text}">{h.text} (h{h.depth})</a>
    {/each}
</nav>

The callback fires reactively via a Svelte $effect — it will be called again whenever the tokens change (e.g., when source changes).

Default Options

The full set of default options applied when no options prop is provided:

const defaultOptions: SvelteMarkdownOptions = {
    async: false,
    breaks: false,
    gfm: true,
    pedantic: false,
    renderer: null,
    silent: false,
    tokenizer: null,
    walkTokens: null,
    headerIds: true,
    headerPrefix: ''
}
const defaultOptions: SvelteMarkdownOptions = {
    async: false,
    breaks: false,
    gfm: true,
    pedantic: false,
    renderer: null,
    silent: false,
    tokenizer: null,
    walkTokens: null,
    headerIds: true,
    headerPrefix: ''
}

Token Caching

When you pass a string to source, the component automatically caches parsed tokens using an internal LRU cache. On subsequent renders with the same source and options, the cached tokens are returned in under 1ms instead of re-parsing (which can take 50-200ms for large documents).

The cache is a global singleton (tokenCache) shared across all SvelteMarkdown instances. It defaults to 50 entries with a 5-minute TTL. See Token Caching for advanced configuration.

When you pass pre-parsed tokens to source, caching is bypassed entirely.

Renderer Merging

The component merges your custom renderers with the defaults:

// Internal behavior
const combinedRenderers = {
    ...defaultRenderers,
    ...renderers,
    html: renderers.html
        ? { ...defaultRenderers.html, ...renderers.html }
        : defaultRenderers.html
}
// Internal behavior
const combinedRenderers = {
    ...defaultRenderers,
    ...renderers,
    html: renderers.html
        ? { ...defaultRenderers.html, ...renderers.html }
        : defaultRenderers.html
}

This means:

  • Top-level renderer keys you provide replace the defaults
  • Top-level renderer keys you omit use the built-in components
  • For html, your overrides are merged with the default HTML renderer map

Complete Example

<script>
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import CustomHeading from './CustomHeading.svelte'
    import CustomCode from './CustomCode.svelte'

    let source = $state('# Hello\n\n```js\nconsole.log("hi")\n```')
    let tokens = $state([])
</script>

<SvelteMarkdown
    {source}
    renderers={{
        heading: CustomHeading,
        code: CustomCode
    }}
    options={{
        headerIds: true,
        headerPrefix: 'doc-',
        gfm: true
    }}
    parsed={(t) => tokens = t}
/>

<p>Parsed {tokens.length} tokens</p>
<script>
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import CustomHeading from './CustomHeading.svelte'
    import CustomCode from './CustomCode.svelte'

    let source = $state('# Hello\n\n```js\nconsole.log("hi")\n```')
    let tokens = $state([])
</script>

<SvelteMarkdown
    {source}
    renderers={{
        heading: CustomHeading,
        code: CustomCode
    }}
    options={{
        headerIds: true,
        headerPrefix: 'doc-',
        gfm: true
    }}
    parsed={(t) => tokens = t}
/>

<p>Parsed {tokens.length} tokens</p>

Related