Guide

Low-Level Editors

Use editor components directly when you need full control over data binding, styling, and layout.

When to Use

The high-level Data component works great for flat records. Use low-level editors when:

  • Your data is nested or doesn't fit a flat record shape
  • You need precise control over where editors appear in your markup
  • You want to manage save timing yourself

Standalone Usage

Import editors from editable-kit/editors and bind values directly. No Root or Data wrapper needed. Values sync back on blur.

Standalone Editors

Edit this title directly

These editors work without Editable.Root — values bind directly to your variables.

<script lang="ts">
  import { PlainText, RichText } from 'editable-kit/editors';
  import type { ProseMirrorJSON } from 'editable-kit';

  let editing = $state(false);

  let title: ProseMirrorJSON = $state({
    type: 'doc',
    content: [{ type: 'text', text: 'My Title' }]
  });

  let body: ProseMirrorJSON = $state({
    type: 'doc',
    content: [
      { type: 'paragraph', content: [{ type: 'text', text: 'Body text here.' }] }
    ]
  });
</script>

<button onclick={() => editing = !editing}>
  {editing ? 'Done' : 'Edit'}
</button>

<h1><PlainText bind:value={title} {editing} /></h1>
<div><RichText bind:value={body} {editing} /></div>

With Root

Add a key prop to register editors with Editable.Root. This enables coordinated saves and the floating toolbar — same as Data, but without the snippet pattern.

With Root

Coordinated editing

These editors register with Root via the key prop. The toolbar and save work automatically.

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

  let editing = $state(false);
  let title: ProseMirrorJSON = $state({} as ProseMirrorJSON); // your data here
  let body: ProseMirrorJSON = $state({} as ProseMirrorJSON); // your data here

  async function handleSave(allData) {
    // Standalone editors register with their key
    const title = allData.get('title'); // { title: EditorContent }
    const body = allData.get('body');   // { body: EditorContent }
  }
</script>

<Editable.Root {editing} onsave={handleSave}>
  {#snippet children({ state, save })}
    <Toolbar {state} />
    <h1><PlainText bind:value={title} {editing} key="title" /></h1>
    <div><RichText bind:value={body} {editing} key="body" /></div>
    <button onclick={save}>Save</button>
  {/snippet}
</Editable.Root>

Comparison

Both APIs use the same underlying editor components. Choose based on your data shape.

Data (High-Level)

  • Type-safe field selectors
  • Automatic save coordination
  • Best for flat records

Editors (Low-Level)

  • Direct value binding
  • Any data shape
  • Full layout control

Custom Layouts

Low-level editors can be placed anywhere in your markup. Wrap them in a single Editable.Root and each editor registers independently via its key prop — no matter where it sits in the DOM tree.

Two-Column Layout

Custom Layout Demo

Main Content

The main content area. Both this and the sidebar are separate RichText editors registered with the same Root.

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

  let editing = $state(false);
  let title: ProseMirrorJSON = $state({
    type: 'doc',
    content: [{ type: 'text', text: 'Page Title' }]
  });
  let sidebar: ProseMirrorJSON = $state({
    type: 'doc',
    content: [{ type: 'paragraph',
      content: [{ type: 'text', text: 'Sidebar content' }] }]
  });
  let body: ProseMirrorJSON = $state({
    type: 'doc',
    content: [{ type: 'paragraph',
      content: [{ type: 'text', text: 'Main content' }] }]
  });
  let hero: ImageState = $state({ src: '/hero.jpg', alt: 'Hero image' });
</script>

<Editable.Root {editing}>
  {#snippet children({ state, save })}
    <!-- Editors can go anywhere in the layout -->
    <header>
      <h1><PlainText bind:value={title} {editing} key="title" /></h1>
      <EditableImage bind:value={hero} {editing} key="hero"
        maxWidth={1200} aspect={21/9} />
    </header>

    <div class="grid grid-cols-3 gap-8">
      <aside>
        <RichText bind:value={sidebar} {editing} key="sidebar" />
      </aside>
      <main class="col-span-2">
        <RichText bind:value={body} {editing} key="body" />
      </main>
    </div>
  {/snippet}
</Editable.Root>

As Form Inputs

Without an Editable.Root, editors work as standalone rich form fields. Set editing={true} to keep them always editable and use placeholder for empty-state hints.

Form Inputs

<script lang="ts">
  import { PlainText, MultilineText } from 'editable-kit/editors';
  import type { ProseMirrorJSON } from 'editable-kit';

  let name: ProseMirrorJSON = $state({
    type: 'doc',
    content: [{ type: 'text', text: '' }]
  });
  let bio: ProseMirrorJSON = $state({
    type: 'doc',
    content: [{ type: 'paragraph',
      content: [{ type: 'text', text: '' }] }]
  });

  // No Editable.Root needed — editors manage their own state
  // Values update on blur
</script>

<form>
  <label>
    Name
    <PlainText bind:value={name} editing={true} placeholder="Your name" />
  </label>
  <label>
    Bio
    <MultilineText bind:value={bio} editing={true}
      placeholder="Tell us about yourself" />
  </label>
</form>