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>