noteloom

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.

Flip to the Code tab above to see the actual install command and React setup behind this page — the same 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:

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

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

js
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 with captureBlockTemplate/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).

Next: Architecture for how the document model underneath all of this fits together, or Registering custom blocks for the complete API this page's rating block uses.