# Custom Message Menu

The message menu appears on long-press. It lets users perform actions (reply, delete, etc.) and add/view reactions.

## Best Practices

- Reuse default menu subcomponents to keep behavior consistent and reduce maintenance.
- Keep the menu accessible: large tap targets and clear labels for destructive actions.
- Use `messageContentOrder` to mirror the main message layout inside the menu.
- Load reactions lazily to avoid long menu open times on large threads.
- Dismiss the overlay after an action to avoid stale state.

## How to build a custom message menu

To build a custom message menu, you need to override the `MessageMenu` component in the [`Channel`](/chat/docs/sdk/react-native/v8/core-components/channel/) component.

```tsx
import { Text, View } from "react-native";
import { MessageMenu } from "stream-chat-react-native";

const CustomMessageMenu = () => {
  return (
    <View>
      <Text>Custom Message Menu</Text>
    </View>
  );
};

const App = () => {
  return (
    <Channel MessageMenu={CustomMessageMenu}>
      {/** MessageList and MessageInput component here */}
    </Channel>
  );
};
```

### Example

Example: a custom message menu with a reactions picker and message actions.

![Custom Message Menu](@chat-sdk/react-native/v8/_assets/ui-cookbook/custom-message-menu/custom-message-menu.png)

For this example, we use default components wherever possible to keep the guide simple.
We used the `MessageActionListItem`, `MessageUserReactionsAvatar` and `MessageReactionPicker` components with the default styles and wrapped them in a `View` component with the styles for the actions container and the reactions container.

Message actions are provided by the `MessageActionsList` props.

`MessageUserReactionsItem` comes from `useMessagesContext`. Reactions are fetched via `useFetchReactions`.

We render the long-pressed message/attachments using default components.

Use `messageContentOrder` from `useMessagesContext` to determine which message parts to render.

For text, we use `MessageTextContainer`.

For attachments, use `Attachment` and `otherAttachments` from `useMessageContext`.

For quoted replies, use `Reply` and `message.quoted_message` from `useMessageContext`.

For galleries, use `Gallery` and `images` from `useMessageContext`.

For files, use `FileAttachmentGroup` and `files` from `useMessageContext`.

<admonition type="note">
The styles are added to have the message menu in the center of the screen and have the message bubble style.
</admonition>

