# Pin indicator

A pinned message shows a "Pinned by" line above its bubble. This recipe changes how that line looks and what it shows.

>
> **Note:** **What you'll build**
>
> - A "Pinned by" line restyled as a chip
> - Who pinned the message and on which day
> - When the pin expires
> - An Unpin button
>

---

**Preview**

![The custom indicator: who pinned the message and when, and an Unpin button](https://getstream.io/docs-assets/images/eb6cf4b541cf.png)

---

**Approach:** Restyle and Replace one part. [Choosing where to start](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/overview/#choosing-where-to-start) explains the approaches.

**Starts from:** [the base app](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/overview/#the-base-app). The page shows only what the recipe adds.

## Best practices

- Restyle the default indicator with CSS when only its look changes. A pinned message gets the class `str-chat__message--pinned`, and the indicator renders `str-chat__message-pin-indicator`.
- Replace `PinIndicator` when the indicator needs more data or an action. It renders inside the message, so it can read `useMessageContext()`.
- Keep the `str-chat__message-pin-indicator` class on your component's root. The message's grid uses it to place the indicator above the bubble.
- Offer Unpin only to users with the `pin-message` capability, as the default Pin action does. 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.

## Custom pin indicator

### CSS based solution

By default, a pinned message shows the "Pinned by" line above its bubble, and its row in the list is highlighted:

---

**The default pin indicator**

![The default pin indicator above a pinned message](https://getstream.io/docs-assets/images/6f47ff616ad8.png)

---

The default indicator is an icon and a label inside `str-chat__message-pin-indicator__content`. These rules turn it into a chip with a border and an icon in the accent color:

```css title="src/pin-indicator.css"
@layer stream-overrides {
  .str-chat__message--pinned
    .str-chat__message-pin-indicator
    .str-chat__message-pin-indicator__content {
    width: fit-content;
    padding: var(--str-chat__spacing-xxs) var(--str-chat__spacing-xs);
    border: 1px solid var(--str-chat__accent-primary);
    border-radius: var(--str-chat__radius-sm);
    background-color: var(--str-chat__background-core-surface-subtle);
  }

  .str-chat__message--pinned
    .str-chat__message-pin-indicator__content
    .str-chat__message-pin-indicator__icon {
    color: var(--str-chat__accent-primary);
  }
}
```

---

**The indicator as a chip**

![The pin indicator restyled as a chip](https://getstream.io/docs-assets/images/d4afb52583df.png)

---

The rules sit in the `stream-overrides` layer, so they win over the SDK's own rules once the SDK's stylesheet is in an earlier layer, as [Theming → CSS layers](https://getstream.io/chat/docs/sdk/react/features/app-experience/theming/#css-layers) sets up. They take their colors, spacing and type from the SDK's tokens, so they follow your theme.

The icon takes the text color, so `color` on the icon colors it. The highlighted row comes from the list item around a pinned message, which paints `--str-chat__background-core-highlight` behind it.

### Component based solution

CSS 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 label="React" title="src/PinnedByIndicator.tsx"
import type { PropsWithChildren } from "react";
import { convertTimestampToDate } from "stream-chat";
import {
  useChatContext,
  useMessageContext,
  useMessagePermissions,
  WithComponents,
} from "stream-chat-react";
import "./PinnedByIndicator.css";

const PinnedByIndicator = () => {
  const { client } = useChatContext();
  const { handlePin, message } = useMessageContext();
  const { canPin } = useMessagePermissions(message);

  if (!message.pinned) return null;

  const pinnedBy = message.pinned_by;
  const pinnedByName =
    pinnedBy?.id === client.user?.id
      ? "you"
      : (pinnedBy?.name ?? pinnedBy?.id ?? "someone");
  const pinnedAt = convertTimestampToDate(message.pinned_at);
  const expiresAt = convertTimestampToDate(message.pin_expires);

  return (
    <div className="str-chat__message-pin-indicator cookbook-pinned-by">
      <span>
        <span aria-hidden>📌</span> Pinned by {pinnedByName}
        {pinnedAt && ` on ${pinnedAt.toLocaleDateString()}`}
      </span>
      {expiresAt && (
        <span className="cookbook-pinned-by__expiry">
          until {expiresAt.toLocaleString()}
        </span>
      )}
      {canPin && (
        <button
          className="cookbook-pinned-by__unpin"
          onClick={handlePin}
          type="button"
        >
          Unpin
        </button>
      )}
    </div>
  );
};

// Declared once, so WithComponents gets the same object on every render.
const overrides = { PinIndicator: PinnedByIndicator };

export const WithPinnedByIndicator = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);
```

```css label="CSS" title="src/PinnedByIndicator.css"
@layer stream-overrides {
  .cookbook-pinned-by {
    display: flex;
    align-items: center;
    gap: var(--str-chat__spacing-xs);
    color: var(--str-chat__text-secondary);
    font: var(--str-chat__font-caption-default);
  }

  .cookbook-pinned-by__expiry {
    color: var(--str-chat__text-tertiary);
  }

  .cookbook-pinned-by__unpin {
    padding: 0;
    border: 0;
    background: none;
    color: var(--str-chat__text-link);
    font: var(--str-chat__font-caption-emphasis);
    cursor: pointer;
  }
}
```

The screenshot at the top of this page shows the result.

Wrap the channel in `WithPinnedByIndicator`:

```tsx title="src/PinnedChannel.tsx"
import type { Channel as StreamChannel } from "stream-chat";
import { Channel, MessageComposer, MessageList } from "stream-chat-react";
import { WithPinnedByIndicator } from "./PinnedByIndicator";

export const PinnedChannel = ({ channel }: { channel: StreamChannel }) => (
  <WithPinnedByIndicator>
    <Channel channel={channel}>
      <MessageList />
      <MessageComposer />
    </Channel>
  </WithPinnedByIndicator>
);
```

## How it works

1. The default `MessageUI` renders the `PinIndicator` key above the bubble of every message with `message.pinned`, and passes the message as `message`. `PinnedByIndicator` reads it from `MessageContext` instead, together with `handlePin`.
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/state/state-and-entities/data-conventions/#timestamps) explains why.
3. `useMessagePermissions(message).canPin` is the user's `pin-message` capability in the channel. Without it, the button isn't rendered.
4. Clicking Unpin calls `handlePin`, which unpins a pinned message. The indicator disappears at once, and comes back if the request fails.

The indicator only renders where the default `MessageUI` does. A [custom message UI](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/custom-message/) renders whatever it wants for a pinned message.

## Related

- [Pinned messages](https://getstream.io/chat/docs/sdk/react/features/messaging/pinned-messages/) explains how pinning works and the list of a channel's pinned messages. [Turning it on or off](https://getstream.io/chat/docs/sdk/react/features/messaging/pinned-messages/#turning-it-on-or-off) there explains who may pin.
- [Actions](https://getstream.io/chat/docs/sdk/react/ui/components/actions/#adding-removing-and-replacing-actions) replaces the Pin action with one that asks for confirmation.
- [Permissions reference](https://getstream.io/chat/docs/javascript/permissions-reference/) lists the `pin-message` capability and the roles that have it.
- [Theming](https://getstream.io/chat/docs/sdk/react/features/app-experience/theming/#css-layers) puts the SDK's stylesheet in a layer, so the `stream-overrides` rules win.
- [MessageContext](https://getstream.io/chat/docs/sdk/react/component-reference/message/messagecontext/) lists `handlePin` and the message's other handlers.
- [Custom message](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/custom-message/) replaces the message UI that renders the indicator.

---

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