From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
UNPUBLISHED
  • Overview
  • Why This Fork
  • Examples
Walkthroughs
  • Installing Plite
  • Adding Event Handlers
  • Defining Custom Elements
  • Applying Custom Formatting
  • Executing Commands
  • Saving to a Database
  • Canonical Change Substrate
  • Improving Performance
Concepts
  • Interfaces
  • Nodes
  • Locations
  • Transforms
  • Document Changes
  • Commands
  • Editor
  • Extensions
  • Rendering
  • Serializing
  • Normalizing
  • Using TypeScript
  • Roots
  • Document State
  • Editing Behavior
  • Selection And DOM
  • Clipboard And Paste
  • Projection And Overlays
  • Schema
API
  • Anchor API
  • Location API
  • Path API
  • PointEntry API
  • Point API
  • Range API
  • Selection API
  • Location Types APIs
  • Span API
  • Editor
  • Element API
  • NodeEntry API
  • Node API
  • Node Types APIs
  • Text API
  • Debug Value Scrubbing
  • Transforms API
Libraries
  • Plite DOM
  • History Editor API
  • History Extension Setup
  • History
  • Plite History
  • Plite Hyperscript
  • Plite Layout
  • Annotations
  • DOM Coverage Boundaries
  • Editable Component
  • Plite React Event Handling
  • Virtualized Rendering
  • Plite React Hooks
  • React Editor Setup
  • React Editor
  • Plite React
  • Plite Component
  • Plite Yjs
  • Plite
General
  • Migration
  • Contributing
  • Docs Proof Map
  • FAQ
  • Resources

Roots

PreviousNext

Use extra roots for headers, footers, synced blocks, captions, and other editable regions.

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.

Value Shape

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

Using TypeScriptDocument State

On This Page

Value ShapeReading And Writing RootsRendering RootsContent Roots
Build your editor
Production-ready AI template and reusable components.
Get all-access
=
createEditor
({
initialValue: [{ type: "paragraph", children: [{ text: "Body" }] }],
});

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.

Reading And Writing Roots

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.

Rendering 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

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.