# Pinned messages

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

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, oldest pin first. It's a paginator like the others in the state layer: its `state` store has `items`, `isLoading`, `hasMoreTail`, `hasMoreHead` and `lastQueryError`. [How pagination works](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/how-pagination-works/) explains these fields.

>
> **Info:** **Example**
>
> Take a channel with 25 pinned messages:
>
> 1. When the channel is queried or watched, the response carries a page of its pinned messages. They become the first page of the list.
> 2. `PinnedMessagePaginator.toTail()` loads the next page of earlier pins from the pinned messages endpoint.
> 3. Bob pins another message. The `message.updated` event arrives, the message now has `pinned: true`, and the list adds it. When he unpins it, the next `message.updated` removes it.
> 4. A pinned message that is edited, reacted to or deleted changes in the list too, because the list and the message list share each message.
>

Thread replies stay out of the list: the channel only adds messages from its main list. Truncating the channel also clears the pins from before the cutoff.

#### 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 default Pin action doesn't check that capability: every user sees it on channel messages, and the server rejects the request from a user who can't pin. To hide it from those users, filter the action set by the capability:

```tsx
import { useMemo } from "react";
import type { ChannelStateData } from "stream-chat";
import {
  MessageActions,
  defaultMessageActionSet,
  useChannel,
  useStateStore,
} from "stream-chat-react";

const capabilitiesSelector = ({ ownCapabilities }: ChannelStateData) => ({
  ownCapabilities,
});

export const MessageActionsWithPinCheck = () => {
  const channel = useChannel();
  const { ownCapabilities } = useStateStore(
    channel.state,
    capabilitiesSelector,
  );
  const canPin = ownCapabilities.includes("pin-message");

  const messageActionSet = useMemo(
    () =>
      canPin
        ? defaultMessageActionSet
        : defaultMessageActionSet.filter(
            (action) =>
              action.placement === "quick-dropdown-toggle" ||
              action.type !== "pin",
          ),
    [canPin],
  );

  return <MessageActions messageActionSet={messageActionSet} />;
};
```

Register it with `<WithComponents overrides={{ MessageActions: MessageActionsWithPinCheck }}>`, as [Message actions](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/message-actions/#customizing) shows.

## 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/v15/recipes/message/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/v15/ui/reading-state-in-components/) explains the selector rules this sample follows. [Message list UI](https://getstream.io/chat/docs/sdk/react/v15/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/v15/features/channels/channel-details-and-members/).

## Reference

- [Pinned messages paginator](https://getstream.io/chat/docs/sdk/react/v15/reference/state/pinned-messages-paginator/): the state, loading methods and what updates the list.
- [Channel Detail Views](https://getstream.io/chat/docs/sdk/react/v15/component-reference/channel-detail/channel-detail-views/): `PinnedMessagesView` and its props.
- [ComponentContext](https://getstream.io/chat/docs/sdk/react/v15/component-reference/channel/componentcontext/): the `PinIndicator` override key.
- [Message hooks](https://getstream.io/chat/docs/sdk/react/v15/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/v15/features/messaging/pinned-messages/](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/pinned-messages/).