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: true with the last chunk; an empty chunk is fine.
  • Remount the component for each new document or replay.

MarkdownViewer API

PropTypeDefaultDescription
markdownstringrequiredMarkdown source to parse and render.
baseUrlstring | URLundefinedResolves relative links and images against an HTTP or HTTPS document URL.
idPrefixstringundefinedNamespaces heading, footnote, and local fragment IDs.
allowRawHtmlbooleanfalseRenders 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.