# Pin indicator

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

A pinned message shows a line with a pin icon above its bubble, "Pinned by" and the name of who pinned it, and its row in the list is highlighted. This UI cookbook changes how a pin looks, first with the theme, then with a component of your own that shows when the message was pinned and when the pin expires, and lets the user unpin it. How pinning works, and the list of a channel's pinned messages, are on [Pinned messages](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/pinned-messages/).

## Best practices

- Restyle the default indicator through the theme when only its look changes. `messageItemView.pinnedHeader` styles the line, and `backgroundCoreHighlight` colors the row.
- Replace `MessagePinnedHeader` when the indicator needs more data or an action. It renders inside the message, so it reads the message from `useMessageContext()`.
- Give the indicator a background of its own when you change its colors. The message menu shows the message, with its indicator, over a dimmed backdrop.
- Offer Unpin only to users with the `pin-message` capability. The pin handler doesn't check it, and the server rejects the request from a user without it.
- Keep the indicator to one short line, so it doesn't push the message content around.

## Restyling the default indicator

The default indicator is a row with the `Pin` icon and a label. `messageItemView.pinnedHeader.container` styles the row, and `messageItemView.pinnedHeader.label` the label. This theme turns the indicator into an orange chip, and tints the highlight of the row to match, with values for each color scheme:

```tsx title="src/App.tsx"
import type { PropsWithChildren } from "react";
import { useColorScheme } from "react-native";
import type { StreamChat } from "stream-chat";
import { Chat, OverlayProvider } from "stream-chat-react-native";
import type { DeepPartial, Theme } from "stream-chat-react-native";

const lightTheme: DeepPartial<Theme> = {
  messageItemView: {
    pinnedHeader: {
      container: {
        backgroundColor: "#FFF3E0",
        borderColor: "#FF9800",
        borderRadius: 6,
        borderWidth: 1,
        marginBottom: 2,
        paddingHorizontal: 8,
      },
      label: { color: "#E65100" },
    },
  },
  semantics: { backgroundCoreHighlight: "#FFF8EE" },
};

const darkTheme: DeepPartial<Theme> = {
  messageItemView: {
    pinnedHeader: {
      container: {
        backgroundColor: "#3E2200",
        borderColor: "#FFA726",
        borderRadius: 6,
        borderWidth: 1,
        marginBottom: 2,
        paddingHorizontal: 8,
      },
      label: { color: "#FFB74D" },
    },
  },
  semantics: { backgroundCoreHighlight: "#2A1A05" },
};

export const App = ({
  children,
  client,
}: PropsWithChildren<{ client: StreamChat }>) => {
  const scheme = useColorScheme();
  const theme = scheme === "dark" ? darkTheme : lightTheme;

  return (
    <OverlayProvider value={{ style: theme }}>
      <Chat client={client} style={theme}>
        {children}
      </Chat>
    </OverlayProvider>
  );
};
```

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

