diff --git a/AGENTS.md b/AGENTS.md
index 35d0310b..6dd6cfdd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -239,6 +239,7 @@ packages/comark-svelte/
│ │ ├── Comark.svelte # High-level markdown → render ($state + $effect)
│ │ ├── ComarkRenderer.svelte # Low-level AST → render component
│ │ ├── ComarkNode.svelte # Recursive AST node renderer
+│ │ ├── ComarkComponent.svelte # Custom component renderer with named snippets
│ │ └── Resolve.svelte # Stable promise resolver for lazy components
│ ├── async/
│ │ ├── index.ts # Async export (@comark/svelte/async)
diff --git a/packages/comark-svelte/src/components/ComarkComponent.svelte b/packages/comark-svelte/src/components/ComarkComponent.svelte
new file mode 100644
index 00000000..d593b0e6
--- /dev/null
+++ b/packages/comark-svelte/src/components/ComarkComponent.svelte
@@ -0,0 +1,78 @@
+
+
+{#if slotIndex < namedSlots.length}
+ {@const slot = namedSlots[slotIndex]}
+ {#snippet namedSlot()}
+ {#each slot.children as child, i (i)}
+
+ {/each}
+ {/snippet}
+
+
+ {@render children?.()}
+
+{:else if Component}
+
+ {@render children?.()}
+
+{:else if componentPromise}
+
+ {@render children?.()}
+
+{/if}
diff --git a/packages/comark-svelte/src/components/ComarkNode.svelte b/packages/comark-svelte/src/components/ComarkNode.svelte
index e9265083..13b1fa1d 100644
--- a/packages/comark-svelte/src/components/ComarkNode.svelte
+++ b/packages/comark-svelte/src/components/ComarkNode.svelte
@@ -68,9 +68,9 @@ naturally appears inline after the deepest trailing text node.
{#snippet renderChildren()}
@@ -270,12 +250,36 @@ naturally appears inline after the deepest trailing text node.
class={caretClass || undefined}
style={CARET_STYLE}>{CARET_TEXT}{/if}
+{:else if Component && namedSlots.length > 0}
+
+ {@render renderChildren()}
+
{:else if Component}
-
+
{@render renderChildren()}
+{:else if componentPromise && namedSlots.length > 0}
+
+ {@render renderChildren()}
+
{:else if componentPromise}
-
+
{@render renderChildren()}
{:else if isVoid}
diff --git a/packages/comark-svelte/test/ComarkNode.svelte.test.ts b/packages/comark-svelte/test/ComarkNode.svelte.test.ts
index aee6a3ff..3671c8bb 100644
--- a/packages/comark-svelte/test/ComarkNode.svelte.test.ts
+++ b/packages/comark-svelte/test/ComarkNode.svelte.test.ts
@@ -4,6 +4,7 @@ import { parse } from 'comark'
import ComarkRenderer from '../src/components/ComarkRenderer.svelte'
import ComarkNode from '../src/components/ComarkNode.svelte'
import Alert from './test-components/Alert.svelte'
+import CardWithHeaderFooter from './test-components/CardWithHeaderFooter.svelte'
import CardWithFooter from './test-components/CardWithFooter.svelte'
import ProseH1 from './test-components/ProseH1.svelte'
@@ -192,6 +193,34 @@ Footer slot content.
expect(screen.container.querySelector('template[name="footer"]')).toBeNull()
})
+ it('passes multiple named slots as Svelte snippet props', async () => {
+ const tree = await parse(`::card{title="My Card"}
+Default slot content.
+
+#header
+Header slot content.
+
+#footer
+Footer slot content.
+::`)
+ const screen = render(ComarkRenderer, {
+ tree,
+ components: { card: CardWithHeaderFooter },
+ })
+ const header = screen.container.querySelector('header')!
+ const main = screen.container.querySelector('main')!
+ const footer = screen.container.querySelector('footer')!
+
+ expect(header).not.toBeNull()
+ expect(main).not.toBeNull()
+ expect(footer).not.toBeNull()
+ await expect.element(header).toHaveTextContent('Header slot content.')
+ await expect.element(main).toHaveTextContent('Default slot content.')
+ await expect.element(footer).toHaveTextContent('Footer slot content.')
+ expect(screen.container.querySelector('template[name="header"]')).toBeNull()
+ expect(screen.container.querySelector('template[name="footer"]')).toBeNull()
+ })
+
it('resolves custom components from componentsManifest', async () => {
const tree = await parse('::alert{type="warning"}\nLazy content\n::')
const screen = render(ComarkRenderer, {
diff --git a/packages/comark-svelte/test/ComarkNode.test.ts b/packages/comark-svelte/test/ComarkNode.test.ts
index ffea8f3e..8915de56 100644
--- a/packages/comark-svelte/test/ComarkNode.test.ts
+++ b/packages/comark-svelte/test/ComarkNode.test.ts
@@ -6,6 +6,7 @@ import ComarkNode from '../src/components/ComarkNode.svelte'
import ComarkAsync from '../src/async/ComarkAsync.svelte'
import Alert from './test-components/Alert.svelte'
import Card from './test-components/Card.svelte'
+import CardWithHeaderFooter from './test-components/CardWithHeaderFooter.svelte'
import CardWithFooter from './test-components/CardWithFooter.svelte'
import ProseH1 from './test-components/ProseH1.svelte'
@@ -265,6 +266,26 @@ Footer slot content.
expect(output).not.toContain(' {
+ const tree = await parse(`::card{title="My Card"}
+Default slot content.
+
+#header
+Header slot content.
+
+#footer
+Footer slot content.
+::`)
+ const { body } = render(ComarkRenderer, {
+ props: { tree, components: { card: CardWithHeaderFooter } },
+ })
+ const output = html(body)
+ expect(output).toContain('')
+ expect(output).toContain('Default slot content.
')
+ expect(output).toContain('')
+ expect(output).not.toContain(' {
const tree = await parse('::alert{type="warning"}\nLazy content\n::')
const { body } = render(ComarkRenderer, {
diff --git a/packages/comark-svelte/test/test-components/CardWithHeaderFooter.svelte b/packages/comark-svelte/test/test-components/CardWithHeaderFooter.svelte
new file mode 100644
index 00000000..d92565b6
--- /dev/null
+++ b/packages/comark-svelte/test/test-components/CardWithHeaderFooter.svelte
@@ -0,0 +1,22 @@
+
+
+
+ {title}
+
+ {@render children?.()}
+
+