Skip to content

Notion Mode

Notion mode is the Notion-style editor experience layered on top of Domternal: centered content, side gutter with a drag handle, slash command, block context menu, named-token color picker, floating Table of Contents, and a requireExplicitTrigger-style floating menu (no auto-show on empty paragraphs).

All four wrappers (Angular, React, Vue, Vanilla) support Notion mode with the same feature set. This guide covers the cross-framework setup; the per-framework guides cover wrapper-specific details.

  • Block handle appears in the side gutter on hover, with a ⋮⋮ drag handle and a + button
  • Drag-to-reorder with a custom drop indicator. Sibling vs. nested-child drop is decided by the X position over list items.
  • Slash command - type / to open the insert popup with all available block types
  • Block context menu - click the drag handle for Delete / Duplicate / Turn into / Colors / Copy link
  • Notion color picker - the bubble menu has an “A” trigger that opens a named-token color picker (9 colors text + 9 colors background, plus default)
  • Block-level colors persist across “Turn into” transformations
  • Smart paste - heading-into-heading and trailing Shift+Enter paste behave correctly
  • Keyboard reorder - Mod-Shift-ArrowUp/Down moves the current top-level block
  • Floating Table of Contents - sticky outline with hover-expanded card; tracks the active heading via IntersectionObserver
  • Inline /toc block - insertable atom node renders a reactive heading list
  • Notion-strict list/task UX - listItem/taskItem schema is paragraph block*. Children-zone Enter inserts sibling in the item; Backspace at offset 0 lifts as top-level paragraph.
PiecePackagePurpose
StarterKit (without codeBlock)@domternal/coreBase extensions
CodeBlockLowlight@domternal/extension-code-block-lowlightReplaces StarterKit’s codeBlock with syntax-highlighted blocks
BlockHandle@domternal/extension-block-menuHover gutter, drag, + button
BlockContextMenu@domternal/extension-block-menuDelete / Duplicate / Turn into / Colors / Copy link
KeyboardReorder@domternal/extension-block-menuMod-Shift-Up/Down moves blocks
SlashCommand@domternal/extension-block-menu/ insert popup
SmartPaste@domternal/extension-block-menuBlock-format-preserving paste
FloatingMenu (with requireExplicitTrigger)@domternal/extension-block-menuBlock-insert menu (opens only on + button or Mod-/)
NotionColorPicker@domternal/core”A” bubble-menu trigger + colorToken/bgToken attrs on textStyle
BlockColor@domternal/coreBlock-level bgColor/textColor attrs (Colors picker in context menu)
UniqueID@domternal/coreStable ids on headings (TOC anchor source)
ListIndent@domternal/coreTab/Shift-Tab at list boundaries
Placeholder@domternal/coreNotion-style empty-paragraph placeholder hint
TableOfContents@domternal/extension-tocHeading observer + scrollToHeading
FloatingTocOutline@domternal/extension-tocSticky outline UI
TableOfContentsBlock@domternal/extension-tocInline /toc atom node
.dm-notion-mode CSS class@domternal/themeCentered content, side gutter, larger font
Terminal window
pnpm add @domternal/core @domternal/theme @domternal/extension-block-menu @domternal/extension-toc @domternal/extension-code-block-lowlight

Plus your framework wrapper of choice: @domternal/angular, @domternal/react, @domternal/vue, or @domternal/vanilla.

import {
StarterKit, BubbleMenu, NotionColorPicker, BlockColor,
UniqueID, ListIndent, Placeholder,
} from '@domternal/core';
import {
BlockHandle, BlockContextMenu, SlashCommand, SmartPaste,
KeyboardReorder, FloatingMenu,
} from '@domternal/extension-block-menu';
import {
TableOfContents, FloatingTocOutline, TableOfContentsBlock,
} from '@domternal/extension-toc';
import { CodeBlockLowlight } from '@domternal/extension-code-block-lowlight';
import {
DomternalEditor, DomternalBubbleMenu, DomternalFloatingMenu,
DomternalNotionColorPicker,
} from '@domternal/vanilla';
import '@domternal/theme';
const editorEl = document.getElementById('editor')!;
const bubbleEl = document.getElementById('bubble')!;
const floatingEl = document.getElementById('floating')!;
editorEl.classList.add('dm-notion-mode');
const dm = new DomternalEditor(editorEl, {
extensions: [
StarterKit.configure({ codeBlock: false }),
CodeBlockLowlight,
UniqueID.configure({ types: ['heading', 'paragraph', 'blockquote', 'bulletList', 'orderedList', 'taskList', 'listItem', 'taskItem'] }),
Placeholder.configure({ placeholder: "Press '/' for commands" }),
BlockColor,
NotionColorPicker,
ListIndent,
BlockHandle.configure({ nested: true }),
BlockContextMenu,
SlashCommand,
SmartPaste,
KeyboardReorder,
FloatingMenu.configure({ element: floatingEl, requireExplicitTrigger: true }),
TableOfContents,
FloatingTocOutline.configure({ anchor: 'editor' }),
TableOfContentsBlock,
BubbleMenu.configure({ element: bubbleEl }),
],
});
new DomternalBubbleMenu(bubbleEl, { editor: dm.editor });
new DomternalFloatingMenu(floatingEl, { editor: dm.editor });
new DomternalNotionColorPicker({ editor: dm.editor });
NotionColorPicker.configure({
palette: ['slate', 'rose', 'amber', 'emerald'], // requires matching --dm-block-text-* and --dm-block-bg-* variables in your theme CSS
})
BlockColor.configure({
bgColors: ['slate', 'rose', 'amber', 'emerald'],
textColors: ['slate', 'rose', 'amber', 'emerald'],
})

