noteloom

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:

js
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:

jsx
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

FieldRequiredPurpose
componentYesReact component rendered for this block type. Receives only { id } — read everything else via useBlock(id).
isLeafYestrue if contentIds holds run ids (like paragraph/heading); false if it holds child block ids (like callout/layout column).
defaultPropsNoFallback props merged in where the stored block is missing them.
toHTML(block, ctx)NoHTML export/copy serialization.
toPlainText(block, ctx)NoPlain-text export.
fromHTML(domNode, ctx)NoRecognize 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 / slashCommandsNoOne 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.

See the actual source for every built-in block (e.g. callout, toggleHeading) as worked examples — none of them take a shortcut unavailable to your own code.