markdown-svelte
A typed Markdown renderer for Svelte 5. It parses Markdown into nodes and renders those nodes with Svelte components.
Installation
npm install markdown-svelte svelte ^5.57.1 is a peer dependency.
Basic usage
<script lang="ts">
import { MarkdownViewer } from 'markdown-svelte'
let source = $state('# Hello, **Svelte**!')
</script>
<MarkdownViewer markdown={source} /> The component renders an <article>. Updating markdown reparses the source and updates
the rendered document.
Parsed nodes
Use MarkdownRenderer to render a parsed node array without animation. Replace nodes as the document
changes, including when new nodes arrive from a worker.
<script lang="ts">
import { MarkdownRenderer, parseMarkdown } from 'markdown-svelte'
const nodes = parseMarkdown('# Hello, **Svelte**!')
</script>
<MarkdownRenderer {nodes} /> MarkdownRenderer and MarkdownStream accept nodes and the same document
options as MarkdownViewer: baseUrl, idPrefix, allowRawHtml, and
article attributes.
Streaming
Use MarkdownStream to animate incoming node updates, with a worker to keep parsing off the main thread.
For updates without animation, use MarkdownRenderer with the same nodes array.
npm install stream-markdown-parser Save these two files together in a Vite / SvelteKit app.
1. Parse in a worker
markdown.worker.ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const parser = getMarkdown('answer')
let source = ''
self.onmessage = ({ data }: MessageEvent<{ chunk: string; final?: boolean }>) => {
source += data.chunk
self.postMessage(
parseMarkdownToStructure(source, parser, {
final: data.final ?? false,
reuseStableTopLevelNodes: true
})
)
} 2. Update at most once per frame
Use one pending requestAnimationFrame callback to apply the latest result at the display's refresh rate.
Stream.svelte
<script lang="ts">
import { onMount } from 'svelte'
import type { ParsedMarkdownNode } from 'markdown-svelte'
import { MarkdownStream } from 'markdown-svelte/stream'
let nodes = $state.raw<ParsedMarkdownNode[]>([])
onMount(() => {
const worker = new Worker(new URL('./markdown.worker.ts', import.meta.url), { type: 'module' })
let latest: ParsedMarkdownNode[] = []
let frame: number | null = null
worker.onmessage = ({ data }: MessageEvent<ParsedMarkdownNode[]>) => {
latest = data
if (frame !== null) return
frame = requestAnimationFrame(() => {
nodes = latest
frame = null
})
}
// Replace these sample messages with chunks from your stream.
worker.postMessage({ chunk: '# Hello\n\n' })
worker.postMessage({ chunk: 'From a **worker**.', final: true })
return () => {
worker.terminate()
if (frame !== null) cancelAnimationFrame(frame)
}
})
</script>
<MarkdownStream {nodes} />
- Send
final: truewith the last chunk; an empty chunk is fine. - Remount the component for each new document or replay.
MarkdownViewer API
| Prop | Type | Default | Description |
|---|---|---|---|
markdown | string | required | Markdown source to parse and render. |
baseUrl | string | URL | undefined | Resolves relative links and images against an HTTP or HTTPS document URL. |
idPrefix | string | undefined | Namespaces heading, footnote, and local fragment IDs. |
allowRawHtml | boolean | false | Renders sanitized raw HTML when explicitly enabled. |
Standard article attributes are also accepted and forwarded to the root element, including class, aria-* attributes, and event handlers.
<script lang="ts">
import { MarkdownViewer } from 'markdown-svelte'
const source = '# Hello, **Svelte**!'
</script>
<MarkdownViewer
markdown={source}
baseUrl="https://docs.example.com/guide/"
idPrefix="guide"
class="documentation"
aria-label="Rendered documentation"
/>Parser and types
Use parseMarkdown when an application needs the parsed node array separately from rendering. The package
also exports the component props and parsed-node types.
import { parseMarkdown, type ParsedMarkdownNode } from 'markdown-svelte'
const nodes: ParsedMarkdownNode[] = parseMarkdown('# API')Styling
No global package stylesheet is required. Set CSS custom properties on the component class for theme changes. Stable markdown-svelte-* classes are available for element-level overrides.
<script lang="ts">
import { MarkdownViewer } from 'markdown-svelte'
const source = '# Hello, **Svelte**!'
</script>
<MarkdownViewer markdown={source} class="documentation" />
<style>
:global(.documentation) {
--markdown-color-text: #172033;
--markdown-color-link: #075985;
--markdown-color-surface: #f5f7fa;
--markdown-radius: 0.4rem;
}
:global(.documentation .markdown-svelte-heading--2) {
border-bottom: 0;
}
</style> Base element rules use zero-specificity :where(...) selectors, so these overrides do not need !important. Use :global(...) from a scoped Svelte style block when targeting rendered descendants.
The same class and variables work on MarkdownStream. See the full styling reference for every element, theme variable, and streaming animation
setting.
Rendering and safety
- Text and code are rendered through Svelte interpolation.
- Raw HTML is escaped by default and must be enabled with
allowRawHtml. - Enabled raw HTML is passed through the parser's safe sanitizer before rendering.
- Active URL schemes and protocol-relative URLs are rejected.
- External HTTP links receive
rel="noopener noreferrer". - Math source is displayed without bundling a math typesetter.