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
- Wrap the layout in
SlotGeometryProvider. - Register the element of each slot with the ref that
useRegisterSlotGeometry(slot)returns. - 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> }.