Full example: every block, inline type, and a custom plugin
One document, touching everything the editor ships with — every block type, every inline type, tables, layout columns, a toggle heading, and a live, editable canvas — plus two custom types (a rating block and a priority inline field) registered exactly the way any built-in type is, to show that there is no separate, more-privileged internal path. Undo/redo, voice dictation, comments, and version history are all wired up too — this is real, live, and editable, not a screenshot.
useEditor()/<NoteloomEditor /> pattern, with the custom block, custom inline field, and block template all wired in. For the live document instead (the JSON this exact content produces), stay on Preview and click "View source" in the toolbar. Select some text for a Comment button in the floating toolbar, try Dictate if your browser supports the Web Speech API, or Save version now below the editor and then edit something — the restore button brings back exactly what you saved. Press "/" anywhere to insert a new block, including Rating (custom) and Meeting agenda (a block template).The setup behind this page
Nothing above is special-cased. It is the same useEditor()/<NoteloomEditor /> pattern from Getting started, with two extra lines per registry:
import { useEditor, NoteloomEditor, registerBuiltInBlocks, registerBuiltInInlineTypes } from 'noteloom';
function Editor() {
const editor = useEditor({
doc: myStarterDoc,
registerBlocks: (registry) => {
registerBuiltInBlocks(registry); // every built-in block...
registry.register('rating', ratingBlockType); // ...plus your own
},
registerInlineTypes: (inlineRegistry) => {
registerBuiltInInlineTypes(inlineRegistry);
inlineRegistry.register('priority', priorityFieldType);
},
});
return <NoteloomEditor editor={editor} />;
}The custom block, in full
rating is a genuinely new block type — its own component, its own HTML/plain-text export, and its own slash command — registered with registry.register(), the identical call every built-in block goes through internally:
function RatingBlock({ id }) {
const block = useBlock(id);
const store = useEditorStore();
const value = block?.props?.value ?? 0;
return (
<div style={{ display: 'flex', gap: 6 }}>
{[1, 2, 3, 4, 5].map((n) => (
<button
key={n}
onClick={() => store.applyOperation(operations.updateBlockProps(id, { value: n }))}
style={{ color: n <= value ? '#f5a623' : '#d0d0d0' }}
>
★
</button>
))}
</div>
);
}
const ratingBlockType = {
component: RatingBlock,
isLeaf: true,
defaultProps: { value: 0 },
toHTML: (block) => `<div data-rating="${block.props?.value ?? 0}">…</div>`,
toPlainText: (block) => `Rating: ${block.props?.value ?? 0}/5`,
slashCommand: {
label: 'Rating',
keywords: ['rating', 'stars', 'review'],
run: (store, ctx) => store.applyOperation(operations.changeBlockType(ctx.blockId, 'rating', { value: 0 })),
},
};See Registering custom blocks for the full field reference.
The custom inline field, in full
priority needed no component at all — createSelectFieldType builds a complete, ready-to-register inline type from a plain config object, the same way this site's own docs build a "Status" field:
const priorityFieldType = createSelectFieldType({
type: 'priority',
label: 'Priority',
placeholder: 'Set priority…',
variant: 'tag',
options: [
{ value: 'low', label: 'Low', color: { bg: '#e9e9e7', text: '#37352f' } },
{ value: 'medium', label: 'Medium', color: { bg: '#fdecc8', text: '#a06400' } },
{ value: 'high', label: 'High', color: { bg: '#fbe4e4', text: '#a83232' } },
],
});See Custom select field types for dynamic/API-backed options and letting end users create their own field types in-editor.
Also wired up on this page
Beyond the blocks and inline types above, this demo also mounts:
- Undo/redo via
useHistory(). - Voice dictation via
useVoiceTyping()— say "heading one", "new paragraph", or "undo" while dictating. Only shows up if your browser supports the Web Speech API. See Mobile & voice typing. - Comments — select text for the floating toolbar's Comment button, plus a persistent panel via
showCommentsPanel. See Comments. - Version history — "Save version now" below the editor, with a list of saved versions and one-click restore. See Version history.
- A block template — type
"/agenda"to insert a captured snippet, registered withcaptureBlockTemplate/registerBlockTemplates. See Templates for document templates and a saved-library UI too. - Right-to-left text — the last paragraph has
props.dir: "rtl"set explicitly. See Accessibility & RTL. - End-user-created field types — the "+ New field type" button opens
FieldTypeEditorModal, letting anyone define their own select field from inside the editor, no code required. - Printing — the Print button is a plain
window.print()call; see Getting started for what the built-in print stylesheet hides automatically.
What's deliberately not on this page
Features that need more than one connected instance to mean anything — live collaboration and offline persistence across reloads — aren't crammed in here, since a single static seed document can't demonstrate multi-session behavior honestly, and persisting real edits to this exact page would mean every visitor sees whatever the last visitor left behind, instead of a clean reference document. The mobile action bar is also absent — it needs direct access to the editor surface's DOM node, which the convenience <NoteloomEditor> component deliberately doesn't expose (that one piece needs the granular API instead). Each of these has its own live demo where relevant (the collaboration playground, for instance).
rating block uses.