![A pinned message with an orange chip reading Pinned by You and a tinted row on iOS](https://getstream.io/docs-assets/images/325c22f79a81.png)

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

![A pinned message with an orange chip reading Pinned by You and a tinted row on Android](https://getstream.io/docs-assets/images/71163f5c064d.png)

  </td>
  </tr>
</table>

- Each style is spread over the default one, so the properties you leave out keep their defaults. `MessageHeader` aligns the indicator to the side of the message, so the chip is as wide as its content.
- The icon takes the label's color.
- The default label is `textPrimary`, and `textOnAccent` while the message menu shows the message. A color in `pinnedHeader.label` wins over both, so the chip's background keeps it readable over the menu's backdrop too.

How the theme is passed, and how to switch it with the color scheme, is explained in [Theming](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/theming/#light-and-dark).

### The highlighted row

`Message` paints `backgroundCoreHighlight` behind the whole row of a pinned message, and spreads `messageItemView.targetedMessageContainer` over it. The message the list [jumps to](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/message-list-ui/#jumps-on-screen) gets the same highlight for 3 seconds, so both keys change that highlight too. No theme key applies to pinned rows only. Setting `targetedMessageContainer` to `{ backgroundColor: "transparent" }` removes the highlight from pinned rows and from jumped-to messages alike.

A pinned message that has been deleted gets neither the highlight nor the indicator.

## A custom pin indicator

The theme can't add information. This indicator says who pinned the message and on which day, shows when the pin expires, and has an Unpin button:

```tsx title="src/PinnedByIndicator.tsx"
import { Pressable, StyleSheet, Text, View } from "react-native";
import { convertTimestampToDate } from "stream-chat";
import {
  getDateString,
  useChatContext,
  useComponentsContext,
  useMessageActionHandlers,
  useMessageContext,
  useOwnCapabilitiesContext,
  useTheme,
  useTranslationContext,
} from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";

const PinnedByIndicator = () => {
  const { client } = useChatContext();
  const { message } = useMessageContext();
  const { handleTogglePinMessage } = useMessageActionHandlers();
  const { pinMessage } = useOwnCapabilitiesContext();
  const { icons } = useComponentsContext();
  const { tDateTimeParser } = useTranslationContext();
  const {
    theme: { semantics },
  } = useTheme();

  if (!message.pinned) return null;

  const pinnedBy = message.pinned_by;
  const pinnedByName =
    pinnedBy?.id === client.userId
      ? "you"
      : (pinnedBy?.name ?? pinnedBy?.id ?? "someone");
  const pinnedOn = getDateString({
    format: "LL",
    messageCreatedAt: convertTimestampToDate(message.pinned_at),
    tDateTimeParser,
  });
  const expiresAt = getDateString({
    format: "LLL",
    messageCreatedAt: convertTimestampToDate(message.pin_expires),
    tDateTimeParser,
  });

  return (
    <View
      style={[
        styles.chip,
        {
          backgroundColor: semantics.backgroundCoreSurfaceSubtle,
          borderColor: semantics.borderCoreDefault,
        },
      ]}
    >
      <icons.Pin height={12} stroke={semantics.textSecondary} width={12} />
      <Text style={[styles.text, { color: semantics.textPrimary }]}>
        Pinned by {pinnedByName}
        {pinnedOn ? ` on ${pinnedOn}` : null}
      </Text>
      {expiresAt ? (
        <Text style={[styles.text, { color: semantics.textSecondary }]}>
          until {expiresAt}
        </Text>
      ) : null}
      {pinMessage ? (
        <Pressable
          accessibilityLabel="Unpin message"
          accessibilityRole="button"
          hitSlop={8}
          onPress={() => void handleTogglePinMessage()}
        >
          <Text
            style={[styles.text, styles.unpin, { color: semantics.textLink }]}
          >
            Unpin
          </Text>
        </Pressable>
      ) : null}
    </View>
  );
};

export const pinOverrides: ComponentOverrides = {
  MessagePinnedHeader: PinnedByIndicator,
};

const styles = StyleSheet.create({
  chip: {
    alignItems: "center",
    borderRadius: 6,
    borderWidth: 1,
    flexDirection: "row",
    flexWrap: "wrap",
    gap: 6,
    marginBottom: 2,
    paddingHorizontal: 8,
    paddingVertical: 2,
  },
  text: { fontSize: 12 },
  unpin: { fontWeight: "600" },
});
```

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

![A pin indicator showing who pinned the message, the date, when the pin expires and an Unpin button on iOS](https://getstream.io/docs-assets/images/04ab948a9eaf.png)

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

![A pin indicator showing who pinned the message, the date, when the pin expires and an Unpin button on Android](https://getstream.io/docs-assets/images/d772ec10a06a.png)

  </td>
  </tr>
</table>

Pass `pinOverrides` to a `WithComponents` around `Channel` and `Thread`. How it works:

1. The default `MessageItemView` renders `MessageHeader` above the bubble, and `MessageHeader` renders the `MessagePinnedHeader` key for a message with `message.pinned`. It passes the message as `message`, but `PinnedByIndicator` reads it from `useMessageContext()`, which has the message after every update.
2. `pinned_by`, `pinned_at` and `pin_expires` are on the message. The timestamps are nanosecond numbers, so `convertTimestampToDate()` turns them into dates; [Data conventions](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/data-conventions/#timestamps) explains why. It returns `undefined` for a pin without an expiry, and `getDateString()` returns `null` then, so the "until" part isn't rendered.
3. `format` is a Day.js format, rendered in the user's language: `LL` is the date, such as "October 26, 2026", and `LLL` adds the time. [The formatting syntax](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/#the-formatting-syntax) lists the tokens.
4. `pinMessage` from `useOwnCapabilitiesContext()` is the user's `pin-message` capability in the channel, after any `overrideOwnCapabilities` on `Channel`. Without it, the button isn't rendered.
5. Pressing Unpin calls `handleTogglePinMessage()` from `useMessageActionHandlers()`, the handler behind the menu's Pin and Unpin actions. It unpins a pinned message and shows a "Message unpinned" notification, or an error notification when the request fails. The request changes no local state, so the indicator stays until the `message.updated` event brings the message back with `pinned: false`.

The chip has its own background, so it reads the same while the message menu shows the message over its dimmed backdrop.

The indicator only renders where `MessageHeader` does. The default `MessageItemView` leaves it out of a deleted message, and a [custom message view](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/message/custom-message/) renders whatever it wants for a pinned message, `MessageHeader` included. The row highlight comes from `Message`, so it stays with any message view.

## Read more

- [Pinned messages](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/pinned-messages/#turning-it-on-or-off): hiding the Pin action from users who can pin.
- [Actions](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/actions/#message-actions-in-your-own-components): the other handlers `useMessageActionHandlers()` returns.
- [Message UI components](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message/message-ui-components/#messageheader): `MessageHeader` and the other marks it renders above the bubble.
- [Permissions reference](https://getstream.io/chat/docs/javascript/permissions-reference/): the `pin-message` capability and the roles that have it.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/message/pin-indicator/](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/message/pin-indicator/).