Reference
API Reference
Complete prop and method reference for all exported components and classes.
<Root>
Componentediting boolean — Toggles editing mode for all nested editors.onsave? (data: Map<string, Record<string, EditorContent>>) => voidchildren Snippet receiving { state, save, get, editing }<Data>
Component Generickey string — Unique key for this data section in the save map.data T extends EditorData — Your typed data object.overrides? NodeOverrides — Custom render overrides for read mode.children Snippet receiving { text, multiline, rich, image }<Renderer>
Componentdoc ProseMirrorJSON — The document to render.overrides? NodeOverrides — Override any node or mark rendering.EditableState
Class.active Editable | undefined — Currently focused editor..isText boolean — Whether active editor is text..isImage boolean — Whether active editor is image..command() Create a reactive toolbar command object..text(fn) Run a function against the active TipTap Editor.<Each>
Component Generickey string — Unique key for this array in the save map.data T[] — Array of typed data objects.overrides? NodeOverrides — Custom render overrides for read mode.onsave? (data: EditorSaveData<T>[]) => MaybePromise<void> — Per-section save callback.each Snippet receiving (item: T, index: number, { text, multiline, rich, image })<!-- Editable.Each manages arrays of items -->
<Editable.Each key="notes" data={notes} onsave={(items) => {
// items is EditorSaveData<Note>[]
items.forEach((item, i) => {
if (item.title.type === 'text') notes[i].title = item.title.content;
if (item.body.type === 'text') notes[i].body = item.body.content;
});
}}>
{#snippet each(item, index, { text, multiline })}
<div class="rounded-lg border p-4">
<h3>{@render text('title')}</h3>
<div>{@render multiline('body')}</div>
</div>
{/snippet}
</Editable.Each>Standalone Editors
ComponentsImport from editable-kit/editors. These work with or without Root.
PlainText / MultilineText / RichText
value ProseMirrorJSON — Bindable document state.editing boolean — Whether the editor is active.key? string — Registers with Root when provided.overrides? NodeOverrides — Custom render overrides for read mode.options? TextEditorOptions — TipTap editor configuration (placeholder,
extensions, etc.).EditableImage
value ImageState — Bindable image state ({ src, alt }).editing boolean — Whether the editor is active.key? string — Registers with Root when provided.maxWidth? number — Max output width in pixels.maxHeight? number — Max output height in pixels.quality? number — WebP compression quality (0-1).aspect? number — Fixed aspect ratio for cropping.Types
Save Payload
When you call save(), Root
collects all editor content into a Map<string, Record<string, EditorContent>>. Each key in the Map matches a Data component's key prop, and each value is
a record mapping field names to their content.
// SaveResult is a Map<string, Record<string, EditorContent>>
// Each key matches a Data component's key prop
async function handleSave(data: SaveResult) {
for (const [key, fields] of data) {
for (const [name, content] of Object.entries(fields)) {
if (content.type === 'text') {
// content.content is ProseMirrorJSON
await saveText(key, name, content.content);
} else if (content.type === 'image-src') {
// Image unchanged — content.src is the original URL
await saveImageUrl(key, name, content.src, content.alt);
} else if (content.type === 'image-blob') {
// Image was cropped/replaced — content.blob is the new image
const url = await uploadBlob(content.blob);
await saveImageUrl(key, name, url, content.alt);
}
}
}
}EditorContent
A discriminated union with three variants. Use the type field to narrow to the
correct shape.
// EditorContent is a discriminated union:
type EditorContent =
| { type: 'text'; content: ProseMirrorJSON }
| { type: 'image-src'; src: string; alt: string }
| { type: 'image-blob'; blob: Blob; alt: string };
// Use the type field to narrow:
if (content.type === 'text') {
content.content // ProseMirrorJSON
} else if (content.type === 'image-blob') {
content.blob // Blob — upload this
}EditableState in Depth
EditableState is the reactive
class exposed by Root's children snippet. It tracks which editor is focused and provides methods for
building toolbars and controlling editors programmatically. All properties are reactive via Svelte
5 runes.
// Access EditableState from Root's children snippet
<Editable.Root editing={true}>
{#snippet children({ state })}
{#if state}
<!-- Check what's focused -->
{state.isText} <!-- boolean -->
{state.isImage} <!-- boolean -->
<!-- Create reactive toolbar commands -->
{@const bold = state.command('toggleBold',
(e) => e.chain().focus().toggleBold().run())}
<button
class:active={bold.isActive}
disabled={!bold.has}
onclick={bold.run}
aria-label="Bold"
aria-pressed={bold.isActive}>Bold</button>
<!-- Run arbitrary TipTap commands -->
<button onclick={() => state.text(
(editor) => editor.chain().focus().toggleItalic().run()
)}>Italic</button>
<!-- Image controls (when isImage is true) -->
{#if state.isImage}
<button onclick={() => state.replaceImage()}>Replace</button>
<input value={state.getImageAlt()}
oninput={(e) => state.setImageAlt(e.currentTarget.value)} />
{/if}
{/if}
{/snippet}
</Editable.Root>Method Reference
.active Editable | undefined — Currently focused editor instance..isText boolean — Whether active editor is a text editor (reactive)..isImage boolean — Whether active editor is an image editor (reactive)..command(name, fn, attrs?) EditableCommand — Create a reactive toolbar command.
Returns { isActive: boolean, has: boolean, run(): void }..isActive(name, attrs?) boolean — Check if a mark/node is active (reactive)..has(extension) boolean — Check if extension exists in active editor..text(fn) R | undefined — Run function against active TipTap Editor..replaceImage() void — Open file picker for active image editor..setImageSrc(src) void — Set image source URL..getImageAlt() string | undefined — Get current alt text..setImageAlt(alt) void — Set alt text.Per-Section Save
Data and Each components can define their own onsave callback. These fire
with typed data for just that section, making it easy to handle saves without parsing the full Map.
<!-- Per-section onsave on Data components -->
<Editable.Data key="profile" data={profile} onsave={(d) => {
// d is EditorSaveData<typeof profile>
// Type-safe: d.name is { type: 'text', content: ProseMirrorJSON }
if (d.name.type === 'text') profile.name = d.name.content;
if (d.avatar.type === 'image-blob') {
uploadImage(d.avatar.blob).then(url => profile.avatar.src = url);
}
}}>
{#snippet children({ text, image })}
<h2>{@render text('name')}</h2>
{@render image('avatar', { maxWidth: 200, maxHeight: 200, aspect: 1 })}
{/snippet}
</Editable.Data>