Display
Renderer
Render ProseMirrorJSON to HTML without loading TipTap. Use for read-only views, SSR, or email templates.
Basic Usage
The Renderer takes a ProseMirrorJSON document and outputs HTML. No TipTap is loaded — this is pure Svelte rendering.
Rendered Output
Hello from the Renderer
This content is rendered without loading TipTap. The Renderer component converts ProseMirrorJSON directly to HTML using Svelte components.
It supports bold, italic, underline, strikethrough, and links.
Zero TipTap dependency at runtime
Perfect for SSR and static pages
Fully customizable via snippet overrides
<script lang="ts">
import { Renderer } from 'editable-kit';
</script>
<article class="prose">
<Renderer doc={post.body} />
</article>Custom Overrides
Override how any node or mark is rendered using Svelte 5 snippets. The example below adds auto-generated id anchors to headings and custom link styling.
With Overrides
Custom Rendered Heading
This paragraph has a custom styled link rendered with overrides. The heading above has an auto-generated id anchor.
{#snippet heading(node, children)}
<h2 id={node.content?.[0]?.text?.toLowerCase().replaceAll(' ', '-')}>
{@render children()}
</h2>
{/snippet}
{#snippet link(mark, children)}
<a href={mark.attrs.href} class="text-blue-600 underline" target="_blank">
{@render children()}
</a>
{/snippet}
<Renderer doc={post.body} overrides={{ nodes: { heading }, marks: { link } }} />Available Overrides
Nodes
paragraph, heading, blockquote, bulletList, orderedList, listItem, image, hardBreak
Marks
bold, italic, underline, strike, link
Overrides with Editable.Data
The Editable.Data component accepts the same overrides prop. When not editing,
the component uses the Renderer internally — your overrides apply to both the read-only display and
the loading state while TipTap initializes.
Editable with Overrides
This rich text body uses custom overrides that apply in both read-only and editing modes. Try editing to see the overrides persist. Visit Svelte for more.
<script lang="ts">
import * as Editable from 'editable-kit';
import type { ProseMirrorJSON } from 'editable-kit';
type Post = {
title: ProseMirrorJSON;
body: ProseMirrorJSON;
};
let data: Post = $state({
title: { type: 'doc', content: [{ type: 'text', text: 'My Post Title' }] },
body: {
type: 'doc',
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Rich text body content.' }]
}
]
}
});
let editing = $state(false);
// Define overrides once — they apply to both read-only and editing views
const overrides = {
nodes: { heading },
marks: { link }
};
</script>
<!-- Define snippet overrides -->
{#snippet heading(node, children)}
<h1 class="text-3xl font-bold tracking-tight">
{@render children()}
</h1>
{/snippet}
{#snippet link(mark, children)}
<a href={mark.attrs.href} class="text-blue-600 underline">
{@render children()}
</a>
{/snippet}
<!-- Pass overrides to Editable.Data -->
<Editable.Root {editing}>
{#snippet children({ save })}
<Editable.Data key="post" {data} {overrides} onsave={(d) => data = { ...data, ...d }}>
{#snippet children({ text, rich })}
{@render text('title')}
{@render rich('body')}
{/snippet}
</Editable.Data>
{/snippet}
</Editable.Root>How it works
- When
editingisfalse, each editor field renders through the Renderer with your overrides - When
editingbecomestrue, TipTap loads and replaces the rendered output — overrides are shown during the loading state - Define overrides once and pass them to
Editable.Data— no need to use the Renderer directly
Node Override Signatures
Each node override is a Svelte 5 snippet. Nodes that contain children (paragraphs, headings,
lists, etc.) receive the node data and a children snippet you must render. Leaf nodes like image and hardBreak only receive the
node data.
Pass node overrides via overrides.nodes. Any node without an override falls back to the default HTML element.
<!-- Each node snippet receives the node data and a children snippet -->
<!-- Nodes with children: paragraph, heading, blockquote, bulletList, orderedList, listItem -->
{#snippet paragraph(node: ParagraphNode, children: Snippet)}
<p class="my-custom-class">{@render children()}</p>
{/snippet}
{#snippet heading(node: HeadingNode, children: Snippet)}
<!-- node.attrs.level is 1 | 2 | 3 -->
<svelte:element this={`h${node.attrs.level}`}>
{@render children()}
</svelte:element>
{/snippet}
{#snippet blockquote(node: BlockquoteNode, children: Snippet)}
<blockquote class="border-l-4 pl-4">{@render children()}</blockquote>
{/snippet}
{#snippet bulletList(node: BulletListNode, children: Snippet)}
<ul class="list-disc pl-6">{@render children()}</ul>
{/snippet}
{#snippet orderedList(node: OrderedListNode, children: Snippet)}
<!-- node.attrs?.start for custom start number -->
<ol start={node.attrs?.start} class="list-decimal pl-6">{@render children()}</ol>
{/snippet}
{#snippet listItem(node: ListItemNode, children: Snippet)}
<li>{@render children()}</li>
{/snippet}
<!-- Leaf nodes (no children snippet) -->
{#snippet image(node: ImageNode)}
<img src={node.attrs.src} alt={node.attrs.alt ?? ''} title={node.attrs.title} />
{/snippet}
{#snippet hardBreak(node: HardBreakNode)}
<br />
{/snippet}Node Types Reference
paragraph Block text container. Default: <p>heading Has attrs.level (1-3). Default: <h1>-<h3>blockquote Default: <blockquote>bulletList Default: <ul>orderedList Has optional attrs.start. Default: <ol>listItem Default: <li>image Leaf node. Has attrs.src, alt, title. Default: <img>hardBreak Leaf node. Default: <br>Mark Override Signatures
Mark overrides customize how inline formatting is rendered. Each receives the mark data and a children snippet for the wrapped
content. Marks can be nested (e.g. bold inside a link), so always render the children snippet.
Pass mark overrides via overrides.marks.
<!-- Each mark snippet receives the mark data and a children snippet -->
{#snippet bold(mark: BoldMark, children: Snippet)}
<strong class="font-bold">{@render children()}</strong>
{/snippet}
{#snippet italic(mark: ItalicMark, children: Snippet)}
<em>{@render children()}</em>
{/snippet}
{#snippet underline(mark: UnderlineMark, children: Snippet)}
<u>{@render children()}</u>
{/snippet}
{#snippet strike(mark: StrikeMark, children: Snippet)}
<s>{@render children()}</s>
{/snippet}
{#snippet link(mark: LinkMark, children: Snippet)}
<!-- mark.attrs: { href, target?, rel?, class? } -->
<a href={mark.attrs.href} target={mark.attrs.target} rel={mark.attrs.rel}>
{@render children()}
</a>
{/snippet}Mark Types Reference
bold No attrs. Default: <strong>italic No attrs. Default: <em>underline No attrs. Default: <u>strike No attrs. Default: <s>link Has attrs.href, target?, rel?, class?. Default: <a>