Skip to main content

Goal

By the end of this guide you will have a text color formatter, working end to end:
  • Composer: a toolbar button picks a color. Text you type next is colored as you type, and selected text can be colored or cleared.
  • Send: colored text is stored on the message as a plain-text token, {color:#e5484d}text{/color}.
  • Display: the token renders as colored text in message bubbles, the conversation list, thread headers, pinned and saved messages, search, reply and edit previews, and copied text is plain.
Each step below explains one part of the formatter with an excerpt. The complete code at the end is ready to drop into your app.
For the built-in Markdown, Mentions, and URL formatters and the full CometChatTextFormatter API, see Text Formatters.

Prerequisites

  • Completed the Integration Guide
  • A chat screen using CometChatMessageList and CometChatMessageComposer
  • React 18 or later (the toolbar button uses useSyncExternalStore)

How It Works

A formatter has three jobs, and ColorFormatter does all three: The composer gives each formatter in its textFormatters a reference to the editor element (inputElementReference), forwards every keyup to onKeyUp, and supplies caret helpers (getCaretPosition, setCaretPosition) and reRender(), which syncs the composer after the formatter changes the editor’s DOM.

Step 1: Token and Display

The token is plain text, so it survives storage and delivery unchanged. customLogicToFormatText turns it into a colored span. You don’t need to override format(): the base class calls customLogicToFormatText from it, so every display surface uses this method. File: src/formatters/ColorFormatter.ts (excerpt)
Formatters run in priority order, lowest first. Markdown runs at 10, mentions at 20, URLs at 100. At 60, the color token is replaced after markdown and mentions have already been rendered inside it.

Step 2: Serialize on Send

On send, the composer passes the editor’s HTML through each formatter’s getOriginalText before converting it to markdown. ColorFormatter replaces each colored span with the token. It walks the DOM instead of using a regex because a colored run can contain other elements. If you insert a mention while typing in color, the mention’s own </span> would end a non-greedy <span …>…</span> match early and leave a broken token in the message.
The composer also runs getOriginalText and then customLogicToFormatText around its paste sanitizer, so colored text copied from one message and pasted into the composer keeps its color. You get this for free.

Step 3: Color Text as You Type

The formatter works like a highlighter pen. setActiveColor turns the pen on (or off, with null), and formatText wraps newly typed text in a span of the pen’s color after each keystroke.
A few details keep typing correct:
  • Only newly typed text is colored. lastCaret records the caret after each keystroke or click, so formatText colors exactly the characters typed since, including a quick burst typed before a keyup arrived, and never text that was already there.
  • Changing color mid-word. The browser keeps inserting into the span the caret is in. When the pen’s color differs from that span, or the pen was turned off inside it (breakoutPending), the typed character is moved out of the span. Text after the caret keeps its color.
  • Protected content. Mentions and other non-editable nodes, code, and links are never colored. Pass protectedSelectors to the constructor to protect more, such as another formatter’s spans.
  • IME input is skipped until the composition is committed.
initializeComposerTracking runs once the composer has assigned the editor. It keeps lastCaret in sync on mouse clicks and turns the pen off when the input is cleared, for example after a message is sent.

Step 4: Color a Selection

applyColorToSelection colors the selected text, replacing any color it already has. clearColorInSelection removes it. Both split existing runs so text outside the selection keeps its color, and both skip protected content.
For the button in the next step, the formatter also exposes onColorChange and getCaretColor, shaped for React’s useSyncExternalStore, so the button can show the color at the caret.

Step 5: The Toolbar Button

The composer renders toolbarTrailingView at the end of the rich-text toolbar. ColorPickerButton puts two controls there:
  • A opens the native color picker. Picking a color colors the current selection (if any) and turns the pen on.
  • ⌫ clears color from the selection, or turns the pen off when nothing is selected.
File: src/formatters/ColorPickerButton.tsx (excerpt)
onMouseDown={(e) => e.preventDefault()} keeps focus in the editor. The native color picker still takes focus when it opens, which is why the selection is saved on mousedown and restored when a color is picked.

Step 6: Wire It Into the Composer

Create one ColorFormatter instance and pass it to both the composer’s textFormatters and the button. The button drives the same instance the composer bound to its editor. Live input requires enableRichTextEditor. File: ChatScreen.tsx
Registering the formatter on the composer also colors its reply and edit previews.

Step 7: Register It on Every Surface

A formatter only applies where it is registered. If a surface doesn’t have it, readers see the raw {color:…} token there. Pass it to every component that shows message text: A thread panel has its own message list and composer, so register the formatter there too.
Display-only surfaces can use their own instance. The shared instance matters only for a composer and its button: each composer (main chat, thread) needs its own formatter and its own button.
The token is plain text, so every client that shows these messages has to render it. If your users also chat from other platforms, use the same {color:#hex}…{/color} token there, or they will see it as raw text.

Complete Code

Drop these two files into your app, then wire them up as in Step 6 and Step 7.

Next Steps