static/app/components/core/drawer/drawer.mdx
import * as Storybook from 'sentry/stories';
import { BasicDemo, ClosingDemo, HelperComponentsDemo, PassiveModeDemo, ShouldCloseDemo, } from './stories/demos';
export const documentation = import('!!type-loader!@sentry/scraps/drawer');
GlobalDrawer is an application-wide singleton drawer that slides in from the right side of the page. Only one drawer can be open at a time. The drawer is controlled via the useDrawer() hook — no JSX required at the call site.
<Storybook.Demo> <BasicDemo /> </Storybook.Demo>
import {useDrawer, DrawerBody, DrawerHeader} from '@sentry/scraps/drawer';
function MyPage() {
const {openDrawer, isDrawerOpen} = useDrawer();
const showDetails = () => {
if (!isDrawerOpen) {
openDrawer(() => <MyDrawer title="Hello!" />, {ariaLabel: 'Details'});
}
};
return <button onClick={showDetails}>Open Drawer</button>;
}
function MyDrawer({title}: {title: string}) {
return (
<div>
<DrawerHeader>{title}</DrawerHeader>
<DrawerBody>Lorem, ipsum...</DrawerBody>
</div>
);
}
[!WARNING] Calling
openDrawerupdates a global context. All subscribed components re-render on every call, which can cause infinite loops. Guard withisDrawerOpen(as above), or wrapopenDrawerinuseCallbackwith stable dependencies.
useDraweruseDrawer() returns a stable set of helpers and state. The open/closed booleans are scoped per call-site, so different components can share the single GlobalDrawer without conflating whose drawer is whose.
| Field | Type | Description |
|---|---|---|
openDrawer | (renderer, options) => void | Opens the drawer with the given renderer and options. Replaces any currently open drawer. The drawer is associated with this useDrawer() call-site for the lifetime of that drawer. |
closeDrawer | () => void | Closes the currently open drawer, regardless of which call-site opened it. Unmounts the drawer content. Invokes the renderer's closeDrawer prop when called from inside the drawer. Does not trigger the onClose option. |
isDrawerOpen | boolean | true only when the drawer currently open was opened by this useDrawer() call-site. A shared wrapper hook (e.g. useSamplesDrawer, useUrlTraceDrawer) shares a single call-site across its consumers, so all callers of the wrapper see the same value. Use this for toggle buttons, disable-while-open buttons, and guards against reopening your own drawer. |
isAnyDrawerOpen | boolean | true when any drawer is open, regardless of who opened it. Escape hatch for call-sites that need the global state — e.g. a page-level effect that auto-opens a drawer only when nothing else is already open. Prefer isDrawerOpen unless you specifically need cross-call-site state. |
panelRef | React.RefObject<HTMLDivElement | null> | Ref to the rendered drawer panel element. null when no drawer is open. Use for positioning or measurement; do not mutate. |
// Component A — opens its own drawer.
function ButtonA() {
const {openDrawer, isDrawerOpen} = useDrawer();
return (
<button
disabled={isDrawerOpen} // true only while A's drawer is open
onClick={() => openDrawer(() => <p>A</p>, {ariaLabel: 'A'})}
>
Open A
</button>
);
}
// Component B — never calls openDrawer.
function StatusB() {
const {isDrawerOpen, isAnyDrawerOpen} = useDrawer();
// isDrawerOpen is always false here (B never opened anything).
// isAnyDrawerOpen is true when any drawer is open — including A's.
return <span>{isAnyDrawerOpen ? 'some drawer is open' : 'idle'}</span>;
}
The mode option on openDrawer controls scroll locking, click-outside, and URL-change behavior.
| Mode | Scroll | Height | Click outside | URL change | Escape |
|---|---|---|---|---|---|
'blocking' (default) | Locked | Full Page | Closes | Closes | Closes |
'passive' | Not locked | Below TopBar | Stays open | Stays open | Closes |
'blocking'The default. Use when the drawer requires the user's full attention. The intro example above uses blocking mode — clicking outside the panel closes it.
openDrawer(() => <DrawerHeader>My Drawer</DrawerHeader>, {
ariaLabel: 'My Drawer',
// mode defaults to 'blocking'
});
'passive'Use when the drawer should coexist with page interaction — for example, a details panel open alongside a live data table.
<Storybook.Demo> <PassiveModeDemo /> </Storybook.Demo>
openDrawer(() => <DrawerHeader>My Drawer</DrawerHeader>, {
ariaLabel: 'My Drawer',
mode: 'passive',
});
The drawer always closes on Escape in both modes.
Use closeDrawer from useDrawer() to close the drawer programmatically. The close button can be placed inside or outside the drawer.
<Storybook.Demo> <ClosingDemo /> </Storybook.Demo>
function MyPage() {
const {openDrawer, closeDrawer} = useDrawer();
return (
<div>
<button onClick={() => openDrawer(() => <p>Details</p>, {ariaLabel: 'Details'})}>
Open
</button>
<button onClick={closeDrawer}>Close</button>
</div>
);
}
In 'blocking' mode, clicking outside the drawer closes it. For fine-grained control, use shouldCloseOnInteractOutside — a function that receives the interacted element and returns false to prevent closing.
// Don't close when clicking links:
openDrawer(() => null, {
ariaLabel: 'My Drawer',
shouldCloseOnInteractOutside: element => element.tagName !== 'A',
});
In 'blocking' mode, the drawer closes when the URL changes. Override with shouldCloseOnLocationChange — a function that accepts the new Location and returns whether to close.
<Storybook.Demo> <ShouldCloseDemo /> </Storybook.Demo>
openDrawer(() => <DrawerHeader>My Drawer</DrawerHeader>, {
ariaLabel: 'My Drawer',
shouldCloseOnLocationChange: () => false,
});
Represent drawer state in the URL so the drawer reopens when the URL is shared or visited directly. Use useEffect to watch the URL and open the drawer when a query param is present.
import {useEffect} from 'react';
import {useDrawer} from '@sentry/scraps/drawer';
import {useLocation} from 'sentry/utils/useLocation';
function OverviewPage() {
const location = useLocation();
const {openDrawer, isDrawerOpen} = useDrawer();
useEffect(() => {
if (!isDrawerOpen && location.query.drawer) {
openDrawer(() => <ModalContent />, {ariaLabel: 'Hello Modal'});
}
}, [isDrawerOpen, location.query.drawer, openDrawer]);
return <p>Hello</p>;
}
The drawer closes automatically on URL change in 'blocking' mode, so no explicit close logic is needed. If the drawer contents update the URL themselves, specify shouldCloseOnLocationChange to prevent the drawer from closing unexpectedly.
DrawerHeaderDrawerHeader renders a sticky header at the top of the drawer with a built-in close button. Pass children as the title content.
DrawerBodyDrawerBody wraps drawer content with the correct padding, font size, scrolling, and overflow behavior.
<Storybook.Demo> <HelperComponentsDemo /> </Storybook.Demo>
import {DrawerBody, DrawerHeader} from '@sentry/scraps/drawer';
openDrawer(
() => (
<Fragment>
<DrawerHeader>My Drawer</DrawerHeader>
<DrawerBody>Lorem, ipsum...</DrawerBody>
</Fragment>
),
{ariaLabel: 'My Drawer', onClose: () => alert('Called my handler!')}
);