docs/solutions/developer-experience/2026-04-15-slate-react-text-block-placeholder-contract-must-omit-native-textarea-placeholder.md
EditableTextBlocksProps exposes a current custom placeholder?: ReactNode
contract so examples can render real JSX placeholder hosts.
The prop bag still inherited the native textarea placeholder?: string
attribute, so TypeScript intersected the two contracts into garbage.
placeholder-surface.tsx
and custom-placeholder.tsx
started failing tsc<><p>Type something</p></> were rejected even
though the runtime rendered themThe problem was not the examples. The package type surface was lying.
Omit the native placeholder key from the inherited textarea attributes in
editable-text-blocks.tsx
so the current custom ReactNode placeholder contract owns the name cleanly.
} & Omit<
TextareaHTMLAttributes<HTMLDivElement>,
| 'autoFocus'
| 'children'
| 'className'
| 'id'
| 'onKeyDown'
| 'onPaste'
| 'placeholder'
| 'readOnly'
| 'spellCheck'
| 'style'
>;
Then pin the contract with a compile-time proof in
primitives-contract.tsx
using JSX placeholder hosts on both EditableBlocksProps and
EditableTextBlocksProps.
Without the omit, the type system sees two different placeholder contracts:
stringReactNodeThat intersection is narrower than either real runtime contract, so callers get type errors for perfectly valid JSX.
Once the native key is removed, the current public surface becomes honest
again: callers can pass strings or richer JSX because ReactNode owns the
prop.
Slate
wrapper, thread projectionStore through that provider or the decoration
slices never reach EditableBlocks.