Rendering

Render Comark in Svelte

Learn how to render Comark in a Svelte 5 application with custom components, plugins, and streaming support.

The @comark/svelte package provides Svelte 5 components for rendering Comark content with full support for custom components, plugins, and streaming.

Installation

pnpm add @comark/svelte

<Comark>

The <Comark> component is the simplest way to render markdown in Svelte 5. It handles parsing and rendering automatically using $state and $effect.

<Comark> uses $effect internally and will not render during SSR. For SvelteKit, parse in your load() function and use <ComarkRenderer> instead.

Usage

Pass markdown content via the markdown prop:

App.svelte
<script lang="ts">
  import { Comark } from '@comark/svelte'

  let content = $state('# Hello\n\nThis is **markdown**.')
</script>

<Comark markdown={content} />

Props

PropTypeDefaultDescription
markdownstring''Markdown content to parse and render
optionsParseOptions{}Parser options (autoUnwrap, autoClose, etc.)
pluginsComarkPlugin[][]Array of plugins
componentsRecord<string, Component>{}Custom Svelte component mappings
componentsManifestComponentManifestundefinedDynamic component resolver
streamingbooleanfalseEnable streaming mode
caretboolean | { class: string }falseAppend caret to last text node
dataRecord<string, unknown>undefinedRuntime values referenced from markdown via :prop="data.path"
classstring''CSS class for wrapper element

options

App.svelte
<Comark markdown={content} options={{ autoUnwrap: true, autoClose: true }} />

plugins

See ComarkPlugin for available plugins.

App.svelte
<script lang="ts">
  import { Comark } from '@comark/svelte'
  import highlight from '@comark/svelte/plugins/highlight'
  import githubLight from '@shikijs/themes/github-light'

  const plugins = [
    highlight({ theme: githubLight }),
  ]
</script>

<Comark markdown="```js\nconsole.log('hello')\n```" {plugins} />

For math and mermaid plugins, also pass the companion components:

App.svelte
<script lang="ts">
  import { Comark } from '@comark/svelte'
  import math, { Math } from '@comark/svelte/plugins/math'
  import mermaid, { Mermaid } from '@comark/svelte/plugins/mermaid'
</script>

<Comark
  {markdown}
  components={{ math: Math, mermaid: Mermaid }}
  plugins={[math(), mermaid()]}
/>

components

Map custom Svelte components to Comark elements. Components receive props from the markdown and children as a Svelte children snippet.

Create a Svelte component

Alert.svelte
<script lang="ts">
  import type { Snippet } from 'svelte'

  let { type = 'info', children }: { type?: string, children?: Snippet } = $props()
</script>

<div class="alert alert-{type}" role="alert">
  {@render children?.()}
</div>

Map the tag to your component

App.svelte
<script lang="ts">
  import { Comark } from '@comark/svelte'
  import Alert from './Alert.svelte'

  const components = { alert: Alert }
</script>

<Comark markdown={content} {components} />

Use it in your Markdown content

::alert{type="warning"}
This is a warning message!
::
See Component Bindings for how props and snippets map to your Svelte component.

componentsManifest

For lazy-loading components on demand:

App.svelte
<script lang="ts">
  import { Comark } from '@comark/svelte'

  const manifest = (name: string) => {
    return import(`./components/${name}.svelte`)
  }
</script>

<Comark {markdown} componentsManifest={manifest} />

data

Expose runtime values to markdown authors. Any prop written with a : prefix is resolved against the render context { frontmatter, meta, data, props } when its value isn't valid JSON — see Data Binding for the full scope.

App.svelte
<script lang="ts">
  import { Comark } from '@comark/svelte'

  const user = { name: 'Ada', role: 'admin' }
  const content = `Hello, :badge{:label="data.user.name"}!`
</script>

<Comark markdown={content} data={{ user }} />

<ComarkAsync> (experimental)

<ComarkAsync> uses Svelte's experimental await in $derived for a more declarative approach. Requires experimental.async in your Svelte config:

svelte.config.js
const config = {
  compilerOptions: {
    experimental: {
      async: true,
    },
  },
}

export default config
App.svelte
<script lang="ts">
  import { ComarkAsync } from '@comark/svelte/async'

  let content = $state('# Hello World')
</script>

