This example renders form controls and a nested Plate editor inside a void
element. It is app-local demo code built with createPlatePlugin, not a packaged
feature plugin.
Demo
Source
The demo registers an editable-void element plugin and renders custom React UI
for that node.
'use client';
import * as React from 'react';
import type { PlateElementProps } from 'platejs/react';
import { createPlatePlugin, Plate, usePlateEditor } from 'platejs/react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { EditorKit } from '@/components/editor/editor-kit';
import { editableVoidsValue } from '@/registry/examples/values/editable-voids-value';
import { Editor, EditorContainer } from '@/components/ui/editor';
export const EditableVoidPlugin = createPlatePlugin({
key: 'editable-void',
node: {
component: EditableVoidElement,
isElement: true,
isVoid: true,
},
});
export function EditableVoidElement({
attributes,
children,
}: PlateElementProps) {
const [inputValue, setInputValue] = React.useState('');
const editor = usePlateEditor({
plugins: EditorKit,
});
return (
// Need contentEditable=false or Firefox has issues with certain input types.
<div {...attributes} contentEditable={false}>
<div className="mt-2 grid gap-6 rounded-md border p-6 shadow-sm">
<Input
id="name"
className="my-2"
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
}}
placeholder="Name"
type="text"
/>
<div className="grid w-full max-w-sm items-center gap-2">
<Label htmlFor="handed">Left or right handed:</Label>
<RadioGroup id="handed" defaultValue="r1">
<div className="flex items-center space-x-2">
<RadioGroupItem id="r1" value="r1" />
<Label htmlFor="r1">Left</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem id="r2" value="r2" />
<Label htmlFor="r2">Right</Label>
</div>
</RadioGroup>
</div>
<div className="grid gap-2">
<Label htmlFor="editable-void-basic-blocks">
Tell us about yourself:
</Label>
<Plate
editor={editor}
// initialValue={basicBlocksValue}
>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
</div>
</div>
{children}
</div>
);
}
export default function EditableVoidsDemo() {
const editor = usePlateEditor({
plugins: [...EditorKit, EditableVoidPlugin],
value: editableVoidsValue,
});
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}'use client';
import * as React from 'react';
import type { PlateElementProps } from 'platejs/react';
import { createPlatePlugin, Plate, usePlateEditor } from 'platejs/react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { EditorKit } from '@/components/editor/editor-kit';
import { editableVoidsValue } from '@/registry/examples/values/editable-voids-value';
import { Editor, EditorContainer } from '@/components/ui/editor';
export const EditableVoidPlugin = createPlatePlugin({
key: 'editable-void',
node: {
component: EditableVoidElement,
isElement: true,
isVoid: true,
},
});
export function EditableVoidElement({
attributes,
children,
}: PlateElementProps) {
const [inputValue, setInputValue] = React.useState('');
const editor = usePlateEditor({
plugins: EditorKit,
});
return (
// Need contentEditable=false or Firefox has issues with certain input types.
<div {...attributes} contentEditable={false}>
<div className="mt-2 grid gap-6 rounded-md border p-6 shadow-sm">
<Input
id="name"
className="my-2"
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
}}
placeholder="Name"
type="text"
/>
<div className="grid w-full max-w-sm items-center gap-2">
<Label htmlFor="handed">Left or right handed:</Label>
<RadioGroup id="handed" defaultValue="r1">
<div className="flex items-center space-x-2">
<RadioGroupItem id="r1" value="r1" />
<Label htmlFor="r1">Left</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem id="r2" value="r2" />
<Label htmlFor="r2">Right</Label>
</div>
</RadioGroup>
</div>
<div className="grid gap-2">
<Label htmlFor="editable-void-basic-blocks">
Tell us about yourself:
</Label>
<Plate
editor={editor}
// initialValue={basicBlocksValue}
>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
</div>
</div>
{children}
</div>
);
}
export default function EditableVoidsDemo() {
const editor = usePlateEditor({
plugins: [...EditorKit, EditableVoidPlugin],
value: editableVoidsValue,
});
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}The initial value contains a normal paragraph, one editable-void element, and
an empty paragraph after it.
/** @jsxRuntime classic */
/** @jsx jsx */
import { jsx } from '@platejs/test-utils';
jsx;
export const editableVoidsValue: any = (
<fragment>
<hp>
In addition to nodes that contain editable text, you can insert void
nodes, which can also contain editable elements, inputs, or an entire
other Slate editor.
</hp>
<element type="editable-void">
<htext />
</element>
<hp>
<htext />
</hp>
</fragment>
);/** @jsxRuntime classic */
/** @jsx jsx */
import { jsx } from '@platejs/test-utils';
jsx;
export const editableVoidsValue: any = (
<fragment>
<hp>
In addition to nodes that contain editable text, you can insert void
nodes, which can also contain editable elements, inputs, or an entire
other Slate editor.
</hp>
<element type="editable-void">
<htext />
</element>
<hp>
<htext />
</hp>
</fragment>
);Runtime Shape
| Piece | Owner | Notes |
|---|---|---|
EditableVoidPlugin | Registry example | Uses createPlatePlugin with node.isElement: true and node.isVoid: true. |
EditableVoidElement | Registry example | Renders inputs, radio controls, and a nested <Plate> instance. |
| Outer editor | Registry example | Loads EditorKit plus EditableVoidPlugin and the editableVoidsValue. |
| Inner editor | Registry example | Creates a separate usePlateEditor({ plugins: EditorKit }) inside the void element. |
| Value shape | Slate | Keeps an empty text child inside the void node so Slate can select it. |
Void Element Rules
Set contentEditable={false} on the custom void wrapper. Without it, browser
editing behavior can leak into the nested controls; the demo notes Firefox input
issues specifically.
Render {children} after the non-editable UI. Slate still needs the hidden void
child mounted even when your visible element is fully custom React.
The nested editor is a separate Plate editor. It does not share the outer editor's value, selection, plugins, or undo history.
Related
- Plate Plugin covers
node.isVoid,node.component, and plugin rendering. - Plugin Components covers custom node components.
- Plate Components covers
<Plate>, editor providers, and node primitives.