Field Value Package @cometchat/chat-uikit-reactKey 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 Related ShortCut Formatter | Mentions Formatter | All Guides
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
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.
Steps
1. Import the base class
import { CometChatTextFormatter } from "@cometchat/chat-uikit-react" ;
2. Extend it
class HashTagTextFormatter extends CometChatTextFormatter {
...
}
Set the character that triggers formatting, the regex to match, and the regex to strip formatting back to plain text.
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
this . setKeyUpCallBack ( this . onKeyUp . bind ( this ));
this . setKeyDownCallBack ( this . onKeyDown . bind ( this ));
getFormattedText ( inputText : string ) { ... }
getOriginalText ( inputText : string ) { ... }
customLogicToFormatText ( inputText : string ) { ... }
Example
A hashtag formatter used with CometChatMessageList and CometChatMessageComposer .
HashTagTextFormatter.ts
MessageListDemo.tsx
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 ;
Pass the formatter via the textFormatters prop. 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 ()] }
/>
);
}
Methods Reference
Field Setter Description trackCharactersetTrackingCharacter(char)Character that starts tracking (e.g. # for hashtags) currentCaretPositionsetCaretPositionAndRange(selection, range)Current selection set by the composer currentRangesetCaretPositionAndRange(selection, range)Text range or cursor position set by the composer inputElementReferencesetInputElementReference(element)DOM reference to the composer input field regexPatternssetRegexPatterns(patterns)Regex patterns to match text for formatting regexToReplaceFormattingsetRegexToReplaceFormatting(patterns)Regex patterns to strip formatting back to plain text keyUpCallBacksetKeyUpCallBack(fn)Callback for key up events keyDownCallBacksetKeyDownCallBack(fn)Callback for key down events reRendersetReRender(fn)Triggers a re-render of the composer to update displayed text loggedInUsersetLoggedInUser(user)Logged-in user object, set by composer and text bubbles idsetId(id)Unique identifier for the formatter instance
Don’t modify textContent or innerHTML of the input element directly. Call reRender instead — the composer will invoke getFormattedText for all formatters in order.
Override Methods
getFormattedText
onKeyUp
onKeyDown
registerEventListeners
getOriginalText
Returns formatted HTML from input text, or edits at cursor position if inputText is null. getFormattedText ( inputText : string | null , params : any ): string | void {
if ( ! inputText ) {
return ; // edit at cursor position
}
return this . customLogicToFromatText ( inputText );
}
Handles keyup events. Start tracking when the track character is typed. onKeyUp ( event : KeyboardEvent ) {
if ( event . key == this . trackCharacter ) {
this . startTracking = true ;
}
if ( this . startTracking && event . key == " " ) {
this . debouncedFormatTextOnKeyUp ();
}
}
Handles keydown events. onKeyDown ( event : KeyboardEvent ) {}
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. 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 ;
}
Strips formatting and returns plain text. 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 ;
}
Next Steps