docs/solutions/developer-experience/2026-04-27-slate-react-void-renderers-should-not-own-hidden-spacer-children.md
VoidElement exposed hidden spacer ownership to app renderers. That made every
image, embed, or custom void renderer responsible for DOM that exists for
selection and browser behavior, not visible UI.
Inline voids have the same trap with hidden anchor children. A mention renderer
should render @R2-D2, not decide where the zero-width child lives for Mac,
Android, Chromium, or selection repair.
{children} or a spacer prop even though
app authors only wanted to render visible content.{children} through platform-specific visible
markup.spacer override as the normal path. That made the bug opt-in to
correctness.{children} manually. That duplicated hidden DOM
responsibility across app code.Move hidden spacer children into internal runtime context and make
VoidElement render visible content only:
export const VoidSpacerChildrenContext = createContext<ReactNode>(null)
EditableTextBlocks provides the hidden children while rendering a void node:
<VoidSpacerChildrenContext.Provider value={voidNode ? children : null}>
<EditableRenderedElement {...props} />
</VoidSpacerChildrenContext.Provider>
VoidElement consumes that context and no longer accepts app-owned hidden
children:
const spacerChildren = useContext(VoidSpacerChildrenContext)
return (
<SlateElement isVoid style={{ position: 'relative', ...style }}>
<Content contentEditable={false} style={contentStyle}>
{content}
</Content>
<SlateSpacer style={spacerStyle}>{spacerChildren}</SlateSpacer>
</SlateElement>
)
Example renderers then stay focused on visible UI:
return <VoidElement content={} />
Inline voids use a separate visible-content primitive:
return (
<InlineVoidElement
content={`@${element.character}`}
contentAs="div"
data-cy={`mention-${element.character.replace(' ', '-')}`}
style={style}
/>
)
The runtime chooses hidden child placement internally:
const hiddenChildren = useContext(VoidHiddenChildrenContext)
const contentChildren = isApplePlatform() ? (
<>
{hiddenChildren}
{content}
</>
) : (
<>
{content}
{hiddenChildren}
</>
)
Hidden spacer and hidden anchor children are part of the editor runtime contract. They support selection mapping, zero-width text, and browser repair behavior. App code should not be able to accidentally omit or move them while rendering visible void content.
Internal context keeps the existing React flexibility for visible content while
making the browser-critical DOM non-optional. It also gives tests a clean
contract: ordinary VoidElement renderers do not pass hidden spacer children.