Skip to content

Notion Color Picker

NotionColorPicker adds a bubble-menu trigger (“A” glyph with a slash) that drives named-token inline colors on the textStyle mark - the same 9-color Notion palette mapped to CSS custom properties. Hex/token mutual exclusion means setting a token clears any existing hex, and setting a hex clears any existing token (last action wins).

Not included in StarterKit. Add it separately for Notion-style color UX.

import {
Editor, StarterKit,
TextStyle, TextColor, Highlight,
NotionColorPicker, BubbleMenu,
} from '@domternal/core';
import '@domternal/theme';
const editor = new Editor({
element: document.getElementById('editor')!,
extensions: [
StarterKit,
TextStyle, TextColor, Highlight, // textStyle mark + color attribute providers
NotionColorPicker, // adds bubble-menu trigger + colorToken/backgroundColorToken attrs
BubbleMenu.configure({ element: bubbleEl }),
],
});

The “A” trigger appears in the bubble menu when text is selected. Framework wrappers render the picker panel; see the per-framework guide for examples.

  • TextStyle mark - required (extension declares dependencies: ['textStyle'])
  • TextColor + Highlight - recommended (they own the colorToken / backgroundColorToken attribute schemas on textStyle)
  • BubbleMenu - required to surface the “A” trigger (the toolbar item has toolbar: false and is bubble-menu-only)
NotionColorPicker.configure({
palette: DEFAULT_NOTION_COLOR_PALETTE,
})
OptionTypeDefaultDescription
palettereadonly string[]DEFAULT_NOTION_COLOR_PALETTENamed tokens shown in the picker. Each must have matching --dm-block-text-<token> and --dm-block-bg-<token> CSS variables in the active theme. Tokens with no theme support render as transparent swatches.
export const DEFAULT_NOTION_COLOR_PALETTE: readonly string[] = Object.freeze([
'gray',
'brown',
'orange',
'yellow',
'green',
'blue',
'purple',
'pink',
'red',
]);

These tokens align with BlockColor’s palette, so inline + block tints share the same color set.

interface NotionColorPickerStorage {
isOpen: boolean;
}

The UI flips editor.storage.notionColorPicker.isOpen as the picker opens and closes. Read this to gate other overlays or hide the bubble menu while the picker is showing.

The extension registers one toolbar item:

{
name: 'notionColor',
type: 'button',
icon: 'textAUnderline',
group: 'textStyle',
priority: 250,
toolbar: false, // bubble-menu only, hidden from main toolbar
emitEvent: 'notionColorOpen',
}

Because toolbar: false, the item appears only in the bubble menu, not the main toolbar. Clicking it emits the notionColorOpen custom event on the editor, which framework wrappers listen for.

EventDirectionPayloadPurpose
notionColorOpeneditor.emit{ anchorElement?: HTMLElement }Trigger emits when clicked; picker components listen

Listen via editor.on('notionColorOpen', ...) or by wiring the relevant component (DomternalNotionColorPicker in each framework).

The textStyle mark can carry EITHER a hex color (color: '#ff0000') OR a named token (colorToken: 'red'), not both. Setting one clears the other inside the affected range:

ActionEffect
Set colorToken: 'blue'Clears color hex on the range
Set color: '#0000ff'Clears colorToken on the range
Set backgroundColorToken: 'yellow'Clears backgroundColor hex on the range
Set backgroundColor: '#ffff00'Clears backgroundColorToken on the range

This is implemented inside TextColor and Highlight when they handle the new attrs.

When BlockColor sets a block-level color and stripInlineColorConflicts() runs, it removes inline textStyle marks of the same kind (text or bg) inside the affected range so the block tint isn’t visually masked by older inline overrides.

The reverse also applies: applying an inline color token via NotionColorPicker on text that has a block-level color does NOT remove the block color; both layer (inline on top of block background, semantic priority).

All four wrappers ship a DomternalNotionColorPicker component that listens for notionColorOpen and renders the panel:

import { DomternalNotionColorPicker } from '@domternal/vanilla';
const picker = new DomternalNotionColorPicker({ editor: dm.editor });
// .destroy() when finished

The components handle positioning, outside-click, keyboard navigation, and applying the token via editor.chain().focus().setColorToken('blue').run()-style commands.

The theme stylesheet defines color values for each token:

VariablePurpose
--dm-block-text-grayText color for the gray token
--dm-block-text-brownText color for the brown token
--dm-block-text-orangeetc.
--dm-block-bg-grayBackground color for the gray token
--dm-block-bg-brownBackground color for the brown token
(9 text + 9 bg)All 9 palette names

These are defined for both light and dark themes in @domternal/theme. Override them in your app’s CSS to customize colors.

  • .dm-ncp-trigger - the “A” trigger button rendered by BubbleMenu (class-based identity, since the trigger button is destroyed and recreated on every transaction)
  • .dm-notion-color-picker - root panel container
  • .dm-notion-color-swatch - individual swatch button
import {
NotionColorPicker,
DEFAULT_NOTION_COLOR_PALETTE,
} from '@domternal/core';
import type {
NotionColorPickerOptions,
NotionColorPickerStorage,
} from '@domternal/core';

@domternal/core - NotionColorPicker.ts