Plite React renders your document with normal React components. You provide the
components for product-specific elements, text nodes, leaves, toolbars, menus,
and overlays. Pass stable render props to Editable so React does not receive
a fresh renderer on every render.
import { EditorRoot, Editable, useEditor } from "plitejs/react";
const ParagraphElement = ({ attributes, children }) => {
return <p {...attributes}>{children}</p>;
};
const QuoteElement =
import { EditorRoot, Editable, useEditor } from "plitejs/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 = useEditor({ initialValue });
return (
<EditorRoot editor={editor}>
<Editable renderElement={renderElement} />
</EditorRoot>
);
};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 Plite's DOM helpers. The children render text leaves, inline
elements, void anchors, and nested editable content.
When text-level formatting is rendered, characters are grouped into leaves that share the same persisted marks.
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 text node into rendered segments, renderLeaf
receives leafPosition. It contains the segment's start and end offsets
plus optional isFirst and isLast flags. Decoration attributes are already
applied inside children; they never become fields on leaf.
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>tBecause 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>tIf 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 Plite 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:
renderLeaf customizes persisted mark presentation. Use renderText when you
need one wrapper around the entire text node, regardless of Decoration
boundaries.
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" data-length={text.text.length}>
{children}
</span>
);
};
<Editable renderText={TextNode} />;const TextNode = ({ attributes, children, text }) => {
return (
<span {...attributes} className="custom-text" data-length={text.text.length}>
{children}
</span>
);
};
<Editable renderText={TextNode} />;When to use renderLeaf vs renderText:
renderLeaf: Use this for persisted marks such as leaf.bold. It may run
more than once for one text node when Decoration boundaries split the rendered
content. Use leafPosition only when mark presentation depends on segment
position.
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 renderText and renderLeaf together. renderLeaf renders marks,
Decoration spans wrap transient ranges inside its children, and renderText
renders the container around every segment.
Decorations are transient range attributes. Configure ordered sources on
<EditorRoot decorations={sources}>; each source returns keyed ranges with
className, style, aria-*, or data-* attributes. Editable applies those
attributes automatically. Decorations never become text properties or leaf
renderer inputs.
Use Decorations for search results, syntax highlighting, and external diagnostics. Use marks for persisted text formatting. See Decorations and annotations for source observation and refresh.
In addition to controlling the rendering of nodes inside Plite, descendants can retrieve the provider editor with useEditorContext().
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 = useEditor({ initialValue });
return (
<EditorRoot editor={editor}>
<Toolbar />
<Editable />
</EditorRoot>
);
};
import { useEditorState } from "plitejs/react";
const Toolbar = () => {
const isBold = useEditorState((state) => {
return state.marks()?.bold === true;
});
return (
<div>
<button type="button" aria-pressed={isBold}>B</button>
<button type="button">I</button>
</div>
);
};const MyEditor = () => {
const editor = useEditor({ initialValue });
return (
<EditorRoot editor={editor}>
<Toolbar />
<Editable />
</EditorRoot>
);
};
import { useEditorState } from "plitejs/react";
const Toolbar = () => {
const isBold = useEditorState((state) => {
return state.marks()?.bold === true
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, and useTextSelector.
Custom styles can be applied to the editor itself by using the style prop on the <Editable> component.
const MyEditor = () => {
const editor = useEditor({ initialValue });
return (
<EditorRoot editor={editor}>
<Editable style={{ minHeight: "200px", backgroundColor: "lime" }} />
</EditorRoot>
);
};const MyEditor = () => {
const editor = useEditor({ initialValue });
return (
<EditorRoot editor={editor}>
<Editable style={{ minHeight: "200px", backgroundColor: "lime" }} />
</EditorRoot>
);
};It is also possible to apply custom styles with a stylesheet and className. However, Plite 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:
style prop instead of a stylesheet, which overrides the default inline styles.disableDefaultStyles prop to the <Editable> component.!important in your stylesheet declarations to make them override the inline styles.See Improving Performance for ways to improve the rendering performance of the editor.