---
title: "Comark vs svelte-markdown"
description: "Compare Comark with svelte-markdown and its Svelte 5 successor @humanspeak/svelte-markdown. See how each handles custom renderers, components in content, LLM streaming, and SvelteKit SSR."
canonical_url: "https://comark.dev/compare/comark-vs-svelte-markdown"
---
# Comark vs svelte-markdown

> Compare Comark with svelte-markdown and its Svelte 5 successor @humanspeak/svelte-markdown. See how each handles custom renderers, components in content, LLM streaming, and SvelteKit SSR.

**TL;DR**: Two packages share this name. [`svelte-markdown`](https://github.com/pablo-abc/svelte-markdown) renders marked tokens to Svelte components, but its last release was in December 2023 and it declares Svelte 4. [`@humanspeak/svelte-markdown`](https://github.com/humanspeak/svelte-markdown) is its Svelte 5 successor, with strong streaming support and marked extensions. Comark's [`@comark/svelte`](https://comark.dev/rendering/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](https://github.com/pablo-abc/svelte-markdown/issues/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>`:

```svelte [@humanspeak/svelte-markdown]
<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](https://comark.dev/syntax/components) with typed props and named slots. Each `#slot` becomes a Svelte 5 snippet prop:

```mdc [content.md]
::card{title="Order summary" :items="3"}
Your order ships tomorrow.

#footer
Questions? Reply to this message.
::
```

```svelte [Card.svelte]
<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](https://comark.dev/rendering/svelte#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`](https://comark.dev/reference/auto-close). `**bold` renders as bold, and an open `::card` renders as a card while its content streams. You pass the growing text to `value`:

```svelte [Message.svelte]
<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](https://comark.dev/rendering/svelte#streaming).

## 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>`:

```svelte [src/routes/docs/[slug\\]/+page.svelte]
<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*` and `srcdoc` stripping, and escaped `script` and `style` work without setup. Its documentation lists gaps, for example `iframe`, `form`, and `embed` still render. Comark keeps raw `<script>` until you add the [security plugin](https://comark.dev/plugins/built-in/security) with a `blockedTags` list.
- **marked ecosystem**: existing marked extensions plug in through the `extensions` prop.

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

::accordion
  :::accordion-item{label="Does Comark work with Svelte 4?"}
  No. `@comark/svelte` requires Svelte 5. Its components use runes, and named slots map to Svelte 5 snippets.
  :::

  :::accordion-item{label="Can I use marked extensions with Comark?"}
  No. Comark's parser is markdown-exit, a TypeScript rewrite of markdown-it. It accepts [markdown-it plugins](https://comark.dev/plugins/custom/markdown-it) and [Comark plugins](https://comark.dev/plugins/custom/plugin-api), not marked extensions.
  :::

  :::accordion-item{label="Is component syntax in model output safe?"}
  Component syntax is data. The renderer only mounts components you registered, and an unknown tag renders as a plain element. For untrusted output, add the [security plugin](https://comark.dev/plugins/built-in/security) from `@comark/svelte/plugins/security`, with a `blockedTags` list.
  :::

  :::accordion-item{label="How do I render Comark in SvelteKit with SSR?"}
  Parse in `load()` and pass the document to `<MarkdownDocument>`. The [SvelteKit example](https://comark.dev/examples/frameworks/sveltekit) shows lazy SSR and stable SSR routes.
  :::
::

---

- [Render in Svelte](https://comark.dev/rendering/svelte)
- [SvelteKit example](https://comark.dev/examples/frameworks/sveltekit)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
