Rendering

PreviousNext

Slate React renders your document with normal React components. You provide the components for your product-specific elements, text nodes, leaves, toolbars, menus, and overlays.

Pass stable render props to Editable. Define them at module scope, or create them once with the editor, so React does not receive a fresh renderer on every render.

import { Slate, Editable, useSlateEditor } from '@platejs/slate-react'
 
const ParagraphElement = ({ attributes, children }) => {
  return <p {...attributes}>{children}</p>
}
 
const QuoteElement = ({ attributes, children }) => {
  return <blockquote {...attributes}>{children}</blockquote>
}
 
const LinkElement = ({ attributes, children, element }) => {
  return (
    <a {...attributes} href={element.url}>
      {children}
    </a>
  )
}
 
const renderElement = props => {
  switch (props.element.type) {
    case 'quote':
      return <QuoteElement {...props} />
    case 'link':
      return <LinkElement {...props} />
    default:
      return <ParagraphElement {...props} />
  }
}
 
const MyEditor = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable renderElement={renderElement} />
    </Slate>
  )
}
import { Slate, Editable, useSlateEditor } from '@platejs/slate-react'
 
const ParagraphElement = ({ attributes, children }) => {
  return <p {...attributes}>{children}</p>
}
 
const QuoteElement = ({ attributes, children }) => {
  return <blockquote {...attributes}>{children}</blockquote>
}
 
const LinkElement = ({ attributes, children, element }) => {
  return (
    <a {...attributes} href={element.url}>
      {children}
    </a>
  )
}
 
const renderElement = props => {
  switch (props.element.type) {
    case 'quote':
      return <QuoteElement {...props} />
    case 'link':
      return <LinkElement {...props} />
    default:
      return <ParagraphElement {...props} />
  }
}
 
const MyEditor = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable renderElement={renderElement} />
    </Slate>
  )
}

Always spread props.attributes on the top-level DOM element returned by the custom renderer, and always render props.children. The attributes connect the DOM node to Slate's DOM helpers. The children render text leaves, inline elements, void anchors, and nested editable content.

Leaves

When text-level formatting is rendered, the characters are grouped into "leaves" of text that each contain the same formatting (marks) applied to them.

To customize leaf rendering, pass renderLeaf:

const renderLeaf = ({ attributes, children, leaf }) => {
  if (leaf.bold) {
    children = <strong>{children}</strong>
  }
  if (leaf.italic) {
    children = <em>{children}</em>
  }
 
  return <span {...attributes}>{children}</span>
}
const renderLeaf = ({ attributes, children, leaf }) => {
  if (leaf.bold) {
    children = <strong>{children}</strong>
  }
  if (leaf.italic) {
    children = <em>{children}</em>
  }
 
  return <span {...attributes}>{children}</span>
}

Keep spreading props.attributes and rendering props.children.

When decorations split a single text node, the renderLeaf function will receive an additional leafPosition property. This object contains the start and end offsets of the leaf within the original text node, along with optional isFirst and isLast booleans. This leafPosition property is only added when a text node is actually split by decorations.

One disadvantage of text-level formatting is that you cannot guarantee that any given format is "contiguous"—meaning that it stays as a single leaf. This limitation with respect to leaves is similar to the DOM, where this is invalid:

<em>t<strong>e</em>x</strong>t
<em>t<strong>e</em>x</strong>t

Because the elements in the above example do not properly close themselves they are invalid. Instead, you would write the above HTML as follows:

<em>t</em><strong><em>e</em>x</strong>t
<em>t</em><strong><em>e</em>x</strong>t

If you happened to add another overlapping section of <strike> to that text, you might have to rearrange the closing tags yet again. Rendering leaves in Slate is similar—you can't guarantee that even though a word has one type of formatting applied to it that that leaf will be contiguous, because it depends on how it overlaps with other formatting.

Use text-level formatting and element-level formatting for different jobs:

  • Text properties are for non-contiguous, character-level formatting.
  • Element properties are for contiguous, semantic elements in the document.

Texts

