> ## Documentation Index
> Fetch the complete documentation index at: https://cometchat-22654f5b-docs-llms-scoped-indexes.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Text Formatters

> Transform plain text into formatted HTML in message bubbles, with built-in URL, mention, and markdown formatters plus custom formatters.

Text formatters detect patterns in message text and transform them into formatted HTML for display in bubbles. They run as a pipeline — each formatter receives the output of the previous one, sorted by priority (lower number = runs first). The Text plugin provides three built-in formatters, and you can add your own.

## Built-in Formatters

| Formatter | Priority | Detects | Output |
| - | - | - | - |
| `CometChatMarkdownFormatter` | 10 | `**bold**`, `_italic_`, `` `code` ``, `> quote`, lists, links | HTML tags (`<b>`, `<i>`, `<code>`, etc.) |
| `CometChatMentionsFormatter` | 20 | `<@uid:xxx>` tokens | Styled `@DisplayName` chips |
| `CometChatUrlFormatter` | 100 | `https://...`, `www.` | Clickable `<a>` links |

```
Raw text: "Hey **@Alice**, check https://example.com"
  ↓ MarkdownFormatter (priority 10)
"Hey <b>@Alice</b>, check https://example.com"
  ↓ MentionsFormatter (priority 20)
"Hey <b><span class="mention">@Alice</span></b>, check https://example.com"
  ↓ UrlFormatter (priority 100)
"Hey <b><span class="mention">@Alice</span></b>, check <a href="...">https://example.com</a>"
```

## The Formatter Interface

All formatters extend the abstract `CometChatTextFormatter` class. **`id` is its only abstract member** — everything else has a working default, so the smallest useful formatter is an `id` plus one transform method.

