Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Overview
    • Why This Fork
    • Examples
    • Installing Plite
    • Adding Event Handlers
    • Defining Custom Elements
    • Applying Custom Formatting
    • Executing Commands
    • Saving to a Database
    • Canonical Change Substrate
    • Improving Performance
    • Interfaces
    • Nodes
    • Locations
    • Transforms
    • Document Changes
    • Commands
    • Editor
    • Plugins
    • Rendering
    • Serializing
    • Normalizing
    • Using TypeScript
    • Roots
    • Document State
    • Editing Behavior
    • Selection And DOM
    • Clipboard And Paste
    • Decorations, annotations, and widgets
    • Schema
    • 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
    • Plite DOM
    • History Editor API
    • History Plugin Setup
    • History
    • Plite History
    • Plite Hyperscript
    • Plite PaginationExperimental
    • Annotations
    • DOM Coverage Boundaries
    • Editable Component
    • External Text Views
    • Plite React Event Handling
    • Virtualized Rendering
    • Plite React Hooks
    • React Editor Setup
    • React Editor
    • Plite React
    • Plite Component
    • Plite Yjs
    • Plite
    • Migration
    • Contributing
    • FAQ
    • Resources

Defining Custom Elements

PreviousNext

Render custom block elements while preserving Plite'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 = useEditor({ initialValue });
 
  return













Adding Event HandlersApplying Custom Formatting

On This Page

Starting pointRender elementsToggle a block type
Build your editor
Production-ready AI template and reusable components.
Get all-access
(
<EditorRoot editor={editor}>
<Editable
onKeyDown={(event) => {
if (event.key === "&") {
event.preventDefault();
editor.update((tx) => {
tx.text.insert("and");
});
}
}}
/>
</EditorRoot>
);
};
const initialValue = [
  {
    type: "paragraph",
    children: [{ text: "A line of text in a paragraph." }],
  },
];
 
const App = () => {
  const editor = useEditor({ initialValue });
 
  return (
    <EditorRoot editor={editor}>
      <Editable
        onKeyDown={(event) => {
          if (event.key === "&") {
            event.preventDefault();
            editor.update((tx) => {
              tx.text.insert("and");
            });
          }
        }}
      />
    </EditorRoot>
  );
};

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




Pass the renderer to Editable:

const App = () => {
  const editor = useEditor({ initialValue });
 
  return (
    <EditorRoot editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === "&") {
            event.preventDefault();
            editor.update((tx) => {
              tx.text.insert("and");
            });
          }
        }}
      />
    </EditorRoot>
  );
};
const App = () => {
  const editor = useEditor({ initialValue });
 
  return (
    <EditorRoot editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === "&") {
            event.preventDefault();
            editor.update((tx) => {
              tx.text.insert("and");
            });
          }
        }}
      />


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

Toggle a block type

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

import { ElementApi } from "plitejs";
 
const App = () => {
  const editor = useEditor({ initialValue });
 
  return (
    <EditorRoot editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === "`" && event.ctrlKey) {
            event.preventDefault();
 
            editor.update((tx) => {
              tx.blocks.set({ type: "code" });
            });
          }
        }}
      />
    </EditorRoot>
  );
};
import { ElementApi } from "plitejs";
 
const App = () => {
  const editor = useEditor({ initialValue });
 
  return (
    <EditorRoot editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === "`" && event.ctrlKey) {
            event.preventDefault();
 
            editor.update((tx) => {
              tx.blocks.set({ type: 






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

import { ElementApi } from "plitejs";
 
const App = () => {
  const editor = useEditor({ initialValue });
 
  return (
    <EditorRoot 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.blocks.set({ type: match ? "paragraph" : "code" });
            });
          }
        }}
      />
    </EditorRoot>
  );
};
import { ElementApi } from "plitejs";
 
const App = () => {
  const editor = useEditor({ initialValue });
 
  return (
    <EditorRoot editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === "`" && event.ctrlKey) {
            event.preventDefault();
 
            const match = editor.read((state) =>
              state.nodes.













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

"code"
:
return <CodeElement {...props} />;
default:
return <DefaultElement {...props} />;
}
};
</EditorRoot>
);
};
"code"
});
});
}
}}
/>
</EditorRoot>
);
};
find
({
match: (node) =>
ElementApi.isElement(node) && node.type === "code",
})
);
editor.update((tx) => {
tx.blocks.set({ type: match ? "paragraph" : "code" });
});
}
}}
/>
</EditorRoot>
);
};