Skip to content

Drawer

Drawer is a siding content panel docked to the right side of a viewport. Drawers are toggled by the user and contain information or workflows related to a feature.
Figma logo
Illustration showing how a drawer is constructed
Anatomy of a Drawer
Anatomy of the Drawer component
ElementPurpose
HeaderHeaders are pinned to the top of a Drawer and contain an optional title describing the purpose of the Drawer and a required close action / × icon. Headers are always required because they contain the close action for the component.
ContentContent can contain almost any element, such as text, images, videos, or other components.
FooterFooter is optional since the close action in the Header allows a user to close the Drawer. When used, the Footer is pinned to the bottom of a Drawer and is sticky positioned, allowing content to scroll behind it.
ActionsActions should be placed inside DrawerActions inside DrawerFooter. Primary actions submit data or move a user to a new flow. Secondary actions are usually used to cancel or close the Drawer.
  • Drawer is available in two widths: medium, the default, which is 668px, and large, which is 960px.
  • Drawers span the entire height of the viewport. On smaller viewports (<600px/sm breakpoint), Drawer becomes fluid and occupies 100% of the viewport width.
Drawer on a mobile web browser
React props
NameTypeDefaultDescription
children  ReactNodeThe content of the Drawer.
onClose  funcCallback triggered when closing the Drawer. Applies to clicking the close action, clicking the backdrop, and pressing the escape key.
open  booleanfalseIf true, the Drawer will be shown.
persistent  booleanfalseBy default Drawers behave like pseudo-pages in the application whose children are not mounted until they are opened in order to reset state and to prevent loading data unnecessarily. If persistent is set, the dialog will mount immediately and stay mounted until it is removed by a parent element, similar to a standard component. This is useful for SEO and for keeping frequently accessed dialogs fast and responsive.
size  mediumlargemediumDetermines the max size of the Drawer.

The following testing snippet(s) offer suggestions for testing the component using React Testing Library with occasional help from Jest.

Setup

Each test code block depends on a stateful Drawer component which manages the open state and renders a Button that can be used to open the Drawer.

const StatefulDrawer = ({ open: defaultOpen, ...props }) => {
const [open, setOpen] = useState(defaultOpen);
return (
<>
<Button onClick={() => setOpen(true)}>Open Drawer</Button>
<Drawer {...props} open={open} onClose={() => setOpen(false)} />
</>
);
};

Example

render(
<>
<section>
<h1>Content hidden by Drawer</h1>
</section>
<StatefulDrawer aria-label="Enrollment completed">
<DrawerHeader heading="Enrollment completed" />
<DrawerBody>
<DrawerDescription>
<p>You’re all set up! Go with Gusto!</p>
</DrawerDescription>
</DrawerBody>
<DrawerFooter>
<DrawerActions>
<Button>Get started</Button>
</DrawerActions>
</DrawerFooter>
</StatefulDrawer>
</>,
);
// Search by aria-label, not heading
const drawer = screen.getByLabelText('Enrollment completed');
// Role may also be used for more specificity:
// const drawer = screen.getByRole('dialog', {
// name: 'Enrollment completed',
// });
//
// When the Drawer is closed an extra hidden option is needed:
// const drawer = screen.getByRole('dialog', {
// name: 'Enrollment completed',
// hidden: true,
// });
// Closed Drawers are not visible and their contents are not mounted
expect(drawer).not.toBeVisible();
expect(drawer).toBeEmptyDOMElement();
const openDrawerButton = screen.getByRole('button', {
name: 'Open Drawer',
});
userEvent.click(openDrawerButton);
expect(drawer).toBeVisible();
expect(drawer).toHaveAccessibleDescription('You’re all set up! Go with Gusto!');
const heading = within(drawer).getByRole('heading', {
name: 'Enrollment completed',
});
// Content outside of the modal is hidden from the accessibility tree.
// This means that all queries will only show results inside the Drawer. Searching
// for elements outside of an open Drawer is discouraged, but may sometimes be needed.
// To find these elements use the hidden option
const hiddenMainContent = screen.getByRole('heading', {
level: 1,
hidden: true,
});
// Focus set on the Drawer and trapped within the Drawer. When the Drawer is closed
// focus is returned to the opening element. Focus changes occur after a delay and
// need to be waited for.
await waitFor(() => expect(drawer).toHaveFocus());
// Use the escape key to close (preferred)
userEvent.type(drawer, '{esc}');
// Alternatively, using the close button
// const closeButton = within(drawer).getByRole('button', { name: 'Close' });
// userEvent.click(closeButton);
// Using the backdrop is discouraged and can be challenging because it has
// been removed from the accessibility tree intentionally.
// Focus will be returned to the opening element
await waitFor(() => expect(openDrawerButton).toHaveFocus());
const getStartedButton = within(drawer).getByRole('button', {
name: 'Get started',
});
userEvent.click(getStartedButton);