Overview

editable-kit

Inline editing for Svelte 5. Plain text, rich text via TipTap, and images with cropping — without compromising performance, safety, or prerendering.

Why editable-kit?

Most content editing libraries assume your page exists to be an editor. They load heavy JavaScript up front, require a client-side runtime, and render content by injecting raw HTML into the DOM. That tradeoff makes sense for a full CMS, but not when you just want a heading, a paragraph, or an image to be editable in place.

editable-kit takes a different approach. Your content is static by default. The editor only loads when a user actually starts editing, and disappears when they stop. The result is a page that is fast, prerenderable, and safe — with inline editing that feels native.

Zero JS by default

TipTap, the image cropper, and the toolbar are all lazily imported. Until a user toggles editing, none of that code is loaded. Your visitors get a static page with no editor overhead.

Fully prerenderable

The included Renderer component converts ProseMirror JSON to real Svelte components at build time. Pages prerender to static HTML with no client-side rendering step required.

No {@html} tag

The Renderer walks ProseMirror JSON and outputs native Svelte elements — no {@html} injection. Content is rendered through the framework, not around it, eliminating an entire class of XSS vulnerabilities.

Try it out

Click "Try editing" above

This is a live editor. Toggle editing to change this heading and paragraph, then save your changes.

Core Concepts

Two components form the editing context. Root manages global editing state. Data wraps a typed data object and exposes editor snippets.

<script lang="ts">
  import * as Editable from 'editable-kit';
  import type { ProseMirrorJSON, ImageState } from 'editable-kit';

  type PageData = {
    title: ProseMirrorJSON;
    body: ProseMirrorJSON;
    image: ImageState;
  };

  let data: PageData = $state({} as PageData); // your data here
  let editing = $state(false);
</script>

<Editable.Root {editing} onsave={handleSave}>
  {#snippet children({ state, save, editing })}
    <Editable.Data key="page" {data}>
      {#snippet children({ text, rich, image })}
        <h1>{@render text('title')}</h1>
        <div>{@render rich('body')}</div>
        {@render image('image', { maxWidth: 800, maxHeight: 500, quality: 0.85 })}
      {/snippet}
    </Editable.Data>
  {/snippet}
</Editable.Root>

Root

Provides EditableContext. Toggle editing mode, handle saves.

Data

Generic <T> wrapper. Exposes text, multiline, rich, image snippets.

Renderer

Renders ProseMirrorJSON to HTML without loading TipTap.