Skip to content

Block Color

BlockColor adds block-level background and text colors as global attributes. The colors persist across turnIntoBlock transformations (the heading-to-paragraph swap keeps its tint), and integrate with BlockContextMenu’s Colors picker so the user can apply colors via the right-click menu.

This complements NotionColorPicker, which handles inline color (per-character tints via textStyle). Use BlockColor for whole-block tints (e.g. an entire paragraph or list item highlighted).

Not included in StarterKit.

Use BlockColor when you need:

  • Block-level background or text color (color applies to a whole paragraph, heading, or callout)
  • Configurable target node types (e.g. allow background-color on paragraph but not on heading)
  • Palette overrides for brand-specific colors

Skip it if:

  • Your coloring needs are inline-only (use TextColor and Highlight instead)
  • You enforce a strict typographic design without per-block coloring
import { Editor, StarterKit, BlockColor } from '@domternal/core';
import '@domternal/theme';
const editor = new Editor({
element: document.getElementById('editor')!,
extensions: [
StarterKit,
BlockColor,
],
});
// Apply a background color to the block at the cursor
editor.chain().focus().setBlockBgColor('blue').run();
BlockColor.configure({
types: DEFAULT_BLOCK_COLOR_TYPES,
bgColors: DEFAULT_BLOCK_COLORS,
textColors: DEFAULT_BLOCK_COLORS,
})
OptionTypeDefaultDescription
typesstring[]DEFAULT_BLOCK_COLOR_TYPESNode types that receive bgColor / textColor global attributes
bgColorsstring[]DEFAULT_BLOCK_COLORSPalette used by setBlockBgColor. Commands reject values outside this list (no-op + returns false) so host apps can curate what’s selectable.
textColorsstring[]DEFAULT_BLOCK_COLORSPalette used by setBlockTextColor

Passing an empty array for bgColors or textColors falls back to DEFAULT_BLOCK_COLORS rather than emptying the picker, since a Colors section containing only the reset swatch is a broken UI. Note this is the opposite of FontFamily, where an empty fontFamilies array does disable the toolbar dropdown. To hide the Colors section entirely, use BlockContextMenu.configure({ blockColorEnabled: false }) (see Integration with BlockContextMenu).

['paragraph', 'heading', 'blockquote', 'bulletList', 'orderedList', 'taskList', 'listItem', 'taskItem']

codeBlock is intentionally excluded - <pre><code> already has its own background that would clash visually. Details-content blocks similarly have their own affordance.

['gray', 'brown', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink', 'red']

Same 9-color palette as NotionColorPicker, so inline and block tints share the same color set. 'default' is implicit - represented by null (no data-* attribute).

The extension adds two global attributes (parse/render on every type in types):

AttributeParsesRendersMeaning
bgColordata-bg-color="<token>"data-bg-color="<token>"Block background color
textColordata-text-color="<token>"data-text-color="<token>"Block text color

The theme stylesheet maps these data attributes to CSS custom properties:

[data-bg-color="blue"] { background-color: var(--dm-block-bg-blue); }
[data-text-color="red"] { color: var(--dm-block-text-red); }
declare module '@domternal/core' {
interface RawCommands {
setBlockBgColor: CommandSpec<[color: string | null]>;
setBlockTextColor: CommandSpec<[color: string | null]>;
unsetBlockColors: CommandSpec;
}
}
CommandSignatureBehavior
setBlockBgColor(color: string | null)Sets bgColor on the nearest ancestor block of the cursor whose type is in types (a single block; multi-block selections are not applied across the range). null clears. Rejects values outside bgColors palette (returns false).
setBlockTextColor(color: string | null)Same for textColor, rejects values outside textColors palette
unsetBlockColors()Clears both bgColor and textColor on that same single block
editor.chain().focus().setBlockBgColor('green').run(); // set
editor.chain().focus().setBlockTextColor(null).run(); // clear
editor.chain().focus().unsetBlockColors().run(); // clear both
editor.chain().focus().setBlockBgColor('notacolor').run(); // false - palette guard rejects

When the user changes a block’s type via “Turn into” (e.g. heading -> paragraph), BlockContextMenu’s turnIntoBlock helper transforms attrs through a custom function so global attrs survive. Without this preservation, every block-type change would silently reset the color.

// User has: <p data-bg-color="blue">text</p>
// User clicks "Turn into > Heading 1"
// Result: <h1 data-bg-color="blue">text</h1> <-- color persists

Conflict resolution: stripInlineColorConflicts

Section titled “Conflict resolution: stripInlineColorConflicts”

All three commands strip conflicting inline colors themselves, in the same transaction that writes the node attribute: last action wins, so a block tint is never masked by an inline color already sitting on that block’s text. This happens whether or not BlockContextMenu is loaded, so applying a block color programmatically erases the inline counterpart across the target block’s whole range.

Commandwhich argumentInline textStyle attributes cleared
setBlockTextColor'text'color, colorToken
setBlockBgColor'bg'backgroundColor, backgroundColorToken
unsetBlockColors'both'all four

The pass is also exported, because BlockContextMenu writes the node attributes directly with setNodeMarkup (it already knows the block position, so it bypasses the commands’ ancestor walk) and has to share the same behavior:

import { stripInlineColorConflicts } from '@domternal/core';
function stripInlineColorConflicts(
tr: Transaction,
state: EditorState,
from: number,
to: number,
which: 'text' | 'bg' | 'both',
): void;

Strips inline textStyle marks inside [from, to] that carry the inline counterpart of a block-level color attribute. Mutates the transaction in place. 'both' handles the unset case (unsetBlockColors). A textStyle mark carrying other attributes as well (font family, font size) is removed and re-added with only the conflicting keys nulled, so clearing a color does not take the rest of the mark with it.

When both BlockColor and BlockContextMenu are loaded, the context menu’s Colors section appears automatically for blocks of types in BlockColor.options.types. The Colors row shows two ribbons (background + text) with the full palette plus a “default” swatch (clear).

Toggle off via BlockContextMenu.configure({ blockColorEnabled: false }).

Block colors render only as data attributes; the theme stylesheet picks them up via attribute selectors. No new classes are added directly by this extension.

Defined in @domternal/theme for both light and dark modes:

VariablePurpose
--dm-block-bg-grayBackground for data-bg-color="gray"
--dm-block-bg-brownBackground for data-bg-color="brown"
--dm-block-bg-orange etc.Backgrounds for each palette token
--dm-block-text-grayText color for data-text-color="gray"
--dm-block-text-brown etc.Text colors for each palette token

Override these in your app’s CSS to customize the palette colors.

import {
BlockColor,
DEFAULT_BLOCK_COLORS,
DEFAULT_BLOCK_COLOR_TYPES,
stripInlineColorConflicts,
} from '@domternal/core';
import type { BlockColorOptions } from '@domternal/core';

@domternal/core - BlockColor.ts