Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

Slot geometry

Slot geometry reports which slots are obscured on screen: collapsed to nothing, or covered by another slot. It only reports: it never moves, hides or closes anything. Responsive layouts explains when an app needs it and what to do with the result.

Everything on this page imports from stream-chat-react/slot-geometry. 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.

The problem it solves

ChatView knows which slots are open, not which ones the user can see. On a phone, the app's CSS may lay an open thread over the channel at full width: both slots are open, but the channel is covered. Slot geometry measures the elements that show the slots and reports that, without knowing the app's breakpoints or class names.

Usage

  1. Wrap the layout in SlotGeometryProvider.
  2. Register the element of each slot with the ref that useRegisterSlotGeometry(slot) returns.
  3. Read the result with useSlotGeometry().
import type { PropsWithChildren, ReactNode } from "react";
import {
  SlotGeometryProvider,
  useRegisterSlotGeometry,
  useSlotGeometry,
} from "stream-chat-react/slot-geometry";

const Panel = ({ children, slot }: PropsWithChildren<{ slot: string }>) => {
  const register = useRegisterSlotGeometry(slot);
  return (
    <div className={`panel panel--${slot}`} ref={register}>
      {children}
    </div>
  );
};

const CoveredNotice = () => {
  const { coverage } = useSlotGeometry();
  return coverage.obscured.channel ? <p>The channel is covered.</p> : null;
};

export const Workspace = ({
  channel,
  thread,
}: {
  channel: ReactNode;
  thread: ReactNode;
}) => (
  <SlotGeometryProvider>
    <Panel slot="channel">{channel}</Panel>
    <Panel slot="thread">{thread}</Panel>
    <CoveredNotice />
  </SlotGeometryProvider>
);

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.

Detecting covered and collapsed slots has the CSS that lays a thread over the channel on a phone.

What counts as obscured

A registered slot is obscured when either holds:

  • It's collapsed: its area is under 1px². That covers zero width or height, and display: none.
  • It's covered: another registered slot covers at least 60% of its area. A slot beside it, touching only its edge, doesn't count.

The boxes are compared without regard to which one is drawn on top, so two slots laid over each other usually both count as obscured.

The slots are measured again when one of their elements changes size, when the window is resized, and when an element is registered or removed, at most once per animation frame. coverage changes only when a slot's result flips. A slot that isn't registered isn't in coverage.obscured, and isObscured returns false for it. Outside a SlotGeometryProvider, useSlotGeometry() reports nothing as obscured, and its functions do nothing.

Deferred reveal

Right after a view switch, the slot to reveal may not be measured yet. requestReveal(slot) records the intent to reveal it, and resolveRevealAction(revealSlot, coverage) says what to do once it's measured:

Result When What to do
"wait" revealSlot isn't in coverage.obscured yet: it hasn't been measured. Nothing. Wait for the next coverage.
"act" The slot is measured and obscured. Reveal it, then call clearReveal().
"clear" There's no intent, or the slot is measured and visible. Call clearReveal().

resolveRevealAction is pure and synchronous, so call it from an effect that depends on revealSlot and coverage. The intent belongs to the provider, so a provider around ChatView keeps it across a view switch. Deciding what to show has a complete example: View in a thread closes the thread when it covers the channel.

API

Export Type Description
SlotGeometryProvider component, takes children Measures the slots registered inside it.
useSlotGeometry() () => SlotGeometryContextValue Reads the nearest provider.
useRegisterSlotGeometry(slot) (slot: SlotId) => (element: HTMLElement | null) => void Returns a stable callback ref that registers the element of slot, and removes it on unmount.
resolveRevealAction (revealSlot: SlotId | undefined, coverage: SlotCoverage) => RevealAction Decides what to do about a pending reveal. See Deferred reveal.

SlotGeometryContextValue, what useSlotGeometry() returns:

Member Type Description
coverage SlotCoverage { obscured: Record<SlotId, boolean> }, for every registered slot. Kept up to date.
isObscured (slot: SlotId) => boolean Measures the slot now.
register (slot: SlotId, element: HTMLElement | null) => void Registers the element of a slot; null removes it. useRegisterSlotGeometry calls it.
requestReveal (slot: SlotId) => void Records the intent to reveal a slot.
revealSlot SlotId | undefined The slot an intent names, or undefined. Re-renders when it changes.
clearReveal () => void Drops the intent.

The types: SlotId is string. RevealAction is "wait" | "act" | "clear". SlotCoverage is { obscured: Record<SlotId, boolean> }.