Bug
BlockPlaceholderPlugin does not render its placeholder when the editor contains exactly one empty block that has been converted to a list item via @platejs/list's indent-list (e.g., via - autoformat). The placeholder renders correctly on subsequent empty list items created after the first.
Versions
@platejs/utils@52.3.21
@platejs/list (indent-list style)
react-day-picker unused here — irrelevant
- Bun runtime
Reproduction
- Create a Plate editor with
BlockPlaceholderPlugin configured (placeholders: { p: "Write here" }, default query: ({ path }) => path.length === 1) and @platejs/list in indent-list style.
- Editor opens with the default empty value:
[{ type: "p", children: [{ text: "" }] }]. Placeholder renders.
- Focus the editor and type
- (or 1. ). The autoformat converts block 0 into an empty list item: { type: "p", listStyleType: "disc", indent: 1, children: [{ text: "" }] }.
- Expected: placeholder continues to render on this single empty list item.
- Actual: placeholder disappears. No placeholder visible anywhere in the editor.
- Press Enter. A second empty list item is created. Placeholder now renders on block 1 (
"Write here" appears at the second bullet).
Root cause
The render gate in BlockPlaceholderPlugin (node_modules/@platejs/utils/dist/react, around line 238) reads:
if (
query({ ...ctx, node: element, path }) &&
placeholder &&
editor.api.isEmpty(element) &&
!editor.api.isEmpty()
) setOption("_target", { node: element, placeholder: placeholders[placeholder] });
editor.api.isEmpty() (no args) returns true when editor.children.length === 1 && isEmpty(editor, editor.children[0]). Slate's element-level isEmpty only inspects element.children — it ignores extra properties like listStyleType and indent. So a single empty list item (indent-list keeps type: "p" and adds those properties) is still "empty" to Slate, editor.api.isEmpty() returns true, and the !editor.api.isEmpty() guard suppresses the placeholder.
The intent of that guard is clearly "don't render the placeholder when the editor is in its pristine initial state." But indent-list transformations produce a state that is not pristine from the user's perspective yet still registers as empty under the current check.
Proposed fix
Replace the !editor.api.isEmpty() guard with a check that also considers block-level structural properties:
// Pseudocode — don't suppress placeholder when the single empty block has list/indent properties.
const isPristineEditor =
editor.children.length === 1 &&
editor.api.isEmpty(editor.children[0]) &&
!editor.children[0].listStyleType &&
!editor.children[0].indent;
if (query(...) && placeholder && editor.api.isEmpty(element) && !isPristineEditor) setOption(...);
Alternatively, expose the guard as a plugin option (e.g., suppressOnPristineEditor: boolean, or a shouldRender(editor, element) override) so consumers can configure the behavior per editor. This would be the most flexible fix — and query alone cannot currently override it because it runs before the hardcoded check.
Workaround for other users hitting this
- Render a custom placeholder overlay outside the plugin, gated on the "single empty list item" state.
- Or patch the plugin via
patch-package to change the hardcoded guard.
Happy to contribute a PR if you'd like.
Bug
BlockPlaceholderPlugindoes not render its placeholder when the editor contains exactly one empty block that has been converted to a list item via@platejs/list's indent-list (e.g., via-autoformat). The placeholder renders correctly on subsequent empty list items created after the first.Versions
@platejs/utils@52.3.21@platejs/list(indent-list style)react-day-pickerunused here — irrelevantReproduction
BlockPlaceholderPluginconfigured (placeholders: { p: "Write here" }, defaultquery: ({ path }) => path.length === 1) and@platejs/listin indent-list style.[{ type: "p", children: [{ text: "" }] }]. Placeholder renders.-(or1.). The autoformat converts block 0 into an empty list item:{ type: "p", listStyleType: "disc", indent: 1, children: [{ text: "" }] }."Write here"appears at the second bullet).Root cause
The render gate in
BlockPlaceholderPlugin(node_modules/@platejs/utils/dist/react, around line 238) reads:editor.api.isEmpty()(no args) returns true wheneditor.children.length === 1 && isEmpty(editor, editor.children[0]). Slate's element-levelisEmptyonly inspectselement.children— it ignores extra properties likelistStyleTypeandindent. So a single empty list item (indent-list keepstype: "p"and adds those properties) is still "empty" to Slate,editor.api.isEmpty()returns true, and the!editor.api.isEmpty()guard suppresses the placeholder.The intent of that guard is clearly "don't render the placeholder when the editor is in its pristine initial state." But indent-list transformations produce a state that is not pristine from the user's perspective yet still registers as empty under the current check.
Proposed fix
Replace the
!editor.api.isEmpty()guard with a check that also considers block-level structural properties:Alternatively, expose the guard as a plugin option (e.g.,
suppressOnPristineEditor: boolean, or ashouldRender(editor, element)override) so consumers can configure the behavior per editor. This would be the most flexible fix — andqueryalone cannot currently override it because it runs before the hardcoded check.Workaround for other users hitting this
patch-packageto change the hardcoded guard.Happy to contribute a PR if you'd like.