docs/solutions/logic-errors/2026-04-04-v2-placeholder-primitives-should-own-overlay-attrs-and-style.md
After packaging the v2 node-shape and text-boundary primitives, the placeholder proof surface was still hand-rolling the overlay DOM:
data-slate-placeholdercontentEditable={false}That was the same mistake in smaller clothes.
The same failure mode came back in examples/custom-placeholder: the built-in
placeholder used SlatePlaceholder and looked right, but custom
renderPlaceholder received attributes.style = {} and rendered as normal
black document content instead of the grey absolute overlay.
The first style fix still missed legacy parity: once the placeholder became an
absolute overlay, it no longer contributed to editable root height. The overlay
was visually outside the editor until the root measured placeholder height and
applied minHeight.
A later delete-to-empty pass exposed the same ownership bug from another angle:
after text insertion hid the placeholder, the root rerendered with no
placeholder value. Deleting all text only rerendered the text node, so custom
renderPlaceholder received children: undefined and root height measurement
never restarted.
slate-react owns a reusable SlatePlaceholder primitive:
The placeholder proof surface consumes that primitive instead of repeating the overlay contract inline.
Custom placeholder renderers use the same owner. SlatePlaceholder exposes the
default style through a shared helper, and EditableText passes that merged
style through renderPlaceholder attributes.
EditableTextBlocks measures the mounted placeholder element and applies the
height as root minHeight, matching the legacy placeholder-height contract.
EditableTextBlocks also subscribes to the placeholder-visible state. Empty
state is text-operation-sensitive, not just structure-sensitive, so the root
must rerender when typing/deleting toggles placeholder visibility.
EditableText only calls custom renderPlaceholder when an actual placeholder
value exists.
Placeholder overlays are part of the renderer/input contract, not decorative markup.
Their DOM attrs and styles determine whether the browser treats them as real editable content, whether they interfere with selection, and whether they sit on the correct visual layer.
If each proof surface hand-writes that contract, drift is inevitable.
If the built-in placeholder has the right visual behavior but
renderPlaceholder does not, the primitive owner is still incomplete. The
attrs object passed to custom renderers is part of the same contract.
If an absolute placeholder is taller than the empty text line, root height must come from the placeholder element. Otherwise the overlay is technically styled correctly but still outside the visible editor box.
For slate-react:
renderPlaceholder attrs must receive the same default overlay style
as the built-in placeholderminHeightrenderPlaceholder must not be called with missing placeholder
childrendata-slate-placeholder existsIf a DOM contract affects browser editing behavior, it should not live forever inside example files.