# Responsive layouts

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

On a wide screen, the app from the [Quick start](https://getstream.io/chat/docs/sdk/react/v15/getting-started/quick-start/#add-threads) shows a channel and its thread side by side. On a phone there's room for one, so an app lays the thread over the channel, at full width. `ChatView` doesn't know that: to the layout, both slots are open. The CSS decides what's on screen.

That matters when the SDK navigates to something the user can't see. In a thread, **View** on "Also sent in channel" jumps to the reply in the channel. On a phone, the channel is under the thread, so nothing seems to happen.

The SDK's stylesheet does one part itself. Below 768px wide, it hides the channel list and the thread list, and the app shows them, for example as an overlay: see [Collapsible sidebar](https://getstream.io/chat/docs/sdk/react/v15/recipes/app/collapsible-sidebar/). For the rest, the app needs to know which slots the user can see.

## Slot geometry

`stream-chat-react/slot-geometry` tells you. It measures the elements that show the slots, and reports which ones are **obscured**: collapsed to nothing, or covered by another slot.

It only reports. It never moves, hides or closes anything: the app decides what to do. It's opt-in, and it doesn't depend on `ChatView`. Slots are plain strings, so it works with any layout that has named panels.

## Detecting covered and collapsed slots

There are three steps:

1. Wrap the layout in `SlotGeometryProvider`.
2. Register the element of each slot with `useRegisterSlotGeometry(slot)`, which returns a ref.
3. Read the result with `useSlotGeometry()`.

This panel registers its element:

```tsx
import type { PropsWithChildren } from "react";
import { useRegisterSlotGeometry } from "stream-chat-react/slot-geometry";

export const Panel = ({
  children,
  slot,
}: PropsWithChildren<{ slot: string }>) => {
  const register = useRegisterSlotGeometry(slot);

  return (
    <div className={`panel panel--${slot}`} ref={register}>
      {children}
    </div>
  );
};
```

And this CSS lays an open thread over the channel on a phone:

```css
.panel {
  display: flex;
  flex: 1 1 0;
  min-width: 0;
}

.panel:empty {
  display: none;
}

@media (max-width: 767px) {
  .panel--thread:not(:empty) {
    background: var(--str-chat__background-core-app);
    inset: 0;
    position: absolute;
    z-index: 1;
  }
}
```

A slot is obscured when either holds:

- **It's collapsed.** Its area is under 1px²: zero width or height, or `display: none`.
- **It's covered.** Another registered slot covers at least 60% of it. A panel beside it, touching only its edge, doesn't count.

So on a phone, with a thread open, the `channel` slot is obscured. On a wide screen, with the thread beside the channel, neither is.

The boxes are compared without regard to which one is drawn on top. On the phone, the channel's box also covers most of the thread's, so the `thread` slot counts as obscured too.

`useSlotGeometry()` gives two readings:

- **`coverage.obscured[slot]`** is kept up to date, and re-renders the component when it changes. Use it to render something.
- **`isObscured(slot)`** measures the elements when you call it. Use it in an event handler, where the latest change of layout may not have reached `coverage` yet.

The slots are measured again when one of their elements changes size, and when the window is resized. Outside a `SlotGeometryProvider`, `useSlotGeometry()` reports nothing as obscured.

## Deciding what to show

What to do about an obscured slot is up to the app. Some options:

- close the panel that covers a slot the user needs;
- render a "back to the channel" button in the covering panel, while `coverage.obscured.channel` is `true`;
- show a list on top only on request, as the SDK's stylesheet does below 768px.

The rest of this page fixes the example from the start: **View** in a thread closes the thread when it covers the channel.

There's a timing problem. **View** in the threads view switches to the channels view first, so at click time the channel isn't on screen and can't be measured. `requestReveal(slot)` records the intent instead. Later, once the slot has been measured, `resolveRevealAction(revealSlot, coverage)` says what to do:

| Result  | Meaning                                                        |
| ------- | -------------------------------------------------------------- |
| `wait`  | The slot hasn't been measured yet. Wait for the next update.   |
| `act`   | The slot is measured and obscured. Reveal it.                  |
| `clear` | There's no intent, or the slot is already visible. Do nothing. |

Here is the whole layout:

```tsx
import { useEffect } from "react";
import {
  ChannelHeader,
  ChannelNavigation,
  MessageAlsoSentInChannelIndicator,
  MessageComposer,
  MessageList,
  ThreadHeader,
  ThreadList,
  useMessageAlsoSentInChannelNavigation,
  useWorkspaceNavigation,
  WithComponents,
} from "stream-chat-react";
import {
  ChannelSlot,
  ChatView,
  ThreadSlot,
} from "stream-chat-react/slot-layout";
import type { LayoutDescriptor } from "stream-chat-react/slot-layout";
import {
  resolveRevealAction,
  SlotGeometryProvider,
  useSlotGeometry,
} from "stream-chat-react/slot-geometry";
import { Panel } from "./Panel";

const layouts: LayoutDescriptor[] = [
  { id: "channels", slots: ["channel", "thread"] },
  { id: "threads", slots: ["thread"] },
];

// "View" in a thread asks for the channel to be revealed, then jumps to the reply.
const RevealingAlsoSentIndicator = () => {
  const { isInThread, viewReference } = useMessageAlsoSentInChannelNavigation();
  const { requestReveal } = useSlotGeometry();

  return (
    <MessageAlsoSentInChannelIndicator
      onView={async () => {
        if (isInThread) requestReveal("channel");
        await viewReference();
      }}
    />
  );
};

// Closes the thread once the channel has been measured and is covered.
const RevealChannel = () => {
  const { clearReveal, coverage, revealSlot } = useSlotGeometry();
  const { closeThread } = useWorkspaceNavigation();

  useEffect(() => {
    const action = resolveRevealAction(revealSlot, coverage);
    if (action === "wait") return;
    if (action === "act") closeThread();
    clearReveal();
  }, [clearReveal, closeThread, coverage, revealSlot]);

  return null;
};

const ThreadPanel = () => (
  <Panel slot="thread">
    <ThreadSlot slot="thread">
      <ThreadHeader />
      <MessageList />
      <MessageComposer />
    </ThreadSlot>
  </Panel>
);

const ChannelsView = () => (
  <>
    <ChatView.Selector />
    <ChannelNavigation />
    <Panel slot="channel">
      <ChannelSlot slot="channel">
        <ChannelHeader />
        <MessageList />
        <MessageComposer />
      </ChannelSlot>
    </Panel>
    <ThreadPanel />
    <RevealChannel />
  </>
);

const ThreadsView = () => (
  <>
    <ChatView.Selector />
    <ThreadList />
    <ThreadPanel />
  </>
);

export const ResponsiveWorkspace = () => (
  <SlotGeometryProvider>
    <WithComponents
      overrides={{
        MessageAlsoSentInChannelIndicator: RevealingAlsoSentIndicator,
      }}
    >
      <ChatView
        layouts={layouts}
        views={{ channels: <ChannelsView />, threads: <ThreadsView /> }}
      />
    </WithComponents>
  </SlotGeometryProvider>
);
```

Here is what happens when the user clicks **View** in a thread:

1. `RevealingAlsoSentIndicator` asks for the `channel` slot to be revealed, then jumps to the reply in the channel.
2. `RevealChannel` runs whenever the intent or the measurements change. Until the `channel` slot has been measured, `resolveRevealAction` returns `wait`.
3. In the channels view on a phone, the thread covers the channel, so it returns `act`, and `closeThread()` closes the thread. The channel shows, at the reply.
4. On a wide screen, the channel is visible beside the thread, so it returns `clear`, and the thread stays open.
5. From the threads view, the jump opens the channel in the channels view first. Opening a channel closes the thread beside it there, so once the channel is measured, it returns `clear`.
6. Either way, `clearReveal()` drops the intent, so it acts once.

`SlotGeometryProvider` wraps `ChatView`, not one view, so the intent made in the threads view is still there when the channels view shows. `RevealChannel` sits in the channels view, where the `channel` slot is.

In the channels view, the channel is already on screen when the user clicks. There, `isObscured("channel")` in the click handler would also answer at once. `requestReveal` covers both cases with one code path.

## Used in

- [Single-pane mobile with back button](https://getstream.io/chat/docs/sdk/react/v15/recipes/layout/single-pane-mobile-with-back-button/)
- [Collapsible sidebar](https://getstream.io/chat/docs/sdk/react/v15/recipes/app/collapsible-sidebar/)

The full API is in [Slot geometry](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/slot-geometry/).

---

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