Registering custom blocks
Every built-in block is registered the same way your own would be — there's no special-cased internal path. A block type is a plain object with a component and a handful of optional serialization/behavior hooks:
registry.register('myBlock', {
component: MyBlockComponent, // receives only { id }
isLeaf: true, // true if contentIds holds run ids, false if it holds child block ids
toHTML(block, ctx) { /* ... */ },
fromHTML(domNode, ctx) { /* ... or return null if this node isn't yours */ },
toPlainText(block, ctx) { /* ... */ },
slashCommand: { label: 'My Block', keywords: ['my'], run(store, ctx) { /* ... */ } },
});registry here is the same registry useEditor() builds for you — pass a registerBlocks callback to reach it, the same pattern as picking only the blocks you want:
import { useEditor, NoteloomEditor, registerBuiltInBlocks } from 'noteloom';
function Editor() {
const editor = useEditor({
registerBlocks: (registry) => {
registerBuiltInBlocks(registry); // keep every built-in block...
registry.register('myBlock', myBlockTypeEntry); // ...plus your own, same object shape as above
},
});
return <NoteloomEditor editor={editor} />;
}Fields
| Field | Required | Purpose |
|---|---|---|
component | Yes | React component rendered for this block type. Receives only { id } — read everything else via useBlock(id). |
isLeaf | Yes | true if contentIds holds run ids (like paragraph/heading); false if it holds child block ids (like callout/layout column). |
defaultProps | No | Fallback props merged in where the stored block is missing them. |
toHTML(block, ctx) | No | HTML export/copy serialization. |
toPlainText(block, ctx) | No | Plain-text export. |
fromHTML(domNode, ctx) | No | Recognize this block type when pasting foreign HTML — return null if the node isn't yours, so the paste pipeline falls through to the next candidate. |
slashCommand / slashCommands | No | One or an array of { label, icon, keywords, run(store, ctx) } entries surfaced in the "/" menu. |
The component's contract
Your component receives only { id } — read the block's current data with useBlock(id), which subscribes only to that block via useSyncExternalStore. For a leaf block, render its runs with <EditableBlockContent /> (the same primitive paragraph/heading/blockquote use) so typing, marks, and inline widgets all work for free. For a container block, render <BlockChildren parentId={id} /> to recurse into its children.
Custom inline types
Inline (run) types follow the same shape, registered on an InlineRegistry instead: component, an isAtomic flag, and the same toHTML/fromHTML/toPlainText/slashCommand hooks. In practice, most custom inline needs are covered by createSelectFieldType rather than building one from scratch.
callout, toggleHeading) as worked examples — none of them take a shortcut unavailable to your own code.