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.4.1

Published

Fast, customizable markdown renderer for Svelte with built-in caching, TypeScript support, and Svelte 5 runes

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 XSS protection
  • 🚀 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} />

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 third-party marked extensions via the extensions prop. The component handles registering tokenizers internally — you just provide renderers for the custom token types.

KaTeX Math Rendering

npm install marked-katex-extension katex

Component renderer approach:

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

    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({ throwOnError: false })]}
    {renderers}
/>

Where KatexRenderer.svelte is:

<script lang="ts">
    import katex from 'katex'

    interface Props {
        text: string
        displayMode?: boolean
    }
    const { text, displayMode = false }: Props = $props()

    const html = $derived(katex.renderToString(text, { throwOnError: false, displayMode }))
</script>

{@html html}

Snippet override approach (no separate component file needed):

<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'
    import katex from 'katex'
    import markedKatex from 'marked-katex-extension'
</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({ throwOnError: false })]}
>
    {#snippet inlineKatex(props)}
        {@html katex.renderToString(props.text, { displayMode: false })}
    {/snippet}
    {#snippet blockKatex(props)}
        {@html katex.renderToString(props.text, { 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: marked-katex-extension 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) |

Security

This package takes a defense-in-depth approach to security:

  • Secure HTML parsing - All HTML is parsed through HTMLParser2's streaming parser rather than innerHTML, preventing script injection
  • XSS protection - HTML entities are safely handled; malicious markdown injection is neutralized during parsing
  • Granular HTML control - Use allowHtmlOnly() / excludeHtmlOnly() to restrict which HTML tags are rendered (see Helper utilities)
  • 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
  • No built-in sanitizer - By design, the package does not bundle a sanitizer. Integrate your own (e.g., DOMPurify) if you accept untrusted input

License

MIT © Humanspeak, Inc.

Credits

Made with ❤️ by Humanspeak