```tsx
import {
  FlatList,
  Modal,
  StyleSheet,
  Text,
  TouchableWithoutFeedback,
  View,
} from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import {
  Attachment,
  FileAttachmentGroup,
  Gallery,
  MessageActionListItem,
  MessageActionListProps,
  MessageAvatar,
  MessageMenuProps,
  MessageReactionPicker,
  MessageTextContainer,
  MessageUserReactionsAvatar,
  MessageUserReactionsProps,
  Poll,
  Reaction,
  Reply,
  ReplyProps,
  StreamingMessageView,
  useChatContext,
  useFetchReactions,
  useMessageContext,
  useMessagesContext,
  useTheme,
  useTranslationContext,
} from "stream-chat-react-native";
import Animated, {
  useSharedValue,
  useAnimatedStyle,
} from "react-native-reanimated";
import { useMemo } from "react";

const CustomMessageActionsList = (props: MessageActionListProps) => {
  const { messageActions } = props;
  return (
    <View style={styles.actionsContainer}>
      {messageActions?.map((action) => (
        <MessageActionListItem key={action.title} {...action} />
      ))}
    </View>
  );
};

const CustomMessageReactions = (props: MessageUserReactionsProps) => {
  const { reactions: propReactions, selectedReaction } = props;
  const { supportedReactions, MessageUserReactionsItem } = useMessagesContext();
  const { message } = useMessageContext();
  const { loadNextPage, reactions: fetchedReactions } = useFetchReactions({
    message,
    reactionType: selectedReaction,
    sort: {
      created_at: -1,
    },
  });
  const { t } = useTranslationContext();

  const reactions = useMemo(
    () =>
      propReactions ||
      (fetchedReactions.map((reaction) => ({
        id: reaction.user?.id,
        image: reaction.user?.image,
        name: reaction.user?.name,
        type: reaction.type,
      })) as Reaction[]),
    [propReactions, fetchedReactions],
  );

  const renderItem = ({ item }: { item: Reaction }) => (
    <MessageUserReactionsItem
      MessageUserReactionsAvatar={MessageUserReactionsAvatar}
      reaction={item}
      supportedReactions={supportedReactions ?? []}
    />
  );

  const renderHeader = () => (
    <Text style={[styles.reactionsText]}>{t("Message Reactions")}</Text>
  );

  return (
    <View style={styles.reactionsContainer}>
      <FlatList
        accessibilityLabel="reaction-flat-list"
        columnWrapperStyle={[styles.flatListColumnContainer]}
        contentContainerStyle={[styles.flatListContainer]}
        data={reactions}
        keyExtractor={(item) => item.id}
        ListHeaderComponent={renderHeader}
        numColumns={4}
        onEndReached={loadNextPage}
        renderItem={renderItem}
      />
    </View>
  );
};

export const MessageMenu = (props: MessageMenuProps) => {
  const { client } = useChatContext();
  const {
    alignment,
    files,
    groupStyles,
    images,
    message,
    dismissOverlay,
    handleReaction,
    onlyEmojis,
    otherAttachments,
    threadList,
    videos,
  } = useMessageContext();
  const {
    messageContentOrder,
    messageTextNumberOfLines,
    isMessageAIGenerated,
  } = useMessagesContext();
  const own_reactions =
    message?.own_reactions?.map((reaction) => reaction.type) || [];
  const {
    messageActions,
    showMessageReactions,
    selectedReaction,
    MessageUserReactionsItem,
  } = props;
  const {
    theme: {
      colors: {
        blue_alice,
        grey_gainsboro,
        grey_whisper,
        light_gray,
        transparent,
        white_snow,
      },
      messageSimple: {
        content: {
          container: { borderRadiusL, borderRadiusS },
          receiverMessageBackgroundColor,
          senderMessageBackgroundColor,
        },
      },
    },
  } = useTheme();
  const groupStyle = `${alignment}_${(groupStyles?.[0] || "bottom").toLowerCase()}`;
  const hasThreadReplies = !!message?.reply_count;
  const messageHeight = useSharedValue(0);
  const messageLayout = useSharedValue({ x: 0, y: 0 });
  const messageWidth = useSharedValue(0);

  return (
    <View style={styles.wrapper}>
      <Modal onRequestClose={dismissOverlay} transparent visible={true}>
        <GestureHandlerRootView style={{ flex: 1 }}>
          <TouchableWithoutFeedback
            onPress={dismissOverlay}
            style={{ flex: 1 }}
          >
            <View style={styles.overlay}>
              <MessageReactionPicker
                dismissOverlay={dismissOverlay}
                handleReaction={handleReaction}
                ownReactionTypes={own_reactions}
              />
              <Animated.View
                onLayout={({
                  nativeEvent: {
                    layout: { height: layoutHeight, width: layoutWidth, x, y },
                  },
                }) => {
                  messageLayout.value = {
                    x: alignment === "left" ? x + layoutWidth : x,
                    y,
                  };
                  messageWidth.value = layoutWidth;
                  messageHeight.value = layoutHeight;
                }}
                style={[styles.alignEnd, styles.row]}
              >
                {alignment === "left" && MessageAvatar && (
                  <MessageAvatar
                    {...{ alignment, message, showAvatar: true }}
                  />
                )}
                <View
                  style={[
                    styles.contentContainer,
                    {
                      backgroundColor:
                        onlyEmojis && !message.quoted_message
                          ? transparent
                          : otherAttachments?.length
                            ? otherAttachments[0].type === "giphy"
                              ? !message.quoted_message
                                ? transparent
                                : grey_gainsboro
                              : blue_alice
                            : alignment === "left"
                              ? (receiverMessageBackgroundColor ?? white_snow)
                              : (senderMessageBackgroundColor ?? light_gray),
                      borderBottomLeftRadius:
                        (groupStyle === "left_bottom" ||
                          groupStyle === "left_single") &&
                        (!hasThreadReplies || threadList)
                          ? borderRadiusS
                          : borderRadiusL,
                      borderBottomRightRadius:
                        (groupStyle === "right_bottom" ||
                          groupStyle === "right_single") &&
                        (!hasThreadReplies || threadList)
                          ? borderRadiusS
                          : borderRadiusL,
                      borderColor: grey_whisper,
                    },
                    (onlyEmojis && !message.quoted_message) ||
                    otherAttachments?.length
                      ? { borderWidth: 0 }
                      : {},
                  ]}
                >
                  {messageContentOrder.map(
                    (messageContentType, messageContentOrderIndex) => {
                      switch (messageContentType) {
                        case "quoted_reply":
                          return (
                            message.quoted_message && (
                              <View
                                key={`quoted_reply_${messageContentOrderIndex}`}
                                style={[styles.replyContainer]}
                              >
                                <Reply
                                  quotedMessage={
                                    message.quoted_message as ReplyProps["quotedMessage"]
                                  }
                                />
                              </View>
                            )
                          );
                        case "gallery":
                          return (
                            <Gallery
                              alignment={alignment}
                              groupStyles={groupStyles}
                              hasThreadReplies={!!message?.reply_count}
                              images={images}
                              key={`gallery_${messageContentOrderIndex}`}
                              message={message}
                              threadList={threadList}
                              videos={videos}
                            />
                          );
                        case "files":
                          return (
                            <FileAttachmentGroup
                              files={files}
                              key={`file_attachment_group_${messageContentOrderIndex}`}
                              messageId={message.id}
                            />
                          );
                        case "poll":
                          const pollId = message.poll_id;
                          const poll = pollId && client.polls.fromState(pollId);
                          return poll ? (
                            <Poll message={message} poll={poll} />
                          ) : null;
                        case "ai_text": {
                          return isMessageAIGenerated?.(message) ? (
                            <StreamingMessageView
                              key={`ai_message_text_container_${messageContentOrderIndex}`}
                              message={message}
                            />
                          ) : null;
                        }
                        case "attachments":
                          return otherAttachments?.map(
                            (attachment, attachmentIndex) => (
                              <Attachment
                                attachment={attachment}
                                key={`${message.id}-${attachmentIndex}`}
                              />
                            ),
                          );
                        case "text":
                        default:
                          return (otherAttachments?.length &&
                            otherAttachments[0].actions) ||
                            isMessageAIGenerated?.(message) ? null : (
                            <MessageTextContainer
                              key={`message_text_container_${messageContentOrderIndex}`}
                              messageOverlay
                              messageTextNumberOfLines={
                                messageTextNumberOfLines
                              }
                              onlyEmojis={onlyEmojis}
                            />
                          );
                      }
                    },
                  )}
                </View>
              </Animated.View>
              {showMessageReactions ? (
                <CustomMessageReactions
                  message={message}
                  MessageUserReactionsAvatar={MessageUserReactionsAvatar}
                  MessageUserReactionsItem={MessageUserReactionsItem}
                  selectedReaction={selectedReaction}
                />
              ) : (
                <CustomMessageActionsList
                  dismissOverlay={dismissOverlay}
                  MessageActionListItem={MessageActionListItem}
                  messageActions={messageActions}
                />
              )}
            </View>
          </TouchableWithoutFeedback>
        </GestureHandlerRootView>
      </Modal>
    </View>
  );
};

const styles = StyleSheet.create({
  alignEnd: { alignItems: "flex-end" },
  wrapper: {
    flex: 1,
  },
  overlay: {
    flex: 1,
    backgroundColor: "#000000CC",
    justifyContent: "center",
    alignItems: "center",
  },
  actionsContainer: {
    backgroundColor: "white",
    borderRadius: 8,
    padding: 16,
  },
  contentContainer: {
    borderTopLeftRadius: 16,
    borderTopRightRadius: 16,
    borderWidth: 1,
    overflow: "hidden",
    marginVertical: 8,
  },
  replyContainer: {
    flexDirection: "row",
    paddingHorizontal: 8,
    paddingTop: 8,
  },
  row: { flexDirection: "row" },
  reactionsContainer: {
    maxHeight: 200,
    backgroundColor: "white",
    borderRadius: 8,
    padding: 16,
    maxWidth: 500,
  },
  flatListColumnContainer: {
    justifyContent: "space-evenly",
  },
  flatListContainer: {
    justifyContent: "center",
  },
  reactionsText: {
    fontSize: 16,
    fontWeight: "bold",
    marginVertical: 16,
    textAlign: "center",
  },
});
```


---

This page was last updated at 2026-04-17T17:33:45.179Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v8/guides/custom-message-menu/](https://getstream.io/chat/docs/sdk/react-native/v8/guides/custom-message-menu/).