Defining Custom Elements

PreviousNext

Render custom block elements while preserving Slate's editable DOM contract.

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.

Starting Point

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.