From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
UNPUBLISHED
  • Overview
  • Why This Fork
  • Examples
Walkthroughs
  • Installing Plite
  • Adding Event Handlers
  • Defining Custom Elements
  • Applying Custom Formatting
  • Executing Commands
  • Saving to a Database
  • Canonical Change Substrate
  • Improving Performance
Concepts
  • Interfaces
  • Nodes
  • Locations
  • Transforms
  • Document Changes
  • Commands
  • Editor
  • Extensions
  • Rendering
  • Serializing
  • Normalizing
  • Using TypeScript
  • Roots
  • Document State
  • Editing Behavior
  • Selection And DOM
  • Clipboard And Paste
  • Projection And Overlays
  • Schema
API
  • 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
Libraries
  • Plite DOM
  • History Editor API
  • History Extension Setup
  • History
  • Plite History
  • Plite Hyperscript
  • Plite Layout
  • Annotations
  • DOM Coverage Boundaries
  • Editable Component
  • Plite React Event Handling
  • Virtualized Rendering
  • Plite React Hooks
  • React Editor Setup
  • React Editor
  • Plite React
  • Plite Component
  • Plite Yjs
  • Plite
General
  • Migration
  • Contributing
  • Docs Proof Map
  • 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 = usePliteEditor({ 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
<Plite editor={editor}>
<Editable
onKeyDown={(event) => {
if (event.key === "&") {
event.preventDefault();
editor.update((tx) => {
tx.text.insert("and");
});
}
}}
/>
</Plite>
);
};
const initialValue = [
  {
    type: "paragraph",
    children: [{ text: "A line of text in a paragraph." }],
  },
];
 
const App = () => {
  const editor = usePliteEditor({ initialValue });
 
  return (
    <Plite editor={editor}>
      <Editable
        onKeyDown={(event) => {
          if (event.key === "&") {
            event.preventDefault();
            editor.update((tx) => {
              tx.text.insert("and");
            });
          }
        }}
      />
    </Plite>
  );
};

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 = usePliteEditor({ initialValue });
 
  return (
    <Plite editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === "&") {
            event.preventDefault();
            editor.update((tx) => {
              tx.text.insert("and");
            });
          }
        }}
      />
    </Plite>
  );
};
const App = () => {
  const editor = usePliteEditor({ initialValue });
 
  return (
    <Plite 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.nodes.set(...) to change the selected block.

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






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

import { ElementApi } from "@platejs/plite";
 
const App = () => {
  const editor = usePliteEditor({ initialValue });
 
  return (
    <Plite 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" });
            });
          }
        }}
      />
    </Plite>
  );
};
import { ElementApi } from "@platejs/plite";
 
const App = () => {
  const editor = usePliteEditor({ initialValue });
 
  return (
    <Plite editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (event.key === "`" && event.ctrlKey) {
            event.preventDefault();
 
            const match = editor.read((state) =>
              state.nodes.find













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

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