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.pinMessageandclient.unpinMessagechange the message on the server. Themessage.updatedevent then brings the change to every client watching the channel, and each channel keeps its pinned messages inchannel.pinnedMessagesPaginator. - UI.
MessageUIrendersPinIndicatorabove any message withmessage.pinned, and adds the classstr-chat__message--pinnedto it. The Pin action in the message menu pins and unpins.PinnedMessagesViewis the Pinned messages section ofChannelDetail. - Layout. Selecting a pinned message in channel details calls
openChannelfromuseWorkspaceNavigation(), so withChatViewthe channel opens in its slot, and with aWorkspaceNavigationProviderof your own, youropenChanneldecides. 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.
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
- Pinned messages paginator: the state, loading methods and what updates the list.
- Channel Detail Views:
PinnedMessagesViewand its props. - ComponentContext: the
PinIndicatoroverride key. - Message hooks:
usePinHandler, the handler behind the Pin action. - Pinned messages in the JavaScript client: the server API, including pin expiry.