<svelte:boundary>
  <ComarkAsync markdown={content} />
  {#snippet pending()}
    <p>Loading...</p>
  {/snippet}
  {#snippet failed(error, reset)}
    <p>Error: {error.message}</p>
    <button onclick={reset}>Retry</button>
  {/snippet}
</svelte:boundary>
The experimental.async feature is still experimental in Svelte 5. For production use, prefer the stable <Comark> component.

<ComarkRenderer>

Renders a pre-parsed ComarkTree without any parsing. Use it when you parse on the server (e.g., in a SvelteKit load function) — no parser or plugin code is shipped to the browser.

Integration

Fetch the tree in your load function

src/routes/docs/[slug]/+page.ts
import type { PageLoad } from './$types'

export const load: PageLoad = async ({ params, fetch }) => {
  const res = await fetch(`/api/content/${params.slug}`)
  const tree = await res.json()
  return { tree }
}

Render with ComarkRenderer

src/routes/docs/[slug]/+page.svelte
<script lang="ts">
  import { ComarkRenderer } from '@comark/svelte'
  import Alert from '$lib/components/Alert.svelte'
  import type { PageData } from './$types'

  let { data }: { data: PageData } = $props()
</script>

<ComarkRenderer tree={data.tree} components={{ alert: Alert }} />

Renderer Props

PropTypeDefaultDescription
treeComarkTreeRequired. The parsed tree returned by parse()
componentsRecord<string, Component>{}Custom Svelte component mappings
componentsManifestComponentManifestundefinedDynamic component resolver for lazy-loaded components
streamingbooleanfalseEnable streaming mode
caretboolean | { class: string }falseAppend a blinking caret to the last text node
dataRecord<string, unknown>undefinedRuntime values referenced from markdown via :prop="data.path"
classstring''CSS class for wrapper element

componentsManifest

For lazy-loading components on demand:

App.svelte
<script lang="ts">
  import { ComarkRenderer } from '@comark/svelte'

  const manifest = (name: string) => {
    return import(`$lib/components/prose/${name}.svelte`)
  }

  let { data } = $props()
</script>

<ComarkRenderer tree={data.tree} componentsManifest={manifest} />

Component Bindings

Comark automatically bridges the gap between Comark syntax and your component's interface.

Prop Binding

Attributes in Comark syntax are passed as props to your component. Use the : prefix to pass typed values:

MarkdownProp value
{type="warning"}"warning" (string)
{:count="5"}5 (number)
{:active="true"}true (boolean)
{:config='{"key":"val"}'}{ key: 'val' } (object)

Named Snippets

Named slots in Comark (#slotname) map to Svelte 5 snippets:

  • Default contentchildren snippet
  • Named snippets → named snippet prop (e.g., #footerfooter snippet)
<script lang="ts">
  import type { Snippet } from 'svelte'

  let { title, children, footer }: {
    title?: string
    children?: Snippet
    footer?: Snippet
  } = $props()
</script>

<div class="card">
  <h3>{title}</h3>
  {@render children?.()}
  <footer>
    {@render footer?.()}
  </footer>
</div>

Overriding HTML Elements

Use the Prose prefix to override how native HTML elements render:

Create an override component

ProseH1.svelte
<script lang="ts">
  import type { Snippet } from 'svelte'

  let { id, children }: { id?: string, children?: Snippet } = $props()
</script>

<h1 {id} class="custom-heading">
  {@render children?.()}
</h1>

Map it via the components prop

App.svelte
<Comark markdown={content} components={{ ProseH1 }} />

Resolution Order

Components are resolved in this order:

  1. Prose{PascalTag} — e.g., ProseH1 for h1
  2. {PascalTag} — e.g., Alert for alert
  3. {tag} — e.g., alert

If no custom component matches, the tag renders as a native HTML element (via <svelte:element>).


Streaming

Enable real-time rendering as content arrives — ideal for AI chat interfaces and live previews.

Set streaming to true while content is being received, then false when done:

components/AiChat.svelte
<script lang="ts">
  import { Comark } from '@comark/svelte'

  let content = $state('')
  let isStreaming = $state(false)

  async function askAI(prompt: string) {
    content = ''
    isStreaming = true

    const response = await fetch('/api/chat', {
      method: 'POST',
      body: JSON.stringify({ prompt }),
    })

    const reader = response.body!.getReader()
    const decoder = new TextDecoder()

    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      content += decoder.decode(value, { stream: true })
    }

    isStreaming = false
  }
</script>

<Comark markdown={content} streaming={isStreaming} caret />
autoClose is enabled by default — incomplete syntax like **bold text is automatically closed on every parse. Disable with options={{ autoClose: false }}.

Caret

The caret prop appends a blinking cursor to the last text node while streaming is true:

App.svelte
<!-- Default caret -->
<Comark markdown={content} streaming={isStreaming} caret />

<!-- Custom caret class -->
<Comark markdown={content} streaming={isStreaming} caret={{ class: 'my-caret' }} />
.my-caret {
  display: inline-block;
  width: 2px;
  height: 1em;
  background: currentColor;
  animation: blink 1s step-end infinite;
  vertical-align: text-bottom;
}

@keyframes blink {
  50% { opacity: 0; }
}

TypeScript Support

ComarkWrapper.svelte
<script lang="ts">
  import { Comark } from '@comark/svelte'
  import type { ComarkPlugin } from 'comark'
  import type { Component } from 'svelte'

  let {
    content,
    components,
    plugins,
  }: {
    content: string
    components?: Record<string, Component>
    plugins?: ComarkPlugin[]
  } = $props()
</script>

<Comark markdown={content} {components} {plugins} />
components/prose/ProseH1.svelte
<script lang="ts">
  import type { Snippet } from 'svelte'
  import type { ComarkElement } from 'comark'

  let { id, __node, children }: {
    id?: string
    __node?: ComarkElement
    children?: Snippet
  } = $props()
</script>