While renderLeaf allows you to customize the rendering of individual leaves based on their formatting (marks and decorations), sometimes you need to customize the rendering for an entire text node, regardless of how decorations might split it into multiple leaves.

This is where renderText comes in. It lets you render a component that wraps all the leaves generated for a single Text node.

const TextNode = ({ attributes, children, text }) => {
  return (
    <span {...attributes} className="custom-text">
      {children}
      {/* Render anything you want here */}
    </span>
  )
}
 
<Editable renderText={TextNode} />
const TextNode = ({ attributes, children, text }) => {
  return (
    <span {...attributes} className="custom-text">
      {children}
      {/* Render anything you want here */}
    </span>
  )
}
 
<Editable renderText={TextNode} />

When to use renderLeaf vs renderText:

  • renderLeaf: Use this when you need to apply styles or rendering logic based on the specific properties of each individual leaf (e.g., applying bold style if leaf.bold is true, or highlighting based on a decoration). This function might be called multiple times for a single text node if decorations split it. You can use the optional leafPosition prop (available when a text node is split) to conditionally render something based on the position of the leaf within the text node.

  • renderText: Use this when you need to render something exactly once for a given text node, regardless of how many leaves it's split into. It's ideal for wrapping the entire text node's content or adding elements associated with the text node as a whole without worrying about duplication caused by decorations.

You can use both renderText and renderLeaf together. renderLeaf renders the individual marks and decorations within a text node (leaves), and renderText renders the container of those leaves.

Decorations

Decorations are another type of text-level formatting. They are similar to regular old custom properties, except each one applies to a Range of the document instead of being associated with a given text node.

However, decorations are computed at render-time based on the content itself. This is helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting.

Decorations are different from Marks in that they are not stored on editor state.

Toolbars, Menus, Overlays, and more!

In addition to controlling the rendering of nodes inside Slate, you can retrieve the editor from other components with useEditor().

Use editor-level hooks for shell UI such as toolbars. Keep broad editor subscriptions out of rendered document nodes.

A common use case for this is rendering a toolbar with formatting buttons that are highlighted based on the current selection:

const MyEditor = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Toolbar />
      <Editable />
    </Slate>
  )
}
 
const Toolbar = () => {
  const isBold = useEditorState(state => {
    return state.marks.get()?.bold === true
  })
 
  return (
    <div>
      <Button active={isBold}>B</Button>
      <Button>I</Button>
    </div>
  )
}
const MyEditor = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Toolbar />
      <Editable />
    </Slate>
  )
}
 
const Toolbar = () => {
  const isBold = useEditorState(state => {
    return state.marks.get()?.bold === true
  })
 
  return (
    <div>
      <Button active={isBold}>B</Button>
      <Button>I</Button>
    </div>
  )
}

Because the toolbar draws editor-level state, useEditorState is the right level of subscription. It runs the selector inside editor.read and re-renders only when the selected result changes. Element renderers should prefer target-scoped hooks such as useElementSelected(), useElementSelected({ mode: 'collapsed' }), useNodeSelector, useTextSelector, and useDecorationSelector.

Editor Styling

Custom styles can be applied to the editor itself by using the style prop on the <Editable> component.

const MyEditor = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable style={{ minHeight: '200px', backgroundColor: 'lime' }} />
    </Slate>
  )
}
const MyEditor = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable style={{ minHeight: '200px', backgroundColor: 'lime' }} />
    </Slate>
  )
}

It is also possible to apply custom styles with a stylesheet and className. However, Slate uses inline styles to provide some default styles for the editor. Because inline styles take precedence over stylesheets, styles you provide using stylesheets will not override the default styles. If you are trying to use a stylesheet and your rules are not taking effect, do one of the following:

  • Provide your styles using the style prop instead of a stylesheet, which overrides the default inline styles.
  • Pass the disableDefaultStyles prop to the <Editable> component.
  • Use !important in your stylesheet declarations to make them override the inline styles.

Performance

See Improving Performance for ways to improve the rendering performance of the editor.