Comark vs svelte-markdown
TL;DR: Two packages share this name. svelte-markdown renders marked tokens to Svelte components, but its last release was in December 2023 and it declares Svelte 4. @humanspeak/svelte-markdown is its Svelte 5 successor, with strong streaming support and marked extensions. Comark's @comark/svelte adds a component syntax for the content, completes unterminated syntax while a response streams, and renders the same content in other frameworks.
At a glance
| Comark | @humanspeak/svelte-markdown | svelte-markdown | |
|---|---|---|---|
| Svelte version | Svelte 5 | Svelte 5 (runes, snippets) | Peer svelte ^4.0.0 |
| Parser | markdown-exit | marked and htmlparser2 | marked 5 |
| Restyle elements | components map, Prose prefix | renderers and snippet overrides | renderers |
| Components in the content | ::alert{type="info"} syntax | Any HTML tag via renderers.html | No |
| Named slots | #footer maps to a named snippet | HTML tags get children only | No |
| Streaming | streaming prop, autoClose completes syntax | streaming prop, writeChunk(), IncrementalParser | No |
| Extensions | Comark plugins, markdown-it plugins | marked extensions, first-party KaTeX, Mermaid, alerts, footnotes | marked options |
| Other outputs | Vue, React, Angular, HTML, ANSI | Svelte only | Svelte only |
Which svelte-markdown?
The original svelte-markdown by Pablo Berganza is built on marked 5. Its renderers prop maps marked token types, such as heading or link, to Svelte components. Svelte 5 support isn't declared, and issue #99 about it is open since October 2024. Its README also states that a paragraph must be all HTML or all Markdown.
@humanspeak/svelte-markdown describes itself as the successor to the original package. It keeps the source and renderers props and adds runes, snippet overrides, streaming, and an HTML parser. If you start a new Svelte 5 project with this API, use the successor.
Components in the content
Both maintained options let the content place your components, with different syntax.
@humanspeak/svelte-markdown routes HTML tags to components. Any tag name works, so a model can write <thinking> or <click>:
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import ClickButton from './ClickButton.svelte'
const renderers = { html: { click: ClickButton } }
</script>
<SvelteMarkdown source={'<click>Click Me</click>'} {renderers} />Comark uses a component syntax with typed props and named slots. Each #slot becomes a Svelte 5 snippet prop:
::card{title="Order summary" :items="3"}
Your order ships tomorrow.
#footer
Questions? Reply to this message.
::<script lang="ts">
import type { Snippet } from 'svelte'
let { title, items, children, footer }: {
title?: string
items?: number
children?: Snippet
footer?: Snippet
} = $props()
</script>
<div class="card">
<h3>{title} ({items} items)</h3>
{@render children?.()}
<footer>{@render footer?.()}</footer>
</div>Register it with <Markdown value={content} components={{ card: Card }} />. For lazy loading, pass a componentsManifest function instead. See component bindings.
Streaming
@humanspeak/svelte-markdown has a detailed streaming engine. You bind the component and call writeChunk(), or append to source. It re-lexes only the open block at the end of the text and coalesces updates once per animation frame. Offset chunks can arrive out of order, and streamId resets the buffer between messages. Its documentation doesn't describe completing unterminated inline syntax; half-open tags wait in a buffer. Streaming turns off when you use an async extension such as Mermaid.
Comark completes the syntax with autoClose. **bold renders as bold, and an open ::card renders as a card while its content streams. You pass the growing text to value:
<script lang="ts">
import { Markdown } from '@comark/svelte'
let { text, isStreaming }: { text: string, isStreaming: boolean } = $props()
</script>
<Markdown value={text} streaming={isStreaming} caret />While streaming is true, <Markdown> reuses completed blocks. Set it to false when the stream ends to run a final full parse. With the AI SDK, use part.state === 'streaming' for the prop. See streaming in Svelte.
SvelteKit SSR
Comark's <Markdown> uses $effect, so it doesn't render during SSR. For SvelteKit pages, parse in load() and render the JSON-safe document with <MarkdownDocument>:
<script lang="ts">
import { MarkdownDocument } from '@comark/svelte'
import Alert from '$lib/components/comark/Alert.svelte'
let { data } = $props()
</script>
<MarkdownDocument value={data.document} components={{ alert: Alert }} /><MarkdownDocument> ships no parser to the browser. To render lazy-loaded components in server HTML, use <MarkdownAsync> from @comark/svelte/async. It requires Svelte's experimental.async option.
What @humanspeak/svelte-markdown does well
- Svelte 5-native API: snippet overrides let you change a renderer inline, without a separate component file.
- Streaming engineering: open-block re-lexing, per-frame coalescing, and out-of-order offset chunks are well documented.
- Secure defaults: a URL allowlist,
on*andsrcdocstripping, and escapedscriptandstylework without setup. Its documentation lists gaps, for exampleiframe,form, andembedstill render. Comark keeps raw<script>until you add the security plugin with ablockedTagslist. - marked ecosystem: existing marked extensions plug in through the
extensionsprop.
If your app is Svelte-only and you write standard Markdown with HTML tags, @humanspeak/svelte-markdown is a good fit. Choose Comark when the content needs components with props and slots. Comark also fits when the same content renders in Vue, React, Angular, HTML, or a terminal.
FAQ
@comark/svelte requires Svelte 5. Its components use runes, and named slots map to Svelte 5 snippets.@comark/svelte/plugins/security, with a blockedTags list.load() and pass the document to <MarkdownDocument>. The SvelteKit example shows lazy SSR and stable SSR routes.Comark vs markdown-to-jsx
markdown-to-jsx renders JSX tags in Markdown with zero dependencies and many output targets. Comark adds a component syntax with YAML props and named slots, streaming auto-close, and Svelte, Angular, and terminal renderers.
Comark vs vue-markdown-render
Compare Comark with vue-markdown-render and @crazydos/vue-markdown for Vue 3. See how each renders Markdown, maps elements to Vue components, handles raw HTML, and streams AI output.