Defining Custom Elements

PreviousNext

The smallest editor can render a paragraph without a custom renderer, but real editors usually need block types such as paragraphs, quotes, code blocks, list items, cards, and embeds.

Start from the editor from the previous walkthrough:

const initialValue = [
  {
    type: 'paragraph',
    children: [{ text: 'A line of text in a paragraph.' }],
  },
]
 
const App = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable
        onKeyDown={(event) => {
          if (event.key === '&') {
            event.preventDefault()
            editor.update((tx) => {
              tx.text.insert('and')
            })
          }
        }}
      />
    </Slate>
  )
}
const initialValue = [
  {
    type: 'paragraph',
    children: [{ text: 'A line of text in a paragraph.' }],
  },
]
 
const App = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable
        onKeyDown={(event) => {
          if (event.key === '&') {
            event.preventDefault()
            editor.update((tx) => {
              tx.text.insert('and')
            })
          }
        }}
      />
    </Slate>
  )
}

Render Elements

Element renderers are normal React functions. Always spread attributes on the top-level DOM element and render children.

const CodeElement = ({ attributes, children }) => {
  return (
    <pre {...attributes}>
      <code>{children}</code>
    </pre>
  )
}
 
const DefaultElement = ({ attributes, children }) => {
  return <p {...attributes}>{children}</p>
}
 
const renderElement = (props) => {
  switch (props.element.type) {
    case 'code':
      return <CodeElement {...props} />
    default:
      return <DefaultElement {...props} />
  }
}
const CodeElement = ({ attributes, children }) => {
  return (
    <pre {...attributes}>
      <code>{children}</code>
    </pre>
  )
}
 
const DefaultElement = ({ attributes, children }) => {
  return <p {...attributes}>{children}</p>
}
 
const renderElement = (props) => {
  switch (props.element.type) {
    case 'code':
      return <CodeElement {...props} />
    default:
      return <DefaultElement {...props} />
  }
}

Pass the renderer to Editable:

const App = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === '&') {
            event.preventDefault()
            editor.update((tx) => {
              tx.text.insert('and')
            })
          }
        }}
      />
    </Slate>
  )
}
const App = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === '&') {
            event.preventDefault()
            editor.update((tx) => {
              tx.text.insert('and')
            })
          }
        }}
      />
    </Slate>
  )
}

Keep renderer functions stable by defining them at module scope or memoizing them once.

Toggle A Block Type

Use editor.update(...) and tx.nodes.set(...) to change the selected block.

import { ElementApi } from '@platejs/slate'
 
const App = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === '`' && event.ctrlKey) {
            event.preventDefault()
 
            editor.update((tx) => {
              tx.nodes.set(
                { type: 'code' },
                {
                  match: (node) =>
                    ElementApi.isElement(node) && tx.schema.isBlock(node),
                }
              )
            })
          }
        }}
      />
    </Slate>
  )
}
import { ElementApi } from '@platejs/slate'
 
const App = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === '`' && event.ctrlKey) {
            event.preventDefault()
 
            editor.update((tx) => {
              tx.nodes.set(
                { type: 'code' },
                {
                  match: (node) =>
                    ElementApi.isElement(node) && tx.schema.isBlock(node),
                }
              )
            })
          }
        }}
      />
    </Slate>
  )
}

To make the shortcut toggle, read first, then write:

import { ElementApi } from '@platejs/slate'
 
const App = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === '`' && event.ctrlKey) {
            event.preventDefault()
 
            const match = editor.read((state) =>
              state.nodes.find({
                match: (node) =>
                  ElementApi.isElement(node) && node.type === 'code',
              })
            )
 
            editor.update((tx) => {
              tx.nodes.set(
                { type: match ? 'paragraph' : 'code' },
                {
                  match: (node) =>
                    ElementApi.isElement(node) && tx.schema.isBlock(node),
                }
              )
            })
          }
        }}
      />
    </Slate>
  )
}
import { ElementApi } from '@platejs/slate'
 
const App = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === '`' && event.ctrlKey) {
            event.preventDefault()
 
            const match = editor.read((state) =>
              state.nodes.find({
                match: (node) =>
                  ElementApi.isElement(node) && node.type === 'code',
              })
            )
 
            editor.update((tx) => {
              tx.nodes.set(
                { type: match ? 'paragraph' : 'code' },
                {
                  match: (node) =>
                    ElementApi.isElement(node) && tx.schema.isBlock(node),
                }
              )
            })
          }
        }}
      />
    </Slate>
  )
}

The renderer controls how a node looks. The transaction controls the document shape.