# Pinned messages

Pinning keeps an important message easy to find. This page covers how a message gets pinned, how the channel keeps its list of pinned messages, and the components that show them.

## What users get

The user opens the menu on a message and picks Pin. A line above the message now reads "Pinned by" and the user's name, or "Pinned by You" on their own screen. Picking Unpin removes it.

Channel details, from `stream-chat-react/channel-detail`, have a Pinned messages section. It lists the channel's pinned messages with their author and the date they were sent, and a search field filters them by text. Clicking one jumps the channel's message list to that message.

## How it works across the layers

A pin is part of the message. A pinned message has `pinned: true`, `pinned_at`, `pinned_by` and, when the pin expires, `pin_expires`.

- **State.** `client.pinMessage` and `client.unpinMessage` change the message on the server. The `message.updated` event then brings the change to every client watching the channel, and each channel keeps its pinned messages in `channel.pinnedMessagesPaginator`.
- **UI.** `MessageUI` renders `PinIndicator` above any message with `message.pinned`, and adds the class `str-chat__message--pinned` to it. The Pin action in the message menu pins and unpins. `PinnedMessagesView` is the Pinned messages section of `ChannelDetail`.
- **Layout.** Selecting a pinned message in channel details calls `openChannel` from `useWorkspaceNavigation()`, so with `ChatView` the channel opens in its slot, and with a `WorkspaceNavigationProvider` of your own, your `openChannel` decides. Then it jumps the channel's message list to the message and closes the details.

### The pinned list

`channel.pinnedMessagesPaginator` holds the channel's pinned messages and keeps them current as messages are pinned, unpinned, edited, reacted to or deleted, because it shares each message with the channel's message list. [Pinned messages paginator](https://getstream.io/chat/docs/sdk/react/reference/state/pinned-messages-paginator/) lists its state, how it loads and which events update it.

>
> **Info:** **Example**
>
> The user opens a channel with 25 pinned messages. The channel's response carries the first page of pins, and `PinnedMessagePaginator.toTail()` loads earlier ones. When someone pins another message, the `message.updated` event adds it to the list; unpinning removes it.
>

Pinning or unpinning a reply sent only to its thread doesn't change the list right away, because the channel updates its pins live only from its main list. The change shows the next time the list loads.

#### Pinning and unpinning

Pass the message, or an object with its `id`:

```ts
// Pinned until someone unpins it
await client.pinMessage(message);

// pin_expires takes a Date or an RFC 3339 string: this pin expires in an hour
await client.pinMessage(message, {
  pin_expires: new Date(Date.now() + 60 * 60 * 1000),
});

await client.pinMessage(
  { id: messageId },
  { pin_expires: "2026-12-31T23:00:00Z" },
);

await client.unpinMessage(message);
```

Neither call changes local state. The message and the pinned list update when `message.updated` arrives. The Pin action in the UI does more: it updates the message on screen at once and puts it back if the request fails.

## Turning it on or off

Pinning is always available. The server decides who may pin through the `pin-message` capability, which comes from the channel type's permissions. See [Permissions reference](https://getstream.io/chat/docs/javascript/permissions-reference/).

The Pin and Unpin actions show only to users with that capability. To ask before pinning, replace the Pin action's component, as `PinWithConfirmation` does in [Adding, removing and replacing actions](https://getstream.io/chat/docs/sdk/react/ui/components/actions/#adding-removing-and-replacing-actions).

## Customizing

### The pin indicator

Replace `PinIndicator` through `WithComponents`. It receives the message as a prop:

```tsx
import type { Channel as ChannelType } from "stream-chat";
import {
  Channel,
  WithComponents,
  type PinIndicatorProps,
} from "stream-chat-react";

const CustomPinIndicator = ({ message }: PinIndicatorProps) => {
  if (!message?.pinned) return null;
  return (
    <div className="custom-pin-indicator">
      📌 {message.pinned_by?.name ?? "Someone"} pinned this
    </div>
  );
};

export const ChannelWithCustomPins = ({
  channel,
}: {
  channel: ChannelType;
}) => (
  <WithComponents overrides={{ PinIndicator: CustomPinIndicator }}>
    <Channel channel={channel}>{/* MessageList, MessageComposer */}</Channel>
  </WithComponents>
);
```

The [Pin indicator](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/pin-indicator/) recipe has a CSS-only version too.

### Your own pinned list

To show pins outside channel details, for example in a bar above the message list, read `channel.pinnedMessagesPaginator.state`. Place this component inside `Channel`:

```tsx
import type { LocalMessage, PaginatorState } from "stream-chat";
import { useChannel, useStateStore } from "stream-chat-react";

const pinnedListSelector = ({
  hasMoreTail,
  isLoading,
  items,
}: PaginatorState<LocalMessage>) => ({ hasMoreTail, isLoading, items });

export const PinnedBar = () => {
  const channel = useChannel();
  const {
    hasMoreTail,
    isLoading,
    items = [],
  } = useStateStore(channel.pinnedMessagesPaginator.state, pinnedListSelector);

  if (!items.length) return null;

  return (
    <div className="pinned-bar">
      {items.map((message) => (
        <button
          key={message.id}
          onClick={() =>
            void channel.messagePaginator.jumpToMessage(message.id)
          }
        >
          {message.user?.name}: {message.text}
        </button>
      ))}
      {hasMoreTail && (
        <button
          disabled={isLoading}
          onClick={() => void channel.pinnedMessagesPaginator.toTail()}
        >
          Earlier pins
        </button>
      )}
    </div>
  );
};
```

The list keeps itself current, so the bar re-renders when someone pins or unpins a message. [Reading state in components](https://getstream.io/chat/docs/sdk/react/ui/reading-state-in-components/) explains the selector rules this sample follows. [Message list UI](https://getstream.io/chat/docs/sdk/react/ui/components/message-list-ui/) covers what the list shows after a jump.

### The Pinned messages section

`PinnedMessagesView` loads its list through a message search, 30 messages per page, filtered to the channel's pinned messages. It runs the search only while `channel.pinnedMessagesPaginator` holds at least one pin. It takes a `searchSource` prop to use a search source of your own. To change the sections of channel details, see [Channel details and members](https://getstream.io/chat/docs/sdk/react/features/channels/channel-details-and-members/).

## Reference

- [Pinned messages paginator](https://getstream.io/chat/docs/sdk/react/reference/state/pinned-messages-paginator/): the state, loading methods and what updates the list.
- [Channel Detail Views](https://getstream.io/chat/docs/sdk/react/component-reference/channel-detail/channel-detail-views/): `PinnedMessagesView` and its props.
- [ComponentContext](https://getstream.io/chat/docs/sdk/react/component-reference/channel/componentcontext/): the `PinIndicator` override key.
- [Message hooks](https://getstream.io/chat/docs/sdk/react/reference/hooks/message-hooks/): `usePinHandler`, the handler behind the Pin action.
- [Pinned messages in the JavaScript client](https://getstream.io/chat/docs/javascript/pinned-messages/): the server API, including pin expiry.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/features/messaging/pinned-messages/](https://getstream.io/chat/docs/sdk/react/features/messaging/pinned-messages/).