Skip to content

Drawer

A dialog that slides in from an edge of the screen, such as the navigation of a page on a phone.

import { Button, Drawer, Portal } from '@contour-ui/react';

<Drawer.Root>
  <Drawer.Trigger asChild>
    <Button variant="outline">Menu</Button>
  </Drawer.Trigger>
  <Portal>
    <Drawer.Backdrop />
    <Drawer.Positioner>
      <Drawer.Content>
        <Drawer.CloseTrigger />
        <Drawer.Header>
          <Drawer.Title>contour-ui</Drawer.Title>
        </Drawer.Header>
        <Drawer.Body>
          <nav aria-label="Sections">…</nav>
        </Drawer.Body>
      </Drawer.Content>
    </Drawer.Positioner>
  </Portal>
</Drawer.Root>;

A drawer is a Dialog that slides in from an edge of the screen: the same parts on the Headless Dialog, with the same behavior. While it is open, focus stays inside it, the page does not scroll and the rest of it is out of reach; Escape, a press on the scrim, the x and the other close triggers close it, and focus goes back to the trigger. It is a panel on the surface color, with a border on its inner side and a shadow, and it slides back out when it closes.

Placement

placement on the root is the edge the drawer slides in from. start, the default, and end follow the direction of the text: in a left-to-right page they are the left and the right edge, and in a right-to-left page the other way round. From a side, the drawer is as high as the screen; from top or bottom, it is as wide as the screen and as high as its content.

<Drawer.Root placement="end">…</Drawer.Root>

Size

size sets how far the drawer reaches in: from a side, its width, sm (320 px, the default), md (448 px) or lg (640 px); from the top or the bottom, its greatest height. full covers the screen. The drawer is never larger than the screen.

The parts

They are the parts of Dialog, with their own classes: Drawer.CloseTrigger is the x in the top end corner, or, with asChild, a button of yours that closes the drawer; the header stacks the title and the description; the body takes the rest of the height and scrolls, from edge to edge of the drawer, opening scrolled to the element focus moves to; the footer sits at the far end and lays out its actions in a row. Put the backdrop and the positioner in a Portal.

Open and close

Every option of the Headless Dialog works the same here: defaultOpen, open and onOpenChange to keep the state yourself, closeOnEscape, closeOnInteractOutside, initialFocusEl, finalFocusEl. Close a navigation drawer once a link is followed: keep open in your state, and set it to false when the page changes.

const [open, setOpen] = useState(false);
const { pathname } = useLocation();
useEffect(() => {
  setOpen(false);
}, [pathname]);

<Drawer.Root open={open} onOpenChange={({ open }) => setOpen(open)}>
  …
</Drawer.Root>;

Anatomy

PartElementClassAttributes
Drawer.Rootnone——
Drawer.Triggerbuttoncontour-drawer__triggerdata-scope="dialog" data-part="trigger", aria-haspopup, aria-expanded, data-state
Drawer.Backdropdivcontour-drawer__backdropdata-part="backdrop", data-state
Drawer.Positionerdivcontour-drawer__positionerdata-part="positioner", data-state, data-placement
Drawer.Contentdivcontour-drawer__contentdata-part="content", role="dialog", aria-modal, data-state, data-placement, data-size
Drawer.CloseTriggerbuttoncontour-drawer__close-triggerdata-part="close-trigger"; with asChild, the class of your element
Drawer.Headerdivcontour-drawer__headerdata-part="header"
Drawer.Bodydivcontour-drawer__bodydata-part="body"
Drawer.Footerdivcontour-drawer__footerdata-part="footer"
Drawer.Titleh2contour-drawer__titledata-part="title", id
Drawer.Descriptionpcontour-drawer__descriptiondata-part="description", id

A drawer is a dialog: every part carries data-scope="dialog". Style a drawer by its classes.

Props

Drawer.Root takes every prop of the Headless Dialog, and:

PropValuesDefault
placement'start', 'end', 'top', 'bottom''start'
size'sm', 'md', 'lg', 'full''sm'

Drawer.CloseTrigger takes the props of CloseButton, size ('md', a 24 px target) among them, or, with asChild, renders its child. Every other part takes asChild and the props of its element; className is merged with the part's class.

Keyboard

KeyAction
Enter, SpaceOn the trigger: opens the drawer. On a close trigger: closes it
Tab, Shift+TabMove focus to the next or the previous element, round the drawer
EscapeCloses the drawer

Accessibility

  • The content is a modal dialog, named by its title: screen readers announce it as focus moves in, and the rest of the page is inert while it is open.
  • Focus goes back to the trigger, such as the menu button, when the drawer closes.
  • start and end follow the direction of the text, so the drawer slides in from the side readers of the page expect.
  • Under prefers-reduced-motion, the drawer fades in and out without sliding.

Tokens

TokenUsed for
--ct-color-overlay-scrimthe backdrop
--ct-color-bg-surfacethe background of the content
--ct-color-border-default, --ct-border-width-defaultthe border on the inner side
--ct-shadow-overlaythe shadow of the content
--ct-size-drawer-sm, --ct-size-drawer-md, --ct-size-drawer-lghow far the content reaches in, by size
--ct-space-panel-insetthe padding of the content and the gap of its parts
--ct-space-control-gap, --ct-space-group-gapthe header, the body and the footer
--ct-text-heading, --ct-color-fg-defaultthe title
--ct-text-body, --ct-color-fg-mutedthe description; other content is fg-default
--ct-size-control-mdthe x, and the room the header makes for it
--ct-layer-overlaythe z-index of the overlay, plus --layer-index
--ct-motion-enter, --ct-motion-exitthe slide of the content and the fade of the backdrop