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 two high-level components for rendering markdown in Svelte 5:

  • <Comark> — Uses $state + $effect for async parsing. No experimental features required.
  • <ComarkAsync> — Uses Svelte's experimental await support with <svelte:boundary>. Cleaner, but requires experimental.async in your Svelte config.

Both components accept the same props and produce the same output.

Installation

npm install @comark/svelte

Basic Usage

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

  const content = `# Hello World

This is **markdown** with Comark components.

::alert{type="info"}
This is an alert!
::
`
</script>

<Comark markdown={content} components={{ alert: Alert }} />

Props

PropTypeDefaultDescription
markdownstring''Markdown content to parse and render
optionsParseOptions{}Parser options (autoUnwrap, autoClose, etc.)
pluginsComarkPlugin[][]Array of plugins (highlight, emoji, toc, etc.)
componentsRecord<string, Component>{}Custom Svelte component mappings
componentsManifestComponentManifestundefinedDynamic component resolver function
streamingbooleanfalseEnable streaming mode
caretboolean | { class: string }falseAppend caret to last text node
classstring''CSS class for wrapper element

With Parser Options

Use the options prop to configure parser behavior:

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

  const content = '# Hello World'
  const options = {
    autoUnwrap: true,  // Remove <p> wrappers from single-paragraph containers
    autoClose: true,   // Auto-close incomplete syntax
  }
</script>

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

With Plugins

Use the plugins prop to add functionality like syntax highlighting, emoji support, or table of contents:

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} />

With Math Plugin

The @comark/svelte/plugins/math subpath bundles the math plugin and a Svelte rendering component:

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

  const markdown = 'The formula $E = mc^2$ is famous.'
</script>

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

With Mermaid Plugin

The @comark/svelte/plugins/mermaid subpath provides Mermaid diagram support:

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

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

With Custom Components

Map Svelte components to Comark elements using the components prop. Components are resolved by:

  1. Prose{PascalTag} — e.g., ProseH1 for <h1> elements
  2. PascalTag — e.g., Alert for ::alert components
  3. tag — e.g., alert for ::alert components
App.svelte
<script lang="ts">
  import { Comark } from '@comark/svelte'
  import Alert from './Alert.svelte'
  import ProseH1 from './ProseH1.svelte'

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

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

A custom component receives the AST node's attributes as props and its children as a Svelte children snippet:

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>

Overriding Native HTML Elements

Use the Prose prefix to override how native HTML elements render. For example, to customize all <h1> elements:

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>
App.svelte
<Comark markdown={content} components={{ ProseH1 }} />

Experimental Async (ComarkAsync)

The ComarkAsync component uses Svelte's experimental await in $derived for a more declarative approach. This requires experimental.async in your Svelte config:

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

export default config

Wrap ComarkAsync in a <svelte:boundary> to handle loading and error states:

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.

Low-Level Rendering

For more control, use ComarkRenderer to render a pre-parsed AST tree:

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

  let tree = $state(null)

  $effect(() => {
    parse('# Hello **World**').then((result) => {
      tree = result
    })
  })
</script>

{#if tree}
  <ComarkRenderer {tree} />
{/if}

Streaming

Enable streaming mode to render content as it arrives, with a blinking caret indicator:

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. The caret is only visible while streaming is true.

Customize the caret with a CSS class:

<Comark markdown={content} streaming={isStreaming} caret={{ class: 'my-caret' }} />

For more details on streaming, see the Streaming guide.


Next Steps

Copyright © 2026