Roots

PreviousNext

Slate 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. Use separate editors when the documents should be fully independent.

Value Shape

The shortest editor value is still an array of blocks. Slate treats it as the primary document.

const editor = createEditor({
  initialValue: [{ type: 'paragraph', children: [{ text: 'Body' }] }],
})
const editor = 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 Slate block array. Rootless operations, 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 State value instead of saving only the provider root.

Reading And Writing Roots

Read the primary document with state.value.root(). Read an extra root by key.

const body = editor.read((state) => state.value.root())
const header = editor.read((state) => state.value.root('header'))
const body = editor.read((state) => state.value.root())
const header = editor.read((state) => state.value.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.

<Slate editor={editor}>
  <Editable aria-label="Header" root="header" />
  <Editable aria-label="Body" />
  <Editable aria-label="Footer" root="footer" />
</Slate>
<Slate editor={editor}>
  <Editable aria-label="Header" root="header" />
  <Editable aria-label="Body" />
  <Editable aria-label="Footer" root="footer" />
</Slate>

Use useSlateRootChrome(root) on non-editable wrappers that should participate in mouse selection and focus restoration for that root.

const chrome = useSlateRootChrome('header')
 
return (
  <section {...chrome.props}>
    <Editable root="header" />
  </section>
)
const chrome = useSlateRootChrome('header')
 
return (
  <section {...chrome.props}>
    <Editable root="header" />
  </section>
)

Use useSlateRootState(root, selector) for UI that reads one root without subscribing to every editor change. Use useSlateRootEditor(root) for commands that must run against a specific root.

Content Roots

Content roots attach an editable child root to an element in another root. The element keeps an empty text child as its model anchor. element.childRoots[slot] stores the root key, and the editable content lives in state.value.root(rootKey).

const syncedBlocks = defineEditorExtension({
  name: 'synced-blocks',
  elements: [
    {
      type: 'synced-block',
      contentRoot: { slot: 'body' },
    },
  ],
})
const syncedBlocks = defineEditorExtension({
  name: 'synced-blocks',
  elements: [
    {
      type: 'synced-block',
      contentRoot: { slot: 'body' },
    },
  ],
})

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: '' }],
  })
})

contentRoot declares the schema slot. childRoots[slot] is the persisted ownership link that survives save/load and collaboration. Copy/paste adapters must decide whether to reuse an existing child root, clone it into a new root, or serialize and create the child root in the paste target.

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.