Porting from v6? Read this page for the class, then [Porting a v6 formatter](/ui-kit/react/migration-property-changes#porting-a-v6-formatter) for the members v6 had that v7 does not.

### Where your formatter runs

A formatter can take part in two independent pipelines. Display is the common case; live input is opt-in.

| Pipeline | What runs | Where it shows |
| - | - | - |
| **Display** (text → HTML) | `format()` → `getFormattedText()` → `customLogicToFormatText()` | Text and caption bubbles, conversation subtitles, reply and edit previews, saved messages, search results |
| **Live input** (composer typing) | The composer assigns `inputElementReference`, calls `initializeComposerTracking()`, then `onKeyUp()`/`onKeyDown()` on every keystroke — **only when [`enableRichTextEditor`](/ui-kit/react/components/message-composer#enablerichtexteditor) is set** | The message composer's editable input |

Formatters run in `priority` order (lower first), each receiving the previous one's output.

<Warning>
  **The built-ins run alongside yours.** Markdown (10), mentions (20) and URL (100) are always in the pipeline, so a formatter that matches what a built-in already produced will process it twice. A ported v6 URL formatter is the common case: both it and `CometChatUrlFormatter` match the same URL, and the second one rewrites the first one's `<a>` into broken markup. Drop your own URL handling in favour of the built-in, or give your formatter a pattern and `priority` that keep it clear of the built-ins' output.
</Warning>

<Warning>
  **Errors inside the display pipeline are caught, and your formatter is dropped.** Text bubbles log `console.warn("CometChatTextBubble: Error applying text formatter")` and skip it; conversation subtitles, reply and edit previews, saved messages and search results swallow the error silently. A formatter that renders nothing is usually a formatter that threw — check the console before assuming it was never registered.
</Warning>

### Display members

| Member | Called by | Default behaviour | Override when |
| - | - | - | - |
| `abstract readonly id: string` | the kit, to key the formatter map | none — you must declare it | always; see [Ids](#ids-must-be-unique) |
| `priority: number` | the kit, to order the pipeline | `100` | you need to run before or after a built-in |
| `shouldFormat(text, message?): boolean` | text bubbles, before formatting | `true` | you want to skip formatting for specific text. Two limits: only the message bubble checks it (subtitles and previews do not), and the kit always calls it with text alone — `message` is never populated |
| `format(text): string` | the kit, per render | stores `originalText`, returns `getFormattedText(text)` | you want full control of the transform |
| `getFormattedText(inputText?): string` | `format()` | with an argument, returns `customLogicToFormatText(inputText)`; without one, returns the last formatted text | rarely — prefer `customLogicToFormatText` |
| `customLogicToFormatText(text): string` | `getFormattedText()` | identity | **the usual override point** |
| `getRegex(): RegExp` | you, and some built-ins | first of `regexPatterns`, else a never-matching pattern | you match a single pattern |
| `getOriginalText(inputText?): string` | you | strips this formatter's markup using `regexToReplaceFormatting` (group 1 kept) | your markup needs custom stripping |
| `getMetadata()` / `metadata` | you | returns what you stored during formatting | you want to expose extracted data |
| `reset(): void` | you | clears `originalText`, `formattedText` and `metadata` | you keep extra state — call `super.reset()` |

The display pipeline never receives the message object or the logged-in user. A formatter sees text only.

### Live-input members

Only relevant if your formatter drives the composer's editable input. Display-only formatters ignore this section.

<Warning>
  **Live input requires `enableRichTextEditor` on the composer.** The composer binds formatters to its editor — assigning `inputElementReference`, calling `initializeComposerTracking()` and fanning keystrokes — only when the rich-text editor is active. Without that prop, `inputElementReference` stays `null`, `onKeyUp()`/`onKeyDown()` are never called and `formatText()` does nothing, with no error. Display formatting is unaffected and works either way.
</Warning>

| Member | Called by | Notes |
| - | - | - |
| `inputElementReference` | the composer assigns it | The contenteditable element. Do not set it by hand |
| `initializeComposerTracking()` | the composer, after assigning the reference | No-op by default. Override for one-time setup |
| `onKeyUp(event)` / `onKeyDown(event)` | the composer, every keystroke | Default forwards to the callbacks set by `setKeyUpCallBack()` / `setKeyDownCallBack()` |
| `formatText()` | the composer, when entering rich-text edit mode | Default replaces the input's `innerHTML` with `getFormattedText(text)`, which **flattens other formatting** such as bold and mentions. Override to edit only the matched text nodes |
| `getCaretPosition()` / `setCaretPosition(position)` | you | Kit-provided defaults; structure-aware, so they survive wrapping. Override only for exotic needs |
| `setTrackingCharacter()` / `trackCharacter` | you | The character that starts tracking, e.g. `#`. Starts empty |
| `setRegexPatterns()` / `getRegexPatterns()` | you | Patterns this formatter matches. Starts empty |
| `setRegexToReplaceFormatting()` | you | Patterns that strip your markup back to storable text |
| `setKeyUpCallBack()` / `setKeyDownCallBack()` | you | Register keystroke handlers |
| `setReRender()` / `reRender()` | you | Ask the composer to re-render after you mutate the DOM |
| `startTracking` | you | Convenience flag; the formatter manages it |

<Note>
  **v7 does not reformat the live input on keystrokes by itself.** The base `onKeyUp()` only forwards to your callback, and `formatText()` runs only on entering rich-text edit mode. To reformat as the user types, call `formatText()` from your own `onKeyUp()` override or from the callback you register.
</Note>

### The full class

Every member of the class, annotated with who calls it and what the default does. "Called by the kit" means the UI Kit invokes it on your formatter, so overriding it takes part in the pipeline; "called by you" means nothing invokes it for you.

```typescript theme={null}
abstract class CometChatTextFormatter {
  /** Pipeline order; lower runs first. Built-ins: markdown 10, mentions 20, URL 100. Default: 100. */
  priority: number;

  /** Unique identifier. The only abstract member — you must declare it. Keys the formatter map,
   *  so a duplicate id drops the earlier formatter and a built-in id replaces that built-in. */
  abstract readonly id: string;

  /** The text passed to the last format() call. Set for you by the default format(). */
  protected originalText: string;

  /** The result of the last format() call. Set for you by the default format(). */
  protected formattedText: string;

  /** Anything you extract while formatting (mentions, URLs, hashtags). Yours to populate. */
  protected metadata: Record<string, unknown>;

  // --- Display pipeline (text -> HTML) ---

  /** Called by the kit, per render. Default: stores originalText, returns getFormattedText(text).
   *  Override for full control of the transform. */
  format(text: string): string;

  /** Called by format(). With an argument, returns customLogicToFormatText(inputText); without one,
   *  returns the text from the last format(). Override to transform, but keep this exact signature:
   *  it must return a string, and v7 passes exactly one argument. */
  getFormattedText(inputText?: string): string;

  /** Called by getFormattedText(). Default: identity. The usual override point — put your
   *  text-to-HTML transform here. */
  customLogicToFormatText(text: string): string;

  /** Called by the kit from text bubbles only, before formatting; subtitles and previews skip it.
   *  Default: true. Override to skip formatting for specific text. NOTE: the kit calls this with
   *  text alone — the optional `message` is never passed, so it cannot be used for context. */
  shouldFormat(text: string, message?: CometChat.BaseMessage): boolean;

  /** Called by you, and by some built-ins. Default: the first entry of regexPatterns, or a
   *  never-matching pattern when none is set. Override when you match a single pattern. */
  getRegex(): RegExp;

  /** Called by you. With an argument, strips this formatter's markup back to storable text using
   *  regexToReplaceFormatting (group 1 kept); without one, returns the last original text. */
  getOriginalText(inputText?: string): string;

  /** Called by you. Returns whatever you stored in metadata while formatting. */
  getMetadata(): Record<string, unknown>;

  /** Called by you. Clears originalText, formattedText and metadata. Call super.reset() when you
   *  override it to clear your own state too. */
  reset(): void;

  // --- Live composer input (optional; display-only formatters ignore all of this) ---

  /** Assigned by the composer — the contenteditable input element. Do not set it by hand. */
  inputElementReference: HTMLElement | null;

  /** The character that starts a tracking session, e.g. '#'. Starts as '' — set it yourself. */
  trackCharacter: string;

  /** Convenience flag for "a tracking session is active". The formatter manages it. */
  startTracking: boolean;

  /** Patterns this formatter matches. Starts as [] — set via setRegexPatterns(). */
  protected regexPatterns: RegExp[];

  /** Patterns that strip this formatter's markup back to storable text (group 1 kept). */
  protected regexToReplaceFormatting: RegExp[];

  /** Callbacks you register; the default onKeyUp/onKeyDown/reRender delegate to them. */
  protected keyUpCallBack?: (event: KeyboardEvent) => void;
  protected keyDownCallBack?: (event: KeyboardEvent) => void;
  protected reRenderCallBack?: () => void;

  /** Called by the composer, after it assigns inputElementReference. No-op by default.
   *  Override for one-time setup. NOTE: the composer only binds formatters — this method,
   *  inputElementReference and the keystroke fan-out below — when enableRichTextEditor is set. */
  initializeComposerTracking(): void;

  /** Called by the composer on every keystroke. Default: forwards to the callback registered with
   *  setKeyUpCallBack() / setKeyDownCallBack(). Note v7 does NOT reformat as you type — call
   *  formatText() from here (or from your callback) if you want live formatting. */
  onKeyUp(event: KeyboardEvent): void;
  onKeyDown(event: KeyboardEvent): void;

  /** Called by the composer when it enters rich-text edit mode — never on keystrokes.
   *  Default: replaces the input's innerHTML with getFormattedText(text), which FLATTENS other
   *  formatting such as bold and mentions. Override to edit only the matched text nodes. */
  formatText(): void;

  /** Called by you. Kit-provided caret helpers: a character offset within the input, structure-aware
   *  so they stay correct after your wrapping introduces nested elements. */
  getCaretPosition(): number;
  setCaretPosition(position: number): void;

  /** Called by you, normally from your constructor, to configure the members above. */
  setInputElementReference(element: HTMLElement | null): void;
  setTrackingCharacter(character: string): void;
  setRegexPatterns(patterns: RegExp[]): void;
  getRegexPatterns(): RegExp[];
  setRegexToReplaceFormatting(patterns: RegExp[]): void;
  setKeyUpCallBack(callback: (event: KeyboardEvent) => void): void;
  setKeyDownCallBack(callback: (event: KeyboardEvent) => void): void;

  /** Register a re-render callback, then call reRender() after you mutate the input's DOM. */
  setReRender(callback: () => void): void;
  reRender(): void;
}
```

### Ids must be unique

The kit merges formatters into a map keyed by `id`. Two formatters with the same id leave only the last one, and reusing a built-in id — `markdown-formatter`, `mentions-formatter`, `url-formatter` — replaces that built-in. Plain JavaScript will run a formatter with no `id`, but every id-less formatter collapses onto the same key, so always declare one.

## Creating a Custom Formatter

Here's a hashtag formatter that wraps `#word` patterns in a styled span:

```typescript title="src/formatters/HashtagFormatter.ts" theme={null}
import { CometChatTextFormatter } from "@cometchat/chat-uikit-react";

export class HashtagFormatter extends CometChatTextFormatter {
  readonly id = "hashtag-formatter";
  override priority = 90; // After mentions (20), before URLs (100)

  private hashtags: string[] = [];

  getRegex(): RegExp {
    return /(#\w+)/g;
  }

  format(text: string): string {
    if (!text) {
      this.originalText = "";
      this.formattedText = "";
      this.hashtags = [];
      return "";
    }

    this.originalText = text;
    this.hashtags = [];

    this.formattedText = text.replace(this.getRegex(), (match) => {
      this.hashtags.push(match);
      return `<span class="cometchat-hashtag" style="color: #6851FF; font-weight: 500;">${match}</span>`;
    });

    this.metadata = { hashtags: this.hashtags };
    return this.formattedText;
  }

  /** Get detected hashtags from the last format() call. */
  getHashtags(): string[] {
    return [...this.hashtags];
  }

  override reset(): void {
    super.reset();
    this.hashtags = [];
  }
}
```

## Registering Custom Formatters

A custom formatter is not registered on its own — it must be wrapped in a custom **text plugin** (see [Plugins overview](/ui-kit/react/plugins/overview)) and that plugin registered via the provider's `plugins` prop (see [With Additional Plugins](/ui-kit/react/cometchat-provider#with-additional-plugins)).

Custom formatters are registered by creating a custom text plugin that provides them:

```typescript title="src/plugins/CustomTextPlugin.ts" theme={null}
import { CometChatTextPlugin } from "@cometchat/chat-uikit-react";
import type { CometChatTextFormatter } from "@cometchat/chat-uikit-react";
import {
  CometChatMarkdownFormatter,
  CometChatMentionsFormatter,
  CometChatUrlFormatter,
} from "@cometchat/chat-uikit-react";
import { HashtagFormatter } from "../formatters/HashtagFormatter";

export const CustomTextPlugin = {
  ...CometChatTextPlugin,
  id: "custom-text",

  getTextFormatters(): CometChatTextFormatter[] {
    return [
      new CometChatMarkdownFormatter(),   // priority 10
      new CometChatMentionsFormatter(),   // priority 20
      new HashtagFormatter(),             // priority 90
      new CometChatUrlFormatter(),        // priority 100
    ];
  },
};
```

Then pass it in your provider's `plugins` prop:

```tsx theme={null}
import { CometChatProvider } from "@cometchat/chat-uikit-react";
import { CustomTextPlugin } from "./plugins/CustomTextPlugin";

<CometChatProvider plugins={[CustomTextPlugin]}>
  <MyChatApp />
</CometChatProvider>
```

Since user plugins are prepended before the defaults in the registry, your custom text plugin takes precedence over the built-in one (first match wins).

## Formatter Details

### CometChatMarkdownFormatter

Converts markdown syntax to HTML. Runs first (priority 10) so subsequent formatters operate on HTML output.

* `**bold**` → `<b>bold</b>`
* `_italic_` → `<i>italic</i>`
* `__underline__` or `++underline++` → `<u>underline</u>`
* `~~strikethrough~~` → `<s>strikethrough</s>`
* `` `inline code` `` → `<code>inline code</code>`
* ` ```code block``` ` → `<pre><code>code block</code></pre>`
* `> blockquote` → `<blockquote>blockquote</blockquote>`
* `[text](url)` → `<a href="url">text</a>`
* `1. item` → ordered list; `• item` / `- item` → unordered list

### CometChatMentionsFormatter

Resolves SDK mention tokens (`<@uid:xxx>` and `<@all:label>`) into styled mention chips. Requires the mentioned-users list from the message to resolve UIDs to display names.

### CometChatUrlFormatter

Detects bare URLs (`https://...` and `www.`) and wraps them in clickable `<a>` tags with `target="_blank"` and `rel="noopener noreferrer"`. Protects existing markdown links and `<a>` tags from double-processing.

## Tips

* **Priority matters** — markdown must run before mentions/URLs so it doesn't break HTML tags
* **Protect code blocks** — formatters should skip content inside `<code>` and `<pre>` tags
* **Keep it fast** — formatters run on every text message render; avoid expensive operations
* **Use `shouldFormat()`** — override to skip formatting for specific messages
* **Store metadata** — use `this.metadata` to expose extracted data (URLs, hashtags, mentions) to consumers


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.