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.
When to use
Section titled “When to use”Use NotionColorPicker when you need:
- A Notion-style bubble menu trigger for text and background colors
- 9 named colors plus a default reset per type, with keyboard navigation and non-modal
role="dialog"semantics - Cooperative dismissal with other overlays (
dm:dismiss-overlays)
Skip it if:
- Your color picking happens in the main toolbar with simple dropdowns
- Your design system only allows a single accent color and per-mark coloring would be off-brand
Quickstart
Section titled “Quickstart”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 and a picker panel is listening. Framework wrappers render that panel; see the per-framework guide for examples.
Dependencies
Section titled “Dependencies”TextStylemark - required (extension declaresdependencies: ['textStyle'])TextColor+Highlight- recommended (they own thecolorToken/backgroundColorTokenattribute schemas ontextStyle)BubbleMenu- required to surface the “A” trigger (the toolbar item hastoolbar: falseand is bubble-menu-only)- A
notionColorOpenlistener - required for the trigger to render at all: aDomternalNotionColorPickercomponent, or your own listener. Loading the extension alone shows nothing, because the trigger’s only action is emitting that event.
Options
Section titled “Options”NotionColorPicker.configure({ palette: DEFAULT_NOTION_COLOR_PALETTE,})| Option | Type | Default | Description |
|---|---|---|---|
palette | readonly string[] | DEFAULT_NOTION_COLOR_PALETTE | Named 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. |
Default palette
Section titled “Default palette”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.
Storage
Section titled “Storage”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.
Toolbar item
Section titled “Toolbar item”The extension registers one toolbar item:
{ name: 'notionColor', type: 'button', command: 'focus', // required by the ToolbarButton schema; emitEvent wins at click time icon: 'textAUnderline', label: 'Text and background color', 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.
command is non-optional on the ToolbarButton type, so the item points it at the harmless focus built-in; it only runs if a host disables the popover UI. label is what a custom toolbar renders as the tooltip.
Events
Section titled “Events”| Event | Direction | Payload | Purpose |
|---|---|---|---|
notionColorOpen | editor.emit | { anchorElement?: HTMLElement | null } | Trigger emits when clicked; picker components listen |
Listen via editor.on('notionColorOpen', ...) or by wiring the relevant component (DomternalNotionColorPicker in each framework).
Registering a listener is also what makes the trigger appear: with no listener the bubble menu hides it, since pressing it could not do anything. Removing the last listener (editor.off('notionColorOpen')) hides it again on the next sync.
Hex/token mutual exclusion
Section titled “Hex/token mutual exclusion”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:
| Action | Effect |
|---|---|
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.
Last-action-wins for block conflicts
Section titled “Last-action-wins for block conflicts”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).
Cross-framework component
Section titled “Cross-framework component”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<domternal-notion-color-picker [editor]="editor()!" /><DomternalNotionColorPicker editor={editor} /><DomternalNotionColorPicker :editor="editor" />The components handle positioning, outside-click, keyboard navigation, and applying the token via editor.commands.setTextColorToken('blue') for the text half and editor.commands.setBackgroundColorToken('yellow') for the background half. Passing null to either command resets that half to the default.
All four panels render as role="dialog" with aria-label="Text and background color" and aria-modal="false": deliberately non-modal, because the picker does not trap focus (outside click closes it and the editor stays interactive). The “A” trigger button carries aria-haspopup="dialog".
CSS theme tokens
Section titled “CSS theme tokens”The theme stylesheet defines color values for each token:
| Variable | Purpose |
|---|---|
--dm-block-text-gray | Text color for the gray token |
--dm-block-text-brown | Text color for the brown token |
--dm-block-text-orange | etc. |
--dm-block-bg-gray | Background color for the gray token |
--dm-block-bg-brown | Background 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.
Picker UI CSS classes
Section titled “Picker UI CSS classes”.dm-ncp-trigger- the “A” trigger button rendered byBubbleMenu(identify it by class rather than caching the element: in the vanilla build the trigger survives a state-only render but is recreated when the bubble menu’s item list changes, while the React, Vue and Angular components render it in its own conditional slot that an item-list change leaves in place).dm-ncp-trigger-glyph- the “A” span inside the trigger, repainted to the selection’s current text color.dm-ncp-trigger-underline- the underline span inside the trigger, repainted to the selection’s current background color.dm-notion-color-picker- root panel container.dm-ncp-section- one section per color type (text / background).dm-ncp-label- the section heading (“Text color”, “Background color”).dm-ncp-grid- the swatch grid inside a section.dm-ncp-swatch- individual swatch button.dm-ncp-swatch--text/.dm-ncp-swatch--bg- variant modifiers on the swatch, styled perdata-color.dm-ncp-active- toggled on the swatch matching the selection’s current token
Exports
Section titled “Exports”import { NotionColorPicker, DEFAULT_NOTION_COLOR_PALETTE,} from '@domternal/core';import type { NotionColorPickerOptions, NotionColorPickerStorage,} from '@domternal/core';Source
Section titled “Source”@domternal/core - NotionColorPicker.ts
See also
Section titled “See also”- Text Color - inline text color via TextStyle
- Highlight - inline background color (text highlight)
- Block Color - block-level background and text colors