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

# Custom Text Formatter Guide

> Build a CometChat Flutter UI Kit text formatter for hashtags, links, tracked characters, composer suggestions, and message styling.

This guide walks you through building a custom `CometChatTextFormatter` that detects `#hashtags` in messages, highlights them, and shows a suggestion dropdown in the composer.

## Prerequisites

* CometChat Flutter UI Kit V6 installed
* Familiarity with [Text Formatters](/ui-kit/flutter/customization-text-formatters)

## Step 1: Create the Formatter Class

Extend `CometChatTextFormatter` and set the tracking character to `#`:

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    import 'package:cometchat_chat_uikit/cometchat_chat_uikit.dart';
    import 'package:flutter/material.dart';

    class HashtagFormatter extends CometChatTextFormatter {
      final List<String> _allHashtags = [
        'flutter', 'dart', 'cometchat', 'uikit', 'mobile',
        'android', 'ios', 'web', 'chat', 'messaging',
      ];

      HashtagFormatter() : super(trackingCharacter: '#');

      @override
      void init() {
        // Called when the formatter is initialized
      }
    }
    ```
  </Tab>
</Tabs>

## Step 2: Implement Search

The `search` method is called whenever the user types after the tracking character. Filter your data and set the suggestion list:

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    @override
    void search(String query) {
      if (query.isEmpty) {
        setSuggestionItems(_allHashtags
            .map((tag) => SuggestionItem(
                  id: tag,
                  name: '#$tag',
                  leadingIcon: Icon(Icons.tag, size: 20),
                ))
            .toList());
        return;
      }

      final filtered = _allHashtags
          .where((tag) => tag.toLowerCase().contains(query.toLowerCase()))
          .map((tag) => SuggestionItem(
                id: tag,
                name: '#$tag',
                leadingIcon: Icon(Icons.tag, size: 20),
              ))
          .toList();

      setSuggestionItems(filtered);
    }
    ```
  </Tab>
</Tabs>

## Step 3: Handle Suggestion Selection

When the user taps a suggestion, insert the hashtag into the composer:

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    @override
    void onItemClick(SuggestionItem item, User? user, Group? group) {
      // The base class handles inserting the text into the composer.
      // You can add custom logic here, like tracking analytics.
      super.onItemClick(item, user, group);
    }
    ```
  </Tab>
</Tabs>

## Step 4: Style Hashtags in Message Bubbles

Override `buildMessageBubbleSpan` to apply styling to hashtags when they appear in sent/received messages:

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    @override
    List<CometChatTextFormatterResult> getFormattedText(
      String text,
      BuildContext context,
      BubbleAlignment alignment,
    ) {
      final results = <CometChatTextFormatterResult>[];
      final regex = RegExp(r'#\w+');

      for (final match in regex.allMatches(text)) {
        results.add(CometChatTextFormatterResult(
          start: match.start,
          end: match.end,
          style: TextStyle(
            color: alignment == BubbleAlignment.right
                ? Colors.white.withOpacity(0.8)
                : Color(0xFF6851D6),
            fontWeight: FontWeight.w600,
          ),
          onTap: () {
            // Handle hashtag tap — navigate to hashtag feed, etc.
            debugPrint('Tapped hashtag: ${match.group(0)}');
          },
        ));
      }

      return results;
    }
    ```
  </Tab>
</Tabs>

## Step 5: Pre-Send Hook (Optional)

Modify the message before it's sent — for example, attach hashtag metadata:

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    @override
    BaseMessage handlePreMessageSend(BaseMessage message) {
      if (message is TextMessage) {
        final regex = RegExp(r'#\w+');
        final hashtags = regex
            .allMatches(message.text)
            .map((m) => m.group(0)!.substring(1))
            .toList();

        if (hashtags.isNotEmpty) {
          message.metadata ??= {};
          message.metadata!['hashtags'] = hashtags;
        }
      }
      return message;
    }
    ```
  </Tab>
</Tabs>

## Step 6: Register the Formatter

Pass your formatter to the components that should use it:

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    final hashtagFormatter = HashtagFormatter();

    // On the message list (for rendering)
    CometChatMessageList(
      user: user,
      textFormatters: [
        CometChatMentionsFormatter(), // Keep default mentions
        hashtagFormatter,
      ],
    )

    // On the composer (for input suggestions)
    CometChatMessageComposer(
      user: user,
      textFormatters: [
        CometChatMentionsFormatter(),
        hashtagFormatter,
      ],
    )
    ```
  </Tab>
</Tabs>

## Complete Example

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    class HashtagFormatter extends CometChatTextFormatter {
      final List<String> _allHashtags = [
        'flutter', 'dart', 'cometchat', 'uikit', 'mobile',
      ];

      HashtagFormatter() : super(trackingCharacter: '#');

      @override
      void init() {}

      @override
      void search(String query) {
        final filtered = query.isEmpty
            ? _allHashtags
            : _allHashtags.where(
                (tag) => tag.toLowerCase().contains(query.toLowerCase()),
              ).toList();

        setSuggestionItems(filtered
            .map((tag) => SuggestionItem(
                  id: tag,
                  name: '#$tag',
                  leadingIcon: Icon(Icons.tag, size: 20),
                ))
            .toList());
      }

      @override
      List<CometChatTextFormatterResult> getFormattedText(
        String text,
        BuildContext context,
        BubbleAlignment alignment,
      ) {
        final results = <CometChatTextFormatterResult>[];
        final regex = RegExp(r'#\w+');

        for (final match in regex.allMatches(text)) {
          results.add(CometChatTextFormatterResult(
            start: match.start,
            end: match.end,
            style: TextStyle(
              color: alignment == BubbleAlignment.right
                  ? Colors.white.withOpacity(0.8)
                  : Color(0xFF6851D6),
              fontWeight: FontWeight.w600,
            ),
          ));
        }
        return results;
      }

      @override
      BaseMessage handlePreMessageSend(BaseMessage message) {
        if (message is TextMessage) {
          final hashtags = RegExp(r'#\w+')
              .allMatches(message.text)
              .map((m) => m.group(0)!.substring(1))
              .toList();
          if (hashtags.isNotEmpty) {
            message.metadata ??= {};
            message.metadata!['hashtags'] = hashtags;
          }
        }
        return message;
      }
    }
    ```
  </Tab>
</Tabs>

## Related

* [Text Formatters](/ui-kit/flutter/customization-text-formatters) — Text formatter API reference.
* [Mentions Formatter Guide](/ui-kit/flutter/mentions-formatter-guide) — Built-in mentions formatter.
* [Shortcut Formatter Guide](/ui-kit/flutter/shortcut-formatter-guide) — Shortcut text replacement.
