Plite stores the primary document as a normal block array. Extra roots let one editor own headers, footers, synced blocks, captions, side panels, and other editable regions without creating separate editor instances. Use roots when the content should share editor state, history, collaboration, schema, and commands.
The shortest editor value is still an array of blocks. Plite treats it as the primary document.
const editor = createEditor({
initialValue: [{ type: "paragraph", children: [{ text: "Body" }] }],
});const editor
Pass initialValue.children plus initialValue.roots when the editor owns
extra roots.
const editor = createEditor({
initialValue: {
children: [{ type: "paragraph", children: [{ text: "Body" }] }],
roots: {
header: [{ type: "paragraph", children: [{ text: "Draft" }] }],
footer: [{ type: "paragraph", children: [{ text: "Internal" }] }],
},
},
});const editor = createEditor({
initialValue: {
children: [{ type: "paragraph", children: [{ text: "Body" }] }],
roots: {
header: [{ type: "paragraph", children: [{ text: "Draft" }] }],
footer: [{ type: "paragraph", children: [{ text: "Internal" }] }],
},
},
});Each root is a normal Plite block array. Rootless transforms, points, and selections resolve against the current editor or view root. A root-bound view resolves rootless locations against its own root.
Persist roots through the full Document Meta value
instead of saving only one mounted root. When an application publishes a
complete external snapshot, use only tx.value.replace({ children, roots, meta, selection }). It fits the combined document atomically, removes omitted
roots, resets omitted persisted meta, and clears selection when selection is
omitted. An invalid combined payload returns false and publishes nothing.
Use tx.roots.* only for a targeted named-root change.
Read the primary document with editor.read.children(). Read an extra root by key.
const body = editor.read.children();
const header = editor.read.root("header");const body = editor.read.children();
const header = editor.read.root("header");Create, replace, or delete extra roots inside editor.update.
editor.update((tx) => {
tx.roots.create("aside:1", [
{ type: "paragraph", children: [{ text: "Aside" }] },
]);
});editor.update((tx) => {
tx.roots.create("aside:1", [
{ type: "paragraph", children: [{ text: "Aside" }] },
]);
});Mutate the primary document with normal node and text transforms instead of
tx.roots.
Pass root to Editable when one React tree renders a named root.
<Plite editor={editor}>
<Editable aria-label="Header" root="header" />
<Editable aria-label="Body" />
<Editable aria-label="Footer" root="footer" />
</Plite><Plite editor={editor}>
<Editable aria-label="Header" root="header" />
<Editable aria-label="Body" />
<Editable aria-label="Footer" root="footer" />
</Plite>Use usePliteRootChrome(root) on non-editable wrappers that should participate
in mouse selection and focus restoration for that root.
const chrome = usePliteRootChrome("header");
return (
<section {...chrome.props}>
<Editable root="header" />
</section>
);const chrome = usePliteRootChrome("header");
return (
<section {...chrome.props}>
<Editable root="header" />
</section>
);Use usePliteRootState(root, selector) for UI that reads one root without
subscribing to every editor change. Use usePliteRootEditor(root) for commands
that must run against a specific root.
Content roots attach a separately addressed editable root to an element in
another root. element.childRoots[slot] stores the root key, and the detached
content lives in editor.read.root(rootKey). Keep ordinary structural content
in the element's children.
import { defineExtension, schema } from "@platejs/plite";
const syncedBlocks = defineExtension("synced-blocks", {
schema: {
elements: {
"synced-block": {
content: schema.content.text({ default: "text", min: 1 }),
contentRoots: {
body: {
content: schema.content.group("block", {
default: { type: "paragraph" },
min: 1,
}),
ownership: "shared",
},
},
},
},
},
});import { defineExtension, schema } from "@platejs/plite";
const syncedBlocks = defineExtension("synced-blocks", {
schema: {
elements: {
"synced-block": {
content: schema.content.text({ default: "text", min: 1 }),
contentRoots: {
body: {
content: schema.content.group("block", {
default: { type: "paragraph" },
min: 1,
}),
ownership: "shared",
},
},
},
},
},
});Install this feature contribution beside the editor's complete schema.
Create a persistent content root by storing the root key on the owning element and creating the named root in the same transaction.
const bodyRoot = `synced-block:${blockId}:body`;
editor.update((tx) => {
tx.roots.create(bodyRoot, [
{ type: "paragraph", children: [{ text: "Synced body" }] },
]);
tx.nodes.insert({
type: "synced-block",
childRoots: { body: bodyRoot },
children: [{ text: "" }],
});
});const bodyRoot = `synced-block:${blockId}:body`;
editor.update((tx) => {
tx.roots.create(bodyRoot, [
{ type: "paragraph", children: [{ text: "Synced body" }] },
]);
tx.nodes.insert({
type: "synced-block",
childRoots: { body: bodyRoot },
children: [{ text: "" }],
});
});Each contentRoots entry declares a slot, child-root grammar, and ownership.
childRoots[slot] is the persisted ownership link that survives save/load and
collaboration.
Use ownership: "shared" when several owners intentionally project the same
root, as with synced blocks. Shared aliases stay attached to one root; removing
even the last owner preserves that root. Use ownership: "exclusive" for
a separately mounted document that belongs to exactly one element, such as an
element-owned annotation surface. Moving an owner preserves its root key.
Removing, retargeting, or changing the type of an exclusive owner removes the
root only when it becomes orphaned.
Duplicate and copy/paste clone detached roots. Keys remap deterministically to
:copy, then :copy:2, while shared aliases in the copied group continue to
share one remapped root. Owner-first and root-first creation are both valid
inside one transaction. ContentSlice carries the transitive detached-root
payload across clipboard and insertion boundaries, including a collapsed
target. Cut captures that payload before deleting the owner and root in one
transaction. Undo and redo restore the owner, root content, and root-qualified
selection together.
Render the child root from renderElement with slots.contentRoot(slot).
const SyncedBlock = ({ attributes, element, slots }) => (
<section {...attributes}>
<header>Synced block</header>
{slots.contentRoot("body", {
ariaLabel: "Synced block body",
placeholder: "Empty synced block",
})}
</section>
);const SyncedBlock = ({ attributes, element, slots }) => (
<section {...attributes}>
<header>Synced block</header>
{slots.contentRoot("body", {
ariaLabel: "Synced block body",
placeholder: "Empty synced block",
})}
</section>
);Use content roots for synced blocks, editable cards, and element-owned editable regions. Use DOM coverage boundaries when the content is in the same root but intentionally not mounted, such as a closed accordion body or inactive tab panel.