Skip to content

Text Color

TextColor adds text color styling via the TextStyle carrier mark. It provides a 25-color palette (5x5 grid), setTextColor and unsetTextColor commands, and a toolbar dropdown with color swatches and a color indicator bar. The palette is fully customizable.

Not included in StarterKit. Add it separately.

Requires the TextStyle mark, which is automatically included as a dependency.

Select some text and pick a color from the palette dropdown. The indicator bar under the icon shows the active color. Click “Default” to reset to the browser default color.

With the default theme. The toolbar shows a color palette dropdown with 25 swatches and a reset button.

Click to try it out
import { Editor, Document, Paragraph, Text, TextColor } from '@domternal/core';
import '@domternal/theme';
const editor = new Editor({
element: document.getElementById('editor')!,
extensions: [Document, Paragraph, Text, TextColor],
content: '<p>Select text and pick a color from the toolbar palette.</p>',
});

With the full theme and ToolbarController, TextColor renders as a dropdown with a 5x5 color palette grid, a “Default” reset button, and a color indicator bar on the trigger icon.

OptionTypeDefaultDescription
colorsstring[]DEFAULT_TEXT_COLORS (25 colors)Color values for the palette. Pass a custom array to use specific colors.
columnsnumber5Number of columns in the palette grid layout
TextColor.configure({
colors: ['#e03131', '#f08c00', '#2f9e44', '#1971c2', '#7048e8'],
columns: 5,
})

The default palette is a 5x5 grid organized by color family (columns) and intensity (rows):

RowDescriptionColors
1Neutrals#000000 #595959 #a6a6a6 #d9d9d9 #ffffff
2Pastel tints#ffc9c9 #fff3bf #b2f2bb #a5d8ff #d0bfff
3Vivid / saturated#e03131 #f08c00 #2f9e44 #1971c2 #7048e8
4Medium shades#ff6b6b #ffd43b #69db7c #4dabf7 #9775fa
5Dark shades#c92a2a #e67700 #2b8a3e #1864ab #6741d9

Columns are: Red, Orange/Yellow, Green, Blue, Purple.

You can import the default palette to extend it:

import { TextColor, DEFAULT_TEXT_COLORS } from '@domternal/core';
TextColor.configure({
colors: [...DEFAULT_TEXT_COLORS, '#ff69b4', '#00ced1'], // add pink and teal
})

Passing an empty colors array disables the toolbar dropdown entirely. No toolbar items are registered.

TextColor.configure({ colors: [] }) // no toolbar, commands only

Sets the text color on the current selection. Accepts any CSS color value as a string.

editor.commands.setTextColor('#e03131');
// With chaining
editor.chain().focus().setTextColor('#1971c2').run();

Under the hood, this calls setMark('textStyle', { color }) to apply the color attribute on the TextStyle carrier mark.

Removes the text color from the current selection, resetting it to the browser default. Also cleans up empty <span> wrappers via removeEmptyTextStyle().

editor.commands.unsetTextColor();

The cleanup step is important: if you set a color and then unset it, the color attribute becomes null. If no other TextStyle attributes are active (no font-family, font-size, or background-color), the <span> wrapper is removed entirely to keep the HTML clean.

TextColor does not register any keyboard shortcuts.

TextColor does not register any input rules.

TextColor registers a dropdown with grid layout in the toolbar.

DropdownIconGroupPriorityLayoutGrid columns
textColortextAUnderlinetextStyle200grid5 (configurable)

The dropdown contains 26 items: 1 reset button + 25 color swatches.

ItemCommandIconLabel
unsetTextColorunsetTextColorprohibitDefault

The reset button spans the full width of the grid and removes the text color.

Each color in the colors array becomes a swatch button:

PropertyValue
nametextColor-{hex} (e.g., textColor-#e03131)
commandsetTextColor
commandArgs[color]
isActive{ name: 'textStyle', attributes: { color } }
colorThe hex color value (used for swatch rendering)

Active swatches show a checkmark overlay with a ring border.

The dropdown trigger shows a color indicator bar below the icon. This bar reflects the currently active text color. When no color is active, it shows the defaultIndicatorColor (#000000 / black). The indicator updates as the cursor moves through differently-colored text.

TextColor does not define its own mark in the ProseMirror schema. Instead, it injects a color attribute into the existing TextStyle mark using addGlobalAttributes():

addGlobalAttributes() {
return [{
types: ['textStyle'],
attributes: {
color: {
default: null,
parseHTML: (element) => normalizeColor(element.style.color) || null,
renderHTML: (attributes) => attributes.color ? { style: `color: ${attributes.color}` } : null,
},
},
}];
}

This means TextColor, FontFamily, FontSize, and Highlight all share the same <span> wrapper. A span with multiple styles looks like:

<span style="color: #e03131; font-family: Georgia; font-size: 18px">styled text</span>

TextColor declares TextStyle as both a dependency (dependencies: ['textStyle']) and includes it via addExtensions(). You do not need to add TextStyle separately.

When parsing HTML, browsers convert hex colors to rgb() format in element.style.color. The normalizeColor() helper converts these back to hex:

// Browser returns: "rgb(224, 49, 49)"
// normalizeColor converts to: "#e03131"
normalizeColor('rgb(224, 49, 49)') // → '#e03131'

This normalization is critical for active state detection. Without it, the stored color (#e03131) would never match the parsed color (rgb(224, 49, 49)), and the swatch checkmark would not appear.

The helper extracts RGB values with a regex, converts each to a two-digit hex string, and returns #RRGGBB. Non-RGB values (hex strings, named colors) pass through unchanged. The alpha channel in rgba() is ignored.

When you call unsetTextColor(), it:

  1. Sets the color attribute to null via setMark('textStyle', { color: null })
  2. Calls removeEmptyTextStyle() to check if the TextStyle mark has any remaining non-null attributes
  3. If all attributes are null (no color, no font-family, no font-size, no background-color), removes the <span> wrapper entirely

This two-step process prevents empty <span> elements from accumulating in the document.

For empty selections (collapsed cursor), it checks stored marks instead of document marks, preventing future typed text from getting a meaningless <span> wrapper.

Colors render as inline styles on <span> elements:

<!-- With color -->
<span style="color: #e03131">Red text</span>
<!-- No color (default) - no span wrapper -->
Plain text

When the color attribute is null, renderHTML returns null for the style, so no color property is added to the span.

import { TextColor, DEFAULT_TEXT_COLORS } from '@domternal/core';
import type { TextColorOptions } from '@domternal/core';
ExportTypeDescription
TextColorExtensionThe text color extension
DEFAULT_TEXT_COLORSstring[]The default 25-color palette array
TextColorOptionsTypeScript typeOptions for TextColor.configure()

@domternal/core - TextColor.ts