Render Comark in Svelte
The @comark/svelte package provides two high-level components for rendering markdown in Svelte 5:
<Comark>— Uses$state+$effectfor async parsing. No experimental features required.<ComarkAsync>— Uses Svelte's experimentalawaitsupport with<svelte:boundary>. Cleaner, but requiresexperimental.asyncin your Svelte config.
Both components accept the same props and produce the same output.
Installation
npm install @comark/svelte
Basic Usage
<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
| Prop | Type | Default | Description |
|---|---|---|---|
markdown | string | '' | Markdown content to parse and render |
options | ParseOptions | {} | Parser options (autoUnwrap, autoClose, etc.) |
plugins | ComarkPlugin[] | [] | Array of plugins (highlight, emoji, toc, etc.) |
components | Record<string, Component> | {} | Custom Svelte component mappings |
componentsManifest | ComponentManifest | undefined | Dynamic component resolver function |
streaming | boolean | false | Enable streaming mode |
caret | boolean | { class: string } | false | Append caret to last text node |
class | string | '' | CSS class for wrapper element |
With Parser Options
Use the options prop to configure parser behavior:
<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:
<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:
<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:
<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:
Prose{PascalTag}— e.g.,ProseH1for<h1>elementsPascalTag— e.g.,Alertfor::alertcomponentstag— e.g.,alertfor::alertcomponents
<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:
<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:
<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>
<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:
const config = {
compilerOptions: {
experimental: {
async: true,
},
},
}
export default config
Wrap ComarkAsync in a <svelte:boundary> to handle loading and error states:
<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>
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:
<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:
<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
- Streaming - Real-time incremental rendering with auto-close
- Vue Rendering - Full Vue component API
- React Rendering - Full React component API
- Parse API - Parser options and configuration