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

Editable Post Title

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 editing is false, each editor field renders through the Renderer with your overrides
  • When editing becomes true, 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>