# Custom Message UI

## Overview

`Channel` accepts the following props for overriding message components:

## Best Practices

- Prefer theming or subcomponent overrides before replacing `MessageSimple`.
- Keep custom message components memoized to protect `FlatList` performance.
- Use `useMessageContext` for per-message data and handlers instead of prop drilling.
- Override only the pieces you need (e.g., `MessageText`, `MessageAvatar`) to reduce surface area.
- Test with reactions, attachments, and threads to confirm layout consistency.

### Message

This higher-order component wraps the UI component (`MessageSimple`) and provides handlers.

<admonition type="note">

**You usually don't need to customize this unless you want custom handlers for actions/gestures.** Use the [Message component](https://github.com/GetStream/stream-chat-react-native/v8/blob/main/package/src/components/Message/Message.tsx) as a reference, and memoize for `FlatList` performance.

</admonition>

```tsx
<OverlayProvider bottomInset={bottom} i18nInstance={streami18n}>
  <Chat client={chatClient} i18nInstance={streami18n}>
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      Message={CustomMessageComponent}
    >
      <View style={{ flex: 1 }}>
        <MessageList />
        <MessageInput />
      </View>
    </Channel>
  </Chat>
</OverlayProvider>
```

### MessageSimple

This is the actual message bubble UI. You can access handlers from the [Message](#message) HOC via `useMessageContext`.

```tsx
import {
  OverlayProvider,
  Chat,
  Channel,
  MessageList,
  MessageInput,
  useChannelContext,
  useMessagesContext,
  useMessageContext,
} from "stream-chat-react-native";

const CustomMessageUIComponent = () => {
  const { channel, members, watchers, error, loading } = useChannelContext();
  const { handleDeleteMessage, handleResendMessage, message, reactions } =
    useMessageContext();

  /** Custom implementation */
};

<OverlayProvider bottomInset={bottom} i18nInstance={streami18n}>
  <Chat client={chatClient} i18nInstance={streami18n}>
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      MessageSimple={CustomMessageUIComponent}
    >
      <View style={{ flex: 1 }}>
        <MessageList />
        <MessageInput />
      </View>
    </Channel>
  </Chat>
</OverlayProvider>;
```

To customize specific parts of `MessageSimple`, pass these props on `Channel`:

- [MessageHeader](/chat/docs/sdk/react-native/v8/core-components/channel#messageheader/)
- [MessageFooter](/chat/docs/sdk/react-native/v8/core-components/channel#messagefooter/)
- [MessageAvatar](/chat/docs/sdk/react-native/v8/core-components/channel#messageavatar/)
- [MessageBounce](/chat/docs/sdk/react-native/v8/core-components/channel#messagebounce/)
- [MessageDeleted](/chat/docs/sdk/react-native/v8/core-components/channel#messagedeleted/)
- [MessageEditedTimestamp](/chat/docs/sdk/react-native/v8/core-components/channel#messageeditedtimestamp/)
- [MessageStatus](/chat/docs/sdk/react-native/v8/core-components/channel#messagestatus/)
- [MessageText](/chat/docs/sdk/react-native/v8/core-components/channel#messagetext/)
- [MessageSystem](/chat/docs/sdk/react-native/v8/core-components/channel#messagesystem/)
- [MessageContent](/chat/docs/sdk/react-native/v8/core-components/channel#messagecontent/)
- [Attachment](/chat/docs/sdk/react-native/v8/core-components/channel#attachment/)
- [Giphy](/chat/docs/sdk/react-native/v8/core-components/channel#giphy/)
- [Card](/chat/docs/sdk/react-native/v8/core-components/channel#card/)
- [FileAttachmentGroup](/chat/docs/sdk/react-native/v8/core-components/channel#fileattachmentgroup/)
- [FileAttachment](/chat/docs/sdk/react-native/v8/core-components/channel#fileattachment/)
- [Gallery](/chat/docs/sdk/react-native/v8/core-components/channel#gallery/)
- [UrlPreview](/chat/docs/sdk/react-native/v8/core-components/channel#urlpreview/)

```tsx
<Channel
  channel={channel}
  keyboardVerticalOffset={headerHeight}
  MessageAvatar={CustomAvatarComponent}
  MessageText={CustomTextComponent}
>
```

## Message bubble with custom text styles & fonts

We use `react-native-simple-markdown` in `Message` to render markdown, so styling differs from a plain `Text` component.

Theme styles for markdown live under `messageSimple -> content -> markdown`. You can override individual markdown sub-component styles.

```tsx
const themeStyle = {
  messageSimple: {
    content: {
      markdown: {
        heading1: {
          color: "pink",
        },
        inlineCode: {
          fontSize: 10,
        },
      },
    },
  },
};

<Chat style={themeStyle}>...</Chat>;
```

Markdown keys available for styling:

```typescript
export type MarkdownStyle = Partial<{
  autolink: TextStyle;
  blockQuoteBar: ViewStyle;
  blockQuoteSection: ViewStyle;
  blockQuoteSectionBar: ViewStyle;
  blockQuoteText: TextStyle | ViewStyle;
  br: TextStyle;
  codeBlock: TextStyle;
  del: TextStyle;
  em: TextStyle;
  heading: TextStyle;
  heading1: TextStyle;
  heading2: TextStyle;
  heading3: TextStyle;
  heading4: TextStyle;
  heading5: TextStyle;
  heading6: TextStyle;
  hr: ViewStyle;
  image: ImageStyle;
  inlineCode: TextStyle;
  list: ViewStyle;
  listItem: ViewStyle;
  listItemBullet: TextStyle;
  listItemNumber: TextStyle;
  listItemText: TextStyle;
  listRow: ViewStyle;
  mailTo: TextStyle;
  mentions: TextStyle;
  newline: TextStyle;
  noMargin: TextStyle;
  paragraph: TextStyle;
  paragraphCenter: TextStyle;
  paragraphWithImage: ViewStyle;
  strong: TextStyle;
  sublist: ViewStyle;
  table: ViewStyle;
  tableHeader: ViewStyle;
  tableHeaderCell: TextStyle;
  tableRow: ViewStyle;
  tableRowCell: ViewStyle;
  tableRowLast: ViewStyle;
  text: TextStyle;
  u: TextStyle;
  view: ViewStyle;
}>;
```

## Message bubble with full width

Because the default bubble is complex (reactions, attachments, etc.), full-width bubbles are not supported out of the box. You can theme them at the app level:

```tsx
import { vw } from "stream-chat-react-native";

const maxWidth = vw(100) - 72;
const themeStyle = {
  messageSimple: {
    card: {
      container: {
        width: maxWidth,
      },
    },
    content: {
      container: {
        width: maxWidth,
      },
      textContainer: {
        width: maxWidth,
        maxWidth: maxWidth,
      },
      wrapper: { width: maxWidth },
    },
    gallery: {
      // -2 because of the 2px border
      width: maxWidth - 2,
    },
    giphy: {
      container: {
        width: maxWidth,
      },
    },
  },
  // Override reply so the reuse of message style does not overflow text in the message input
  reply: {
    textContainer: {
      maxWidth: undefined,
      width: undefined,
    },
  },
};

<Chat style={themeStyle}>...</Chat>;
```

## Message bubble without border

Simple style customization using theme object, does the trick:

```tsx
const themeStyle = {
  messageSimple: {
    content: {
      container: {
        borderWidth: 0,
      },
      containerInner: {
        borderWidth: 0,
      },
      deletedContainerInner: {
        borderWidth: 0,
      },
      textContainer: {
        borderWidth: 0,
      },
    },
  },
};

<Chat style={themeStyle}>...</Chat>;
```

## Instagram style double-tap reaction

Double tap handler can be easily attached to a message component using `react-native-gesture-handler`.
Please check the following example for implementation.

<admonition type="warning">

- v3 of React Native Chat SDK used to support a prop `onDoubleTapMessage`, but it has been dropped
  since v4 of SDK for performance reasons.
- Gesture handlers and animations inside a dynamic list may result in increased memory consumption.

</admonition>

```tsx
const CustomMessage = (props: MessageSimpleProps) => {
  const doubleTapRef = useRef(null);
  const { message, handleToggleReaction } = useMessageContext();

  const _onDoubleTap = useAnimatedGestureHandler<TapGestureHandlerGestureEvent>(
    {
      onActive: () => {
        console.log('double tapped')
        runOnJS(handleToggleReaction)('love')
      },
    },
    [message],
  );

  return (
    <TapGestureHandler
      waitFor={doubleTapRef}>
      <Animated.View>
        <TapGestureHandler
          ref={doubleTapRef}
          numberOfTaps={2}
          onHandlerStateChange={_onDoubleTap}>
          <Animated.View>
            <MessageSimple {...props} />
          </Animated.View>
        </TapGestureHandler>
      </Animated.View>
    </TapGestureHandler>
  );
}

<Channel
  channel={channel}
  MessageSimple={CustomMessage}
>
```

## Slack style messages all on the left side

By default, received messages are shown on left side of the `MessageList` and sent messages are shown on right side of the `MessageList`.

You can change this at the `Message` level via the prop `forceAlignMessages` or set the alignment for the entire `Channel` using the same `forceAlignMessages` prop.

```tsx
<Channel
  channel={channel}
  forceAlignMessages='left'
  keyboardVerticalOffset={headerHeight}
  thread={thread}
>
```

## Message bubble with name of sender

In group messaging it's important to show the name of the sender associated message bubble - similar to Slack or WhatsApp. By default this is done in the `MessageFooter` component. This component is fully replaceable via props on `Channel` and is provided a set of props itself, `MessageFooterProps`, that can be used for rendering. Any additional data for rendering a custom footer can be pulled from contexts such as the `MessageContext` via the `useMessageContext` hook.

If you wanted to move the information about the sender to the top of the message you can provide a `MessageHeader` component to `Channel` which is provided the same props, `MessageHeaderProps`, as the footer, `MessageFooterProps`, and again can utilize the contexts as needed.

```tsx
<Channel
  channel={channel}
  keyboardVerticalOffset={headerHeight}
  MessageHeader={(props) =>
    props.message?.user?.id !== chatClient.userID ? (
      <View
        style={{ flexDirection: 'row' }}
      >
        {Object.keys(props.members).length > 2 &&
          props.message.user?.name ? (
            <Text style={[{ color: grey, marginRight: 8 }]}>
              {props.message.user.name}
            </Text>
          ) : null}
        <Text style={[{ color: grey, textAlign: props.alignment }]}>
          {props.formattedDate}
        </Text>
      </View>
    ) : null
  }
  MessageFooter={() => null}
  thread={thread}
>
```

<table>
  <tr>
  <td align='center' width='33%'>

![StandardFooter](https://raw.githubusercontent.com/GetStream/stream-chat-react-native/v8/main/screenshots/cookbook/StandardFooter.png)

  </td>
  <td align='center' width='33%'>

![NoFooter](https://raw.githubusercontent.com/GetStream/stream-chat-react-native/v8/main/screenshots/cookbook/NoFooter.png)

  </td>
  <td align='center' width='33%'>

![HeaderAdded](https://raw.githubusercontent.com/GetStream/stream-chat-react-native/v8/main/screenshots/cookbook/HeaderAdded.png)

  </td>
  </tr>
  <tr></tr>
  <tr>
  <td align='center'>Standard Footer</td>
  <td align='center'>No Footer</td>
  <td align='center'>Header Added</td>
  </tr>
</table>

## Swipe message left to delete and right to reply

We support a default swipe right to reply gesture on the message component. This can be disabled by passing `false` to the `enableSwipeToReply` prop on the `Channel` component.

The default content on the left side when you swipe right can be customized by passing a custom component to the `MessageSwipeContent` prop on the `Channel` component.

```tsx
const CustomMessageSwipeContent = () => {
  return (
    <View>
      <Text>Reply</Text>
    </View>
  );
};

<Channel
  channel={channel}
  MessageSwipeContent={CustomMessageSwipeContent}
>
```

![MessageSwipeContent](@chat-sdk/react-native/v8/_assets/ui-cookbook/message-customization/message-swipe-content-customization.png)

To add more advanced swipe controls to your messages, it is suggested that you create a custom `Message` component to replace the default one. An easy solution is to wrap the standard exported message component from `stream-chat-react-native` in a `Swipeable` from `react-native-gesture-handler/Swipeable`. You can then use the functions provided by `Swipeable` to fine tune to functionality to your liking.

You can add reply functionality by calling `setQuotedMessageState`, available from the `useMessagesContext` hook. Or you can delete the message using a combination of `client.deleteMessage` and `updateMessage`, the latter of which is also available from the `useMessagesContext` hook. You can find the internal implementation of these functions in the `Message` component; or you can add any other functionality you like. It is suggested to add custom logic when implementing swipe-able messages to ensure you only can swipe appropriate messages, **i.e.** you can only swipe to delete messages you have the ability to delete and have not yet been deleted. Using `Message` props and contexts this is easily achievable.

```tsx
const SwipeableMessage = (props: MessageProps) => {
  return (
    <Swipeable
      onSwipeableLeftOpen={reply(props.message)}
      onSwipeableRightOpen={delete props.message}
      overshootLeft={false}
      overshootRight={false}
      renderLeftActions={(progress) => (
        <Animated.View
          style={{
            backgroundColor: "blue",
            transform: [
              {
                translateX: progress.interpolate({
                  inputRange: [0, 1],
                  outputRange: [-100, 0],
                }),
              },
            ],
            width: 100,
          }}
        />
      )}
      renderRightActions={(progress) => (
        <Animated.View
          style={{
            justifyContent: "center",
            opacity: progress.interpolate({
              inputRange: [0, 1],
              outputRange: [0, 1],
            }),
          }}
        >
          <StreamReaction />
        </Animated.View>
      )}
    >
      <Message {...props} />
    </Swipeable>
  );
};
```

<table>
  <tr>
  <td align='center' width='33%'>

![SwipingOpacity](https://raw.githubusercontent.com/GetStream/stream-chat-react-native/v8/main/screenshots/cookbook/SwipingOpacity.png)

  </td>
  <td align='center' width='33%'>

![SwipingOpen](https://raw.githubusercontent.com/GetStream/stream-chat-react-native/v8/main/screenshots/cookbook/SwipingOpen.png)

  </td>
  <td align='center' width='33%'>

![SwipingTranslateX](https://raw.githubusercontent.com/GetStream/stream-chat-react-native/v8/main/screenshots/cookbook/SwipingTranslateX.png)

  </td>
  </tr>
  <tr></tr>
  <tr>
  <td align='center'>Swiping partially open (opacity partial)</td>
  <td align='center'>Swiping all the way open</td>
  <td align='center'>Swiping using transform -> translateX</td>
  </tr>
</table>

## Hiding deleted messages from the channel

The `deletedMessagesVisibilityType` prop in the Channel component enables changing the visibility of the deleted messages to both sender and receiver, none of them or either of them.

```tsx
<Channel
  ...
  deletedMessagesVisibilityType="sender"
>
```


---

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

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