Render Comark in Svelte
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/sveltenpm install @comark/svelteyarn add @comark/sveltebun 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:
<script lang="ts">
import { Comark } from '@comark/svelte'
let content = $state('# Hello\n\nThis is **markdown**.')
</script>
<Comark markdown={content} />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 |
components | Record<string, Component> | {} | Custom Svelte component mappings |
componentsManifest | ComponentManifest | undefined | Dynamic component resolver |
streaming | boolean | false | Enable streaming mode |
caret | boolean | { class: string } | false | Append caret to last text node |
data | Record<string, unknown> | undefined | Runtime values referenced from markdown via :prop="data.path" |
class | string | '' | CSS class for wrapper element |
options
<Comark markdown={content} options={{ autoUnwrap: true, autoClose: true }} />plugins
See ComarkPlugin for available plugins.
<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:
<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
<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
<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!
::componentsManifest
For lazy-loading components on demand:
<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.
<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:
const config = {
compilerOptions: {
experimental: {
async: true,
},
},
}
export default config<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.<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
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
<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
| Prop | Type | Default | Description |
|---|---|---|---|
tree | ComarkTree | — | Required. The parsed tree returned by parse() |
components | Record<string, Component> | {} | Custom Svelte component mappings |
componentsManifest | ComponentManifest | undefined | Dynamic component resolver for lazy-loaded components |
streaming | boolean | false | Enable streaming mode |
caret | boolean | { class: string } | false | Append a blinking caret to the last text node |
data | Record<string, unknown> | undefined | Runtime values referenced from markdown via :prop="data.path" |
class | string | '' | CSS class for wrapper element |
componentsManifest
For lazy-loading components on demand:
<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:
| Markdown | Prop 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 content →
childrensnippet - Named snippets → named snippet prop (e.g.,
#footer→footersnippet)
<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>::card{title="My Card"}
Default slot content.
#footer
Footer slot content.
::Overriding HTML Elements
Use the Prose prefix to override how native HTML elements render:
Create an override component
<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
<Comark markdown={content} components={{ ProseH1 }} />Resolution Order
Components are resolved in this order:
Prose{PascalTag}— e.g.,ProseH1forh1{PascalTag}— e.g.,Alertforalert{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:
<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:
<!-- 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
<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} /><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>