SlashCommand consumes items from the same addFloatingMenuItems hook as FloatingMenu. Override the items list:

SlashCommand.configure({
items: (defaults, editor) => [
...defaults,
{
name: 'myBlock',
label: 'My Custom Block',
icon: 'star',
group: 'Custom',
command: 'insertMyBlock',
},
],
})
BlockHandle.configure({
nested: {
allowedNodes: ['listItem', 'taskItem', 'detailsContent'], // extend nested resolution
promoteOnEdge: 'left', // shallower ancestor wins near left edge
},
nestThreshold: 48, // require more X displacement before committing to nested-child drop
})

By default BlockContextMenu writes #<blockId> appended to the current URL. Override for client-side routing:

BlockContextMenu.configure({
onCopyLink: (blockId, editor) => `https://myapp.com/docs/${docId}#${blockId}`,
})
BlockContextMenu.configure({
turnIntoTargets: [
{ label: 'Paragraph', icon: 'textT', nodeType: 'paragraph' },
{ label: 'Heading 1', icon: 'textHOne', nodeType: 'heading', attrs: { level: 1 } },
// omit other targets to curate the list
],
})
BlockContextMenu.configure({
turnIntoEnabled: false, // hide "Turn into" section
blockColorEnabled: false, // hide Colors row (still works when BlockColor not loaded either)
copyLinkEnabled: false, // hide Copy link
})

BlockContextMenu dispatches dm:copy-link-success and dm:copy-link-error events on the editor’s .dm-editor element. Add a listener to show toasts:

const editorEl = dm.editor.view.dom.closest('.dm-editor')!;
function showToast(message: string, kind: 'success' | 'error') {
const toast = document.createElement('div');
toast.className = kind === 'error'
? 'notion-demo-toast notion-demo-toast--error'
: 'notion-demo-toast';
toast.setAttribute('role', kind === 'error' ? 'alert' : 'status');
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), kind === 'error' ? 2600 : 1800);
}
editorEl.addEventListener('dm:copy-link-success', () => showToast('Link copied', 'success'));
editorEl.addEventListener('dm:copy-link-error', () => showToast('Failed to copy', 'error'));

All Notion-mode overlays cooperate via the dm:dismiss-overlays custom event dispatched on the .dm-editor element. When one opens, it dispatches the event to close the others.

OverlayDispatchesListens
BlockContextMenuOpenClose
SlashCommandOpenClose
FloatingMenuOpen (explicit trigger)Close
BubbleMenuOpenClose
NotionColorPickerOpenClose

To integrate your own overlays:

const editorEl = dm.editor.view.dom.closest('.dm-editor');
editorEl?.addEventListener('dm:dismiss-overlays', () => {
// close your custom overlay
});

The @domternal/theme package ships the .dm-notion-mode class. Apply it to your .dm-editor element (or a parent) to activate Notion-style layout: centered content, larger font, generous line-height, side gutter for the block handle.

PropertyDefault (notion mode)Purpose
--dm-block-handle-gutter0Column reserved for BlockHandle inside the editor (notion mode pushes it OUTSIDE)
--dm-block-handle-left-3.5remHorizontal offset of the gutter (notion mode places it fully outside the content column)
--dm-task-checkbox-top0.45emVertical alignment of task checkboxes (tuned for line-height 1.7)
--dm-block-children-indent1.5remHorizontal indent for blocks in the children-zone of a list item
.dm-notion-mode {
max-width: 48rem; /* default is 38rem */
}
:root {
--dm-block-text-blue: #3b82f6;
--dm-block-bg-blue: rgba(59, 130, 246, 0.1);
/* ... other tokens */
}

When you load the Notion mode stack, listItem and taskItem schemas become strict paragraph block*. If you load existing JSON content where a list item’s first child is NOT a paragraph, parsing will fail. See List Item and Task Item for migration patterns.