# Cookbooks overview

Each recipe is a complete, working example of one customization: the components, the CSS, and what happens when the code runs. Recipes apply the mechanisms the [UI](https://getstream.io/chat/docs/sdk/react/ui/providers-and-context-hooks/) section explains, such as [Overriding components](https://getstream.io/chat/docs/sdk/react/ui/components/overriding-components/), [Actions](https://getstream.io/chat/docs/sdk/react/ui/components/actions/) and [Workspace with ChatView](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/), and link to them instead of explaining them again.

## Choosing where to start

Make the smallest change that gets you there, in this order:

1. **Restyle.** Colors, spacing and shapes come from design tokens and CSS classes; see [Theming](https://getstream.io/chat/docs/sdk/react/features/app-experience/theming/). It's the change most likely to keep working when the SDK updates its components.
2. **Replace one part.** Most parts have a key in `ComponentContext`. Replace the smallest part that makes the change, such as one row of the channel list rather than the whole list.
3. **Compose.** `Channel`, `Thread` and the layout's slots render only the children you give them. Leave parts out, add your own, or change their order.
4. **Change behaviour in the state layer.** What a send does, what a list loads or what a search finds is configured on the state objects, not in components; see [How the SDK is built](https://getstream.io/chat/docs/sdk/react/getting-started/how-the-sdk-is-built/).

A replaced part has to handle every case the default handles. Test it with the kinds of data your app has, and again after you update the SDK.

## The base app

Every recipe starts from the same app: the channel list on the left, the open channel in the middle, its thread beside it, and a side panel after them, in the SDK's default theme. [Theming](https://getstream.io/chat/docs/sdk/react/features/app-experience/theming/) shows how to customize it. It's the app from the [Quick start](https://getstream.io/chat/docs/sdk/react/getting-started/quick-start/) with presence turned on and the stylesheet in CSS layers. A recipe page shows only what it adds and where it plugs in: a component registered with `WithComponents`, a header or view of its own, or a stylesheet.

```tsx label="React" title="src/App.tsx"
import { useEffect } from "react";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import {
  ChannelHeader,
  ChannelNavigation,
  Chat,
  MessageComposer,
  MessageList,
  ThreadHeader,
  useCreateChatClient,
  useThreadContext,
} from "stream-chat-react";
import {
  ChannelSlot,
  ChatView,
  ThreadSlot,
  useActiveThread,
} from "stream-chat-react/slot-layout";
import type { LayoutDescriptor } from "stream-chat-react/slot-layout";
import { SIDE_PANEL_SLOT, SidePanel, SidePanelProvider } from "./SidePanel";
import "./chat.css";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

const filters: ChannelFilters = {
  members: { $in: [userId] },
  type: "messaging",
};
const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];

// The channel in one slot, a thread opened from it in the next, then the side panel.
const layouts: LayoutDescriptor[] = [
  { id: "channels", slots: ["channel", "thread", SIDE_PANEL_SLOT] },
];

// Marks the open thread as on screen, so its new replies are marked read.
const ActiveThread = () => {
  const thread = useThreadContext();
  useActiveThread({ activeThread: thread });
  return null;
};

const ChannelsView = () => (
  <>
    <ChannelNavigation />
    <ChannelSlot slot="channel">
      <ChannelHeader />
      <MessageList />
      <MessageComposer />
    </ChannelSlot>
    <ThreadSlot slot="thread">
      <ActiveThread />
      <ThreadHeader />
      <MessageList />
      <MessageComposer />
    </ThreadSlot>
    <SidePanel />
  </>
);

export const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  useEffect(() => {
    if (!client) return;
    const paginator = new ChannelPaginator({
      client,
      filters,
      id: "my-channels",
      // Presence keeps members' online status current, which several recipes show.
      requestOptions: { presence: true, state: true },
      sort,
    });
    client.channelManager.insertPaginator({ paginator });
    return () => {
      client.channelManager.removePaginator(paginator);
    };
  }, [client]);

  if (!client) return <div>Connecting…</div>;

  return (
    <Chat client={client}>
      <SidePanelProvider>
        <ChatView layouts={layouts} views={{ channels: <ChannelsView /> }} />
      </SidePanelProvider>
    </Chat>
  );
};
```

```css label="CSS" title="src/chat.css"
/* The SDK's stylesheet in a layer, and every override after it. */
@layer stream, stream-overrides;

@import "stream-chat-react/dist/css/index.css" layer(stream);

html,
body,
#root {
  height: 100%;
  margin: 0;
}

@layer stream-overrides {
  .str-chat {
    --str-chat__channel-list-width: 320px;
  }
}
```

### The side panel

Recipes that open a panel, such as channel details or a user's profile, open it in the side panel: a 360px column at the far right, after the channel and the thread. It shows one thing at a time, so opening another panel replaces the one shown. Opening it leaves the thread open, and opening a thread leaves it open too. A recipe calls `useSidePanel().openSidePanel()` with a title and a body, and the panel draws the title row and the close button around the body. A body with a header of its own, such as the SDK's `ChannelDetail`, passes `ownHeader: true`, and the panel leaves its title row out. A user's profile is the SDK's `userProfile` binding kind, so `useSidePanel().openUserProfile()` binds it into the panel's slot instead, and `SidePanel` renders it with the component passed as its `UserProfile` prop. Opening other content, or closing the panel, releases the whole slot with `useChatViewNavigation().release()`, so an earlier profile never comes back when the panel closes.

Content other than a profile is held by `SidePanelProvider`, outside `ChatView`, so it stays when the user leaves the view. An app with more than one view closes the panel with `useSidePanel().closeSidePanel()` when the user leaves the channels view.

The panel manages focus. When it opens, focus moves to its title, or to the whole panel when the body brings its own header. When it closes while focus is inside it, focus goes back to the element that opened it, or to the channel's composer when that element is gone, such as an item of a menu that has closed. The close button is named after the panel, for example Close Profile.

```tsx label="React" title="src/SidePanel.tsx"
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from "react";
import type {
  ComponentType,
  PropsWithChildren,
  ReactNode,
  RefObject,
} from "react";
import { Button, useComponentContextIcons } from "stream-chat-react";
import {
  useChatViewNavigation,
  useSlotEntity,
} from "stream-chat-react/slot-layout";
import "./SidePanel.css";

/** The side panel's slot in the channels layout, after the channel and the thread. */
export const SIDE_PANEL_SLOT = "sidePanel";

/** What a recipe opens in the side panel. */
export type SidePanelContent = {
  /** Names what the panel shows, so a button can tell whether its own content is the open one. */
  id: string;
  /** The heading of the panel's title row, and the panel's name for assistive technology. */
  title: string;
  /**
   * The body renders its own title row and close button, as the SDK's `ChannelDetail` does, so the
   * panel leaves its own out and `title` only names it.
   */
  ownHeader?: boolean;
  /** Everything under the title row. It reads its data itself, so it stays current. */
  body: ReactNode;
};

type SidePanelState = {
  content?: SidePanelContent;
  /** The element that had focus when the panel opened, to give it back when the panel closes. */
  openerRef: RefObject<HTMLElement | null>;
  /** The panel's `aside` while it's on screen. */
  panelRef: RefObject<HTMLElement | null>;
  setContent: (content?: SidePanelContent) => void;
};

const SidePanelStateContext = createContext<SidePanelState>({
  openerRef: { current: null },
  panelRef: { current: null },
  setContent: () => undefined,
});

/** Holds the content the side panel shows, for the buttons that open it and the panel to share. */
export const SidePanelProvider = ({ children }: PropsWithChildren) => {
  const [content, setContent] = useState<SidePanelContent>();
  const openerRef = useRef<HTMLElement | null>(null);
  const panelRef = useRef<HTMLElement | null>(null);
  const value = useMemo(
    () => ({ content, openerRef, panelRef, setContent }),
    [content],
  );

  return (
    <SidePanelStateContext.Provider value={value}>
      {children}
    </SidePanelStateContext.Provider>
  );
};

/** The `id` the panel gives a user's profile, so a button can tell whether that profile is open. */
const userProfileId = (userId: string) => `user-profile:${userId}`;

/**
 * Where focus goes when the panel closes: the element that opened it, or, when that one is gone
 * (an item of a menu that has closed, say), the open channel's composer.
 */
const focusTargetAfterClose = (opener: HTMLElement | null) =>
  opener?.isConnected
    ? opener
    : document.querySelector<HTMLElement>(
        ".str-chat__channel .str-chat__textarea textarea",
      );

/**
 * Opens and closes the side panel. It shows one thing at a time: opening anything replaces what it
 * showed. A user's profile is the SDK's `userProfile` kind, bound in the panel's slot; everything
 * else is content held by `SidePanelProvider`. Use it inside `ChatView`.
 *
 * Opening remembers the element that had focus; closing from inside the panel gives focus back to
 * it.
 */
export const useSidePanel = () => {
  const { content, openerRef, panelRef, setContent } = useContext(
    SidePanelStateContext,
  );
  const { open, release } = useChatViewNavigation();
  const profile = useSlotEntity({ kind: "userProfile", slot: SIDE_PANEL_SLOT });

  // Focus inside the panel means it moved there from the opener, which stays the one to go back to.
  // Focus on the page itself, after a click on something that can't take focus, names no opener.
  const rememberOpener = useCallback(() => {
    const active = document.activeElement;
    if (panelRef.current?.contains(active)) return;
    openerRef.current =
      active instanceof HTMLElement && active !== document.body ? active : null;
  }, [openerRef, panelRef]);

  /** Shows `next` in the side panel, in place of whatever it showed. */
  const openSidePanel = useCallback(
    (next: SidePanelContent) => {
      rememberOpener();
      // Empties the slot of any profile and every layer on it.
      release(SIDE_PANEL_SLOT);
      setContent(next);
    },
    [release, rememberOpener, setContent],
  );

  /** Shows the user's profile in the side panel, in place of whatever it showed. */
  const openUserProfile = useCallback(
    (userId: string) => {
      rememberOpener();
      setContent(undefined);
      open(
        { key: userId, kind: "userProfile", source: { userId } },
        { slot: SIDE_PANEL_SLOT },
      );
    },
    [open, rememberOpener, setContent],
  );

  /**
   * Empties the side panel. Focus that was inside it goes back to what opened it. Focus on the page
   * body counts as inside too: some browsers leave it there after a click on a button, so a panel
   * that closes on its own can move focus to the channel's composer.
   */
  const closeSidePanel = useCallback(() => {
    const active = document.activeElement;
    const hadFocus =
      active === document.body ||
      !!(active && panelRef.current?.contains(active));
    release(SIDE_PANEL_SLOT);
    setContent(undefined);
    if (hadFocus) focusTargetAfterClose(openerRef.current)?.focus();
    openerRef.current = null;
  }, [openerRef, panelRef, release, setContent]);

  /** The id of what the panel shows, or `undefined` while it's closed. */
  const openId =
    content?.id ?? (profile ? userProfileId(profile.userId) : undefined);

  return { closeSidePanel, openId, openSidePanel, openUserProfile };
};

export type SidePanelProps = {
  /** Renders a user's profile under the title row. Without it, the panel never shows a profile. */
  UserProfile?: ComponentType<{ userId: string }>;
};

/**
 * The column at the far right of the channels view, after the channel and the thread, while it has
 * something to show: a title row with a close button, then the body. Render it as the view's last
 * child.
 */
export const SidePanel = ({ UserProfile }: SidePanelProps) => {
  const { content } = useContext(SidePanelStateContext);
  const profile = useSlotEntity({ kind: "userProfile", slot: SIDE_PANEL_SLOT });

  const shown: SidePanelContent | undefined =
    content ??
    (profile && UserProfile
      ? {
          body: <UserProfile userId={profile.userId} />,
          id: userProfileId(profile.userId),
          title: "Profile",
        }
      : undefined);

  if (!shown) return null;
  // Keyed by what it shows, so other content starts afresh.
  return <SidePanelFrame key={shown.id} {...shown} />;
};

/**
 * The panel around one piece of content: an `aside` named by the title, the title row with its
 * close button (left out when the body brings its own), then the body. It takes focus when it
 * opens: the title, or the whole panel when the body has its own title row.
 */
const SidePanelFrame = ({ body, ownHeader, title }: SidePanelContent) => {
  const { IconXmark } = useComponentContextIcons();
  const { panelRef } = useContext(SidePanelStateContext);
  const { closeSidePanel } = useSidePanel();
  const headingRef = useRef<HTMLHeadingElement>(null);
  const titleId = useId();

  useEffect(() => {
    (ownHeader ? panelRef.current : headingRef.current)?.focus();
  }, [ownHeader, panelRef]);

  return (
    <aside
      aria-label={ownHeader ? title : undefined}
      aria-labelledby={ownHeader ? undefined : titleId}
      className="cookbook-side-panel"
      ref={panelRef}
      tabIndex={ownHeader ? -1 : undefined}
    >
      {!ownHeader && (
        <header className="cookbook-side-panel__header">
          <h2
            className="cookbook-side-panel__title"
            id={titleId}
            ref={headingRef}
            tabIndex={-1}
          >
            {title}
          </h2>
          <Button
            appearance="ghost"
            aria-label={`Close ${title}`}
            circular
            onClick={closeSidePanel}
            size="md"
            variant="secondary"
          >
            <IconXmark />
          </Button>
        </header>
      )}
      <div className="cookbook-side-panel__body">{body}</div>
    </aside>
  );
};
```

```css label="CSS" title="src/SidePanel.css"
@layer stream-overrides {
  /* A 360px column after the channel and the thread, divided from them like the thread is. */
  .cookbook-side-panel {
    display: flex;
    flex: 0 0 360px;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    border-inline-start: 1px solid var(--str-chat__border-core-default);
    background: var(--str-chat__background-core-elevation-1);
    color: var(--str-chat__text-primary);
  }

  /* As tall as the channel and thread headers beside it. */
  .cookbook-side-panel__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--str-chat__spacing-sm);
    box-sizing: border-box;
    height: var(--str-chat__channel-header-height);
    padding: var(--str-chat__spacing-md);
    border-block-end: 1px solid var(--str-chat__border-core-default);
  }

  .cookbook-side-panel__title {
    margin: 0;
    font: var(--str-chat__font-heading-sm);
  }

  /* The panel and its title take focus when it opens, only so reading starts there: no ring. */
  .cookbook-side-panel:focus,
  .cookbook-side-panel__title:focus {
    outline: none;
  }

  /* The body scrolls on its own and sets no padding, so a body can fill it edge to edge. */
  .cookbook-side-panel__body {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-height: 0;
    overflow-y: auto;
  }

  /* The heading of a section inside a panel's body. */
  .cookbook-side-panel__section-title {
    margin: 0 0 var(--str-chat__spacing-xs);
    color: var(--str-chat__text-secondary);
    font: var(--str-chat__font-caption-emphasis);
  }
}
```

A recipe's CSS goes in the `stream-overrides` layer too, so it wins over the SDK's rules without matching their selectors; [Theming → CSS layers](https://getstream.io/chat/docs/sdk/react/features/app-experience/theming/#css-layers) explains why. It takes its colors, spacing and type from the SDK's tokens, so it follows your theme.

The screenshots show the same people throughout: the user is Cleo, and the others are Bob, Cara and Dan. Their channels are Design review, Weekend trip, Book club, Garden plot, General and a direct conversation with Bob.

## Theming

- [Replacing the icon set](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/theming/replacing-the-icon-set/): every SDK icon replaced with one icon library, keeping the SDK's icon classes so its sizes and colors still apply.

## Channel list

- [Preview UI](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/channel-list/preview-ui/): a channel-list row of your own, with the avatar, title, time, unread badge and last message the SDK computes for it.
- [Members and online status](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/channel-list/members-and-online-status/): a list of the open channel's members that shows who is online and updates as they come and go.
- [Starting a conversation](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/channel-list/starting-a-conversation/): a New conversation button that picks people, reuses an existing direct conversation, and creates a new channel on its first message.
- [Empty and loading states](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/channel-list/empty-and-loading-states/): placeholder rows while the first page loads, a message with a New conversation button when the list is empty, and an error with Retry when loading failed.
- [Pinned and archived channels](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/channel-list/pinned-and-archived-channels/): Pinned, Channels and a collapsible Archived section in one scrolling column, each its own channel paginator with a Load more button.
- [Channel invites](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/channel-list/channel-invites/): an Invitations section above the channel list, with Accept and Decline on each invited channel.

## Search

- [Search customization](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/search/search-customization/): the search bar's settings, compact channel results, and search moved into a top bar.
- [Search dialog](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/search/search-dialog/): a search dialog opened from the channel list or with ⌘K, which searches channels, people and messages at once, with filters and sort for each, and results grouped by kind or mixed in one list.
- [Mentions inbox](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/search/mentions-inbox/): a side panel listing the messages that mention the user; picking one opens its channel at that message.

## Message list

- [Custom message](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/custom-message/): a message UI built step by step, with avatars, formatted text, metadata, grouping and a placeholder for deleted messages.
- [System message](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/system-message/): system messages with the event, its time and the user who triggered it.
- [Pin indicator](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/pin-indicator/): the "Pinned by" line restyled with CSS, or replaced with one that shows the pin's date and an Unpin button.
- [Pinned-messages bar](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/pinned-messages-bar/): a bar under the channel header with the latest pinned message; clicking it jumps to the message and then shows the pin before it.
- [Unread and scroll controls](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/unread-and-scroll-controls/): the unread separator, the jump-to-unread banner and the scroll-to-latest button, restyled or replaced.
- [Read-receipt avatars](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/read-receipt-avatars/): a small avatar of each reader under the last message they read, from `channel.messageReceiptsTracker`.
- [Image gallery](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/image-gallery/): a custom image grid, and a lightbox with a thumbnail strip, keyboard and swipe navigation, captions and videos.
- [Giphy preview](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/giphy-preview/): the `/giphy` preview with Send, Shuffle and Cancel buttons of your own.

## Message actions

- [Custom message actions](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-actions/custom-message-actions/): a menu item and a quick button added to the message actions, and default actions removed.
- [Attachment actions](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-actions/attachment-actions/): "Love" and "Loathe" buttons under every image a user sends, added by composer middleware and turned into reactions in the message list.
- [Action bar for a custom message](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-actions/action-bar/): an action bar on a custom message, shown on hover or focus, with pin, thread, flag and delete, and a reply count that opens the thread.
- [Reactions for a custom message](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-actions/reactions/): quick reactions in a custom message's action bar, and each message's reactions shown under it with `MessageReactions`.
- [Editing a poll](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-actions/editing-a-poll/): an Edit poll action that opens a dialog filled from the poll and saves only what changed, with Close and Delete.

## Composer

- [Custom composer](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/composer/custom-composer/): a composer with a layout of its own, built from the SDK's composer parts, so sending, uploads, suggestions and drafts keep working.
- [Attachment previews](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/composer/attachment-previews/): the previews above the input, such as attachments with their upload progress and voice recordings, replaced one kind at a time.
- [Custom suggestions](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/composer/custom-suggestions/): how the suggestion list and its rows look while the user types a mention, a command or an emoji, including rows for a trigger of your own.
- [Inline message editing](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/composer/inline-message-editing/): editing a message where it sits in the list, with Save, Cancel and Esc, while the bottom composer keeps what the user was typing.
- [GIF picker](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/composer/gif-picker/): a GIF button in the composer that opens a picker with search, trending GIFs and more results as the user scrolls, built on a search source.
- [Poll with text and attachments](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/composer/poll-with-text-and-attachments/): a poll that waits in the composer and goes out in one message with the text and attachments, instead of being sent on its own.

## Custom attachments

- [Product card](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/custom-attachments/product-card/): a `product` attachment type of your own, added from the attachment menu and shown as a card in the composer and the message list, with a one-line description in the channel list and in quoted replies.
- [Location on your own map](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/custom-attachments/location-on-your-own-map/): an OpenStreetMap map, with react-leaflet, in the Share Location dialog and in every location message, static or live.

## Headers

- [Channel header](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/headers/channel-header/): a channel header of your own, with the channel's title, image and who is typing.
- [Thread header](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/headers/thread-header/): a compact thread header with a title and subtitle of your own.

## Panels

- [Hide history for new members](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/panels/hide-history-for-new-members/): a dialog that sets how much of the channel's history a member sees when you add them.
- [Channel details](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/panels/channel-details/): the SDK's channel detail panel opened from the channel header, restyled, with a members list that shows who is online.
- [User details](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/panels/user-details/): a profile panel opened from an avatar or a mention, with presence, shared channels and a Send message button.
- [Message info](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/panels/message-info/): who a message was delivered to and who read it, from `channel.messageReceiptsTracker`.
- [Threads view](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/panels/threads-view/): the user's threads in a view of their own, with a custom row and unread counts.

## Notifications

- [System notification banner](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/notifications/system-notification-banner/): a banner that shows while the client can't reach Stream, from the system notification `Chat` publishes.
- [Custom snackbars](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/notifications/custom-snackbars/): a snackbar stack of your own for the notifications in `client.notifications`, with severity styles, actions and auto-dismiss.

## Layout

- [Channel and thread side by side](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/channel-and-thread-side-by-side/): the open channel with its thread in a column beside it, and the space given back when the thread closes.
- [⌘/ctrl-click opens beside](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/ctrl-click-opens-beside/): a ⌘-click or ctrl-click on a channel in the list opens it beside the open channel, while a plain click still replaces it.
- [Single-pane mobile with back button](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/single-pane-mobile-with-back-button/): on a phone, the channel list, a channel and a thread one at a time at full width, with the header's back button and the browser's back going back one step.
- [Collapsible sidebar](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/collapsible-sidebar/): a channel-list sidebar the user can hide and show, with the toggle in the headers.
- [Saving and restoring the layout](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/saving-and-restoring-the-layout/): a reload that puts the user back in the view and channel they had open, and the open channel kept in the URL.

## AI

- [AI chat widget](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/ai/ai-chat-widget/): a floating launcher that opens a compact chat window with the user's assistant channel, beside the rest of the app.
- [Streamed answers with a stop button](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/ai/streamed-answers-with-stop-button/): an answer that appears as it's generated, with a status line for the assistant's state and a Stop button.
- [Voice agent visuals](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/ai/voice-agent-visuals/): a voice conversation with the assistant shown as a glow and a wave that move with what it's doing, and its spoken reply revealed as the user hears it.

---

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