Back to Sentry

GlobalDrawer

static/app/components/core/drawer/drawer.mdx

26.5.19.1 KB
Original Source

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>

jsx
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 openDrawer updates a global context. All subscribed components re-render on every call, which can cause infinite loops. Guard with isDrawerOpen (as above), or wrap openDrawer in useCallback with stable dependencies.

useDrawer

useDrawer() 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.

FieldTypeDescription
openDrawer(renderer, options) => voidOpens 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() => voidCloses 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.
isDrawerOpenbooleantrue 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.
isAnyDrawerOpenbooleantrue 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.
panelRefReact.RefObject<HTMLDivElement | null>Ref to the rendered drawer panel element. null when no drawer is open. Use for positioning or measurement; do not mutate.

Scoped vs global state

tsx
// 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>;
}

Modes

The mode option on openDrawer controls scroll locking, click-outside, and URL-change behavior.

ModeScrollHeightClick outsideURL changeEscape
'blocking' (default)LockedFull PageClosesClosesCloses
'passive'Not lockedBelow TopBarStays openStays openCloses

'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.

jsx
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>

jsx
openDrawer(() => <DrawerHeader>My Drawer</DrawerHeader>, {
  ariaLabel: 'My Drawer',
  mode: 'passive',
});

Closing the Drawer

Escape

The drawer always closes on Escape in both modes.

Manual Close

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>

jsx
function MyPage() {
  const {openDrawer, closeDrawer} = useDrawer();

  return (
    <div>
      <button onClick={() => openDrawer(() => <p>Details</p>, {ariaLabel: 'Details'})}>
        Open
      </button>
      <button onClick={closeDrawer}>Close</button>
    </div>
  );
}

Click Outside

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.

jsx
// Don't close when clicking links:
openDrawer(() => null, {
  ariaLabel: 'My Drawer',
  shouldCloseOnInteractOutside: element => element.tagName !== 'A',
});

URL Change

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>

jsx
openDrawer(() => <DrawerHeader>My Drawer</DrawerHeader>, {
  ariaLabel: 'My Drawer',
  shouldCloseOnLocationChange: () => false,
});

Opening on URL State

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.

jsx
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.

Helper Components

DrawerHeader

DrawerHeader renders a sticky header at the top of the drawer with a built-in close button. Pass children as the title content.

DrawerBody

DrawerBody wraps drawer content with the correct padding, font size, scrolling, and overflow behavior.

<Storybook.Demo> <HelperComponentsDemo /> </Storybook.Demo>

jsx
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!')}
);