Drawer

dialog.drawer[data-ui="drawer"]

Drawer — a panel anchored to an edge of the viewport.

Examples

A side sheet on each inline edge

View “A side sheet on each inline edge” as

Filters

Menu

<button class="btn btn-outline" type="button"
        command="show-modal" commandfor="demo-drawer-end">Filters</button>
<button class="btn btn-outline" type="button"
        command="show-modal" commandfor="demo-drawer-start">Menu</button>

<dialog class="drawer drawer-end drawer-divided" id="demo-drawer-end"
        data-ui="drawer" aria-labelledby="demo-drawer-end-title">
  <header class="drawer-header">
    <h2 class="drawer-title" id="demo-drawer-end-title">Filters</h2>
    <button class="btn btn-quiet btn-icon drawer-close" type="button"
            command="close" commandfor="demo-drawer-end" aria-label="Close">
      <svg class="icon" aria-hidden="true"><use href="#icon-x"></use></svg>
    </button>
  </header>
  <div class="drawer-body">
    <div class="field field-inline">
      <input class="check" id="demo-drawer-instock" type="checkbox" checked />
      <label class="field-label" for="demo-drawer-instock">In stock only</label>
    </div>
    <div class="field">
      <label class="field-label" for="demo-drawer-sort">Sort by</label>
      <select class="select" id="demo-drawer-sort">
        <option>Relevance</option>
        <option>Price</option>
      </select>
    </div>
  </div>
  <footer class="drawer-footer">
    <button class="btn btn-primary" type="button"
            command="close" commandfor="demo-drawer-end">Apply</button>
  </footer>
</dialog>

<dialog class="drawer drawer-start" id="demo-drawer-start"
        data-ui="drawer" aria-labelledby="demo-drawer-start-title">
  <header class="drawer-header">
    <h2 class="drawer-title" id="demo-drawer-start-title">Menu</h2>
    <button class="btn btn-quiet btn-icon drawer-close" type="button"
            command="close" commandfor="demo-drawer-start" aria-label="Close">
      <svg class="icon" aria-hidden="true"><use href="#icon-x"></use></svg>
    </button>
  </header>
  <nav class="drawer-body" aria-label="Example">
    <ul class="menu-list">
      <li><a class="link" href="#demo-drawer-start">Overview</a></li>
      <li><a class="link" href="#demo-drawer-start">Reports</a></li>
      <li><a class="link" href="#demo-drawer-start">Settings</a></li>
    </ul>
  </nav>
</dialog>

A drawer is a <dialog>. The top layer, the backdrop, inert on the rest of the page, the focus trap, Escape and focus returned to the invoker are all the browser’s, exactly as for a centred modal.

What the platform does not give a dialog is closing when you click outside it, so data-ui="drawer" adds that — press and release are both checked, so a selection dragged out of a field inside the panel does not dismiss it, and data-drawer-dismiss="none" turns it off for a panel holding a form. The placements are logical, so .drawer-end is on the left in Arabic with no second rule. A drawer holding navigation says nothing about its contents — the links are a <nav> with a <ul>, because that is what they are.

A bottom sheet, and the two sizes

View “A bottom sheet, and the two sizes” as

Share this page

Anyone with the link can read it.

<button class="btn btn-outline" type="button"
        command="show-modal" commandfor="demo-drawer-sheet">Share</button>

<dialog class="drawer drawer-block-end drawer-sm" id="demo-drawer-sheet"
        data-ui="drawer" aria-labelledby="demo-drawer-sheet-title">
  <span class="drawer-handle" aria-hidden="true"></span>
  <header class="drawer-header">
    <h2 class="drawer-title" id="demo-drawer-sheet-title">Share this page</h2>
  </header>
  <div class="drawer-body">
    <p>Anyone with the link can read it.</p>
  </div>
  <footer class="drawer-footer">
    <button class="btn btn-quiet" type="button"
            command="close" commandfor="demo-drawer-sheet">Cancel</button>
    <button class="btn btn-primary" type="button"
            command="close" commandfor="demo-drawer-sheet">Copy link</button>
  </footer>
</dialog>

A bottom sheet sizes to its content up to --drawer-size, which is what makes a short sheet short.

Drag the grabber down and it closes. .drawer-handle is both the affordance and the opt-in — the gesture is wired only where the grabber is, so a sheet that can be dragged looks draggable and one that cannot does not. It closes past a third of its own height, or on a fast flick whatever the distance, and a drag the wrong way is damped rather than blocked. Escape and the close button remain the dismissals that work without a pointer; the gesture is an addition, never the only route.

Tokens 22

Level 2, declared on .drawer itself. Set any of them on that selector to restyle this component without touching the skin.

Level 2 tokens declared by drawer
TokenDefault
--drawer-backdropvar(--color-overlay)
--drawer-backdrop-filternone
--drawer-divider-colorvar(--color-border-subtle)
--drawer-durationvar(--duration-normal)
--drawer-enter0 0
--drawer-footer-gapvar(--space-2)
--drawer-handle-block-size0.25rem
--drawer-handle-colorvar(--color-border-strong)
--drawer-handle-inline-size2.5rem
--drawer-handle-padding-blockvar(--space-3)
--drawer-size22rem
--drawer-title-font-familyvar(--font-primary)
--drawer-title-font-sizevar(--font-size-lg)
--drawer-title-font-weightvar(--font-weight-semibold)
--drawer-bgvar(--color-surface-raised)
--drawer-border-colorvar(--color-border-subtle)
--drawer-border-widthvar(--border-width)
--drawer-colorvar(--color-on-surface)
--drawer-gapvar(--space-4)
--drawer-paddingvar(--space-5)
--drawer-radiusvar(--radius-box)
--drawer-shadowvar(--elevation-overlay)

Variants and states

Variants

  • .drawer-end
  • .drawer-lg
  • .drawer-sm

Inside it

  • .drawer-header
  • .drawer-body
  • .drawer-handle
  • .drawer-start
  • .drawer-block-end
  • .drawer-block-start
  • .drawer-divided
  • .drawer-footer
  • .drawer-close
  • .drawer-title

State it reads

Read from the platform, never mirrored into a class that could disagree with it.

  • [open]
  • data-dragging
  • data-legacy
  • data-ui

Before you ship it

This one needs a script. Opt the markup in with data-ui="drawer" and import import 'mostlycss/js/components/drawer' — that specifier and not the package root, which is a bundle carrying a second copy of the registry, so a page holding both initialises every element twice. What you lose without the script is listed below — for some components that is only polish, for others it is the whole interaction.

What you have to do 1 requirements

  1. Name it — aria-labelledby on the <dialog> pointing at .drawer-title, or an aria-label. Name the icon-only close button. If the drawer holds the site navigation, the links inside it are a <nav> with a <ul>; a drawer is a container and says nothing about what is in it.

src/css/components/drawer.css · npx mostlycss add drawer