Skip to main content
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

Step 1: Create the Formatter Class

Extend CometChatTextFormatter and set the tracking character to #:
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
  }
}
The search method is called whenever the user types after the tracking character. Filter your data and set the suggestion list:
@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);
}

Step 3: Handle Suggestion Selection

When the user taps a suggestion, insert the hashtag into the composer:
@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);
}

Step 4: Style Hashtags in Message Bubbles

Override buildMessageBubbleSpan to apply styling to hashtags when they appear in sent/received messages:
@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;
}

Step 5: Pre-Send Hook (Optional)

Modify the message before it’s sent — for example, attach hashtag metadata:
@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;
}

Step 6: Register the Formatter

Pass your formatter to the components that should use it:
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,
  ],
)

Complete Example

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;
  }
}