> ## Documentation Index
> Fetch the complete documentation index at: https://www.cometchat.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Text Formatter

> Extend the CometChatTextFormatter base class to implement custom inline text patterns with regex and callbacks.

<Accordion title="AI Integration Quick Reference">
  | Field          | Value                                                                                                                                                                      |
  | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | Package        | `@cometchat/chat-uikit-react`                                                                                                                                              |
  | Key class      | `CometChatTextFormatter` (abstract base class for custom formatters)                                                                                                       |
  | Required setup | `CometChatUIKit.init(UIKitSettings)` then `CometChatUIKit.login("UID")`                                                                                                    |
  | Purpose        | Extend to create custom inline text patterns with regex, styling, and callbacks                                                                                            |
  | Features       | Text formatting, customizable styles, dynamic text replacement, input field integration, key event callbacks                                                               |
  | Sample app     | [GitHub](https://github.com/cometchat/cometchat-uikit-react/tree/v6/sample-app)                                                                                            |
  | Related        | [ShortCut Formatter](/ui-kit/react/shortcut-formatter-guide) \| [Mentions Formatter](/ui-kit/react/mentions-formatter-guide) \| [All Guides](/ui-kit/react/guide-overview) |
</Accordion>

`CometChatTextFormatter` is an abstract class for formatting text in the message composer and message bubbles. Extend it to build custom formatters — hashtags, keywords, or any regex-based pattern.

| Capability          | Description                                         |
| ------------------- | --------------------------------------------------- |
| Text formatting     | Auto-format text based on regex patterns and styles |
| Custom styles       | Set colors, fonts, and backgrounds for matched text |
| Dynamic replacement | Regex-based find-and-replace in user input          |
| Input integration   | Real-time monitoring of the composer input field    |
| Key event callbacks | Hooks for `keyUp` and `keyDown` events              |

<Warning>
  Always wrap formatted output in a `<span>` with a unique CSS class (e.g. `"custom-hashtag"`). This tells the UI Kit to render it as-is instead of sanitizing it.
</Warning>

***

## Steps

### 1. Import the base class

```javascript lines theme={null}
import { CometChatTextFormatter } from "@cometchat/chat-uikit-react";
```

### 2. Extend it

```javascript lines theme={null}
class HashTagTextFormatter extends CometChatTextFormatter {
  ...
}
```

### 3. Configure tracking character and regex

Set the character that triggers formatting, the regex to match, and the regex to strip formatting back to plain text.

```javascript lines theme={null}
this.setTrackingCharacter("#");
this.setRegexPatterns([/\B#(\w+)\b/g]);
this.setRegexToReplaceFormatting([
  /<span class="custom-hashtag" style="color: #30b3ff;">#(\w+)<\/span>/g,
]);
```

### 4. Set key event callbacks

```javascript lines theme={null}
this.setKeyUpCallBack(this.onKeyUp.bind(this));
this.setKeyDownCallBack(this.onKeyDown.bind(this));
```

### 5. Implement formatting methods

```typescript lines theme={null}
getFormattedText(inputText:string) { ... }
getOriginalText(inputText:string) { ... }
customLogicToFormatText(inputText: string) { ... }
```

***

## Example

A hashtag formatter used with [CometChatMessageList](/ui-kit/react/message-list) and [CometChatMessageComposer](/ui-kit/react/message-composer).

<Frame>
  <img src="https://mintcdn.com/cometchat-22654f5b/pIKiBC-6hdnqY01x/images/53d9b07c-custom_hashtag_formatter_web_screens-c7f853c807e9f2fa63e0e1f6245e0a27.png?fit=max&auto=format&n=pIKiBC-6hdnqY01x&q=85&s=f97a971ea8cec95ed207894125659834" width="1282" height="802" data-path="images/53d9b07c-custom_hashtag_formatter_web_screens-c7f853c807e9f2fa63e0e1f6245e0a27.png" />
</Frame>

<Tabs>
  <Tab title="HashTagTextFormatter.ts">
    ```ts lines theme={null}
    import { CometChatTextFormatter } from "@cometchat/chat-uikit-react";

    class HashTagTextFormatter extends CometChatTextFormatter {
      constructor() {
        super();
        this.setTrackingCharacter("#");
        this.setRegexPatterns([/\B#(\w+)\b/g]);
        this.setRegexToReplaceFormatting([/#(\w+)/g]);
        this.setKeyUpCallBack(this.onKeyUp.bind(this));
        this.setKeyDownCallBack(this.onKeyDown.bind(this));
        this.setReRender(() => {
          console.log("Re-rendering message composer to update text content.");
        });
        this.initializeComposerTracking();
      }

      initializeComposerTracking() {
        const composerInput = document.getElementById("yourComposerInputId");
        this.setInputElementReference(composerInput);
      }

      getCaretPosition(): number {
        if (!this.inputElementReference) return 0;
        const selection = window.getSelection();
        if (!selection || selection.rangeCount === 0) return 0;
        const range = selection.getRangeAt(0);
        const clonedRange = range.cloneRange();
        clonedRange.selectNodeContents(this.inputElementReference);
        clonedRange.setEnd(range.endContainer, range.endOffset);
        return clonedRange.toString().length;
      }

      setCaretPosition(position: number) {
        if (!this.inputElementReference) return;
        const range = document.createRange();
        const selection = window.getSelection();
        if (!selection) return;
        range.setStart(
          this.inputElementReference.childNodes[0] || this.inputElementReference,
          position
        );
        range.collapse(true);
        selection.removeAllRanges();
        selection.addRange(range);
      }

      onKeyUp(event: KeyboardEvent) {
        if (event.key === this.trackCharacter) {
          this.startTracking = true;
        }
        if (this.startTracking && (event.key === " " || event.key === "Enter")) {
          const caretPosition = this.getCaretPosition();
          this.formatText();
          this.setCaretPosition(caretPosition);
        }
        if (
          this.startTracking &&
          event.key !== " " &&
          event.key !== "Enter" &&
          this.getCaretPosition() === this.inputElementReference?.innerText?.length
        ) {
          this.startTracking = false;
        }
      }

      formatText() {
        const inputValue =
          this.inputElementReference?.innerText ||
          this.inputElementReference?.textContent ||
          "";
        const formattedText = this.getFormattedText(inputValue);
        if (this.inputElementReference) {
          this.inputElementReference.innerHTML = formattedText || "";
          this.reRender();
        }
      }

      onKeyDown(event: KeyboardEvent) {}

      getFormattedText(inputText: string) {
        if (!inputText) return;
        return this.customLogicToFormatText(inputText);
      }

      customLogicToFormatText(inputText: string) {
        return inputText.replace(
          /\B#(\w+)\b/g,
          '<span class="custom-hashtag" style="color: #5dff05;">#$1</span>'
        );
      }

      getOriginalText(inputText: string) {
        if (!inputText) return "";
        for (let i = 0; i < this.regexToReplaceFormatting.length; i++) {
          let regexPattern = this.regexToReplaceFormatting[i];
          if (inputText) {
            inputText = inputText.replace(regexPattern, "#$1");
          }
        }
        return inputText;
      }
    }

    export default HashTagTextFormatter;
    ```
  </Tab>

  <Tab title="MessageListDemo.tsx">
    Pass the formatter via the `textFormatters` prop.

    ```tsx lines theme={null}
    import { HashTagTextFormatter } from "./HashTagTextFormatter";

    export default function MessageListDemo() {
        const [chatUser, setChatUser] = React.useState<CometChat.User | undefined>();

        React.useEffect(() => {
            CometChat.getUser("uid").then((user) => {
                setChatUser(user);
            })
        }, [])

        return (
            <CometChatMessageList
                user={chatUser}
                textFormatters={[new HashTagTextFormatter()]}
            />
        );
    }
    ```
  </Tab>
</Tabs>

***

## Methods Reference

| Field                      | Setter                                       | Description                                                   |
| -------------------------- | -------------------------------------------- | ------------------------------------------------------------- |
| `trackCharacter`           | `setTrackingCharacter(char)`                 | Character that starts tracking (e.g. `#` for hashtags)        |
| `currentCaretPosition`     | `setCaretPositionAndRange(selection, range)` | Current selection set by the composer                         |
| `currentRange`             | `setCaretPositionAndRange(selection, range)` | Text range or cursor position set by the composer             |
| `inputElementReference`    | `setInputElementReference(element)`          | DOM reference to the composer input field                     |
| `regexPatterns`            | `setRegexPatterns(patterns)`                 | Regex patterns to match text for formatting                   |
| `regexToReplaceFormatting` | `setRegexToReplaceFormatting(patterns)`      | Regex patterns to strip formatting back to plain text         |
| `keyUpCallBack`            | `setKeyUpCallBack(fn)`                       | Callback for key up events                                    |
| `keyDownCallBack`          | `setKeyDownCallBack(fn)`                     | Callback for key down events                                  |
| `reRender`                 | `setReRender(fn)`                            | Triggers a re-render of the composer to update displayed text |
| `loggedInUser`             | `setLoggedInUser(user)`                      | Logged-in user object, set by composer and text bubbles       |
| `id`                       | `setId(id)`                                  | Unique identifier for the formatter instance                  |

<Warning>
  Don't modify `textContent` or `innerHTML` of the input element directly. Call `reRender` instead — the composer will invoke `getFormattedText` for all formatters in order.
</Warning>

***

## Override Methods

<Tabs>
  <Tab title="getFormattedText">
    Returns formatted HTML from input text, or edits at cursor position if `inputText` is null.

    ```js lines theme={null}
    getFormattedText(inputText: string | null, params: any): string | void {
      if (!inputText) {
        return; // edit at cursor position
      }
      return this.customLogicToFromatText(inputText);
    }
    ```
  </Tab>

  <Tab title="onKeyUp">
    Handles `keyup` events. Start tracking when the track character is typed.

    ```js lines theme={null}
    onKeyUp(event: KeyboardEvent) {
      if (event.key == this.trackCharacter) {
        this.startTracking = true;
      }
      if (this.startTracking && event.key == " ") {
        this.debouncedFormatTextOnKeyUp();
      }
    }
    ```
  </Tab>

  <Tab title="onKeyDown">
    Handles `keydown` events.

    ```js lines theme={null}
    onKeyDown(event: KeyboardEvent) {}
    ```
  </Tab>

  <Tab title="registerEventListeners">
    Called by the composer and text bubbles for each HTML element in the formatted string. Check for your formatter's CSS class before attaching listeners.

    ```js lines theme={null}
    registerEventListeners(element: HTMLElement, domTokenList: DOMTokenList) {
      let classList: string[] = Array.from(domTokenList);
      for (let i = 0; i < classList.length; i++) {
        if (classList[i] in this.mentionsCssClassMapping) {
          element.addEventListener("click", (event: Event) => {
            clearTimeout(this.timeoutID);
            CometChatUIEvents.ccMouseEvent.next({
              body: {
                CometChatUserGroupMembersObject:
                  this.mentionsCssClassMapping[classList[i]],
                message: this.messageObject ?? null,
                id: this.getId(),
              },
              event,
              source: MouseEventSource.mentions,
            });
          });

          element.addEventListener("mouseover", (event: Event) => {
            this.timeoutID = setTimeout(() => {
              this.mouseOverEventDispatched = true;
              CometChatUIEvents.ccMouseEvent.next({
                body: {
                  CometChatUserGroupMembersObject:
                    this.mentionsCssClassMapping[classList[i]],
                  message: this.messageObject ?? null,
                  id: this.getId(),
                },
                event,
                source: MouseEventSource.mentions,
              });
            }, CometChatUtilityConstants.MentionsFormatter.MENTIONS_HOVER_TIMEOUT);
          });

          element.addEventListener("mouseout", (event: Event) => {
            clearTimeout(this.timeoutID);
            if (this.mouseOverEventDispatched) {
              CometChatUIEvents.ccMouseEvent.next({
                body: {
                  CometChatUserGroupMembersObject:
                    this.mentionsCssClassMapping[classList[i]],
                  message: this.messageObject ?? null,
                  id: this.getId(),
                },
                event,
                source: MouseEventSource.mentions,
              });
              this.mouseOverEventDispatched = false;
            }
          });
        }
      }
      return element;
    }
    ```
  </Tab>

  <Tab title="getOriginalText">
    Strips formatting and returns plain text.

    ```js lines theme={null}
    getOriginalText(inputText: string | null | undefined): string {
      if (!inputText) return "";
      for (let i = 0; i < this.regexToReplaceFormatting.length; i++) {
        let regexPattern = this.regexToReplaceFormatting[i];
        if (inputText) {
          inputText = inputText.replace(regexPattern, "$1");
        }
      }
      return inputText;
    }
    ```
  </Tab>
</Tabs>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Mentions Formatter" href="/ui-kit/react/mentions-formatter-guide">
    Add @mentions with styled tokens.
  </Card>

  <Card title="Message Composer" href="/ui-kit/react/message-composer">
    Customize the message input component.
  </Card>

  <Card title="All Guides" href="/ui-kit/react/guide-overview">
    Browse all feature and formatter guides.
  </Card>

  <Card title="Sample App" href="https://github.com/cometchat/cometchat-uikit-react/tree/v6/sample-app">
    Full working sample application on GitHub.
  </Card>
</CardGroup>
