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 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:

// 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.

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.

Customizing

The pin indicator

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

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 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:

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 explains the selector rules this sample follows. 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.

Reference