Skip to content

ExpressiveCSS

Browse documentation

Bottom sheet

Secondary content anchored to the bottom of the screen.

View as Markdown

On this page

Use a dialog.bottom-sheet for additional or secondary content on compact and medium windows, not for the app's main view. The dialog is the surface. People dismiss it to get back to the page.

Tokens follow the M3 bottom sheet spec. The container uses surface-container-low, with 28dp top corners and elevation 1. Max width is 640dp. From the Medium breakpoint the sheet is inset 56dp from the sides. It never covers the top 72dp of the viewport. The drag handle is a 32×4dp bar in a 48dp hit target.

The same sheet has two variants:

  • Modal uses showModal(). A scrim sits behind the sheet and the page is inert. Tap the scrim, drag the handle down, or submit a <form method="dialog"> to dismiss.
  • Standard uses show(). It has no scrim, and the page stays interactive. Drag the handle down or use a close action to dismiss.

Open file

  • Draft.md
  • Cover.png
  • Archive

Now playing

Secondary controls for the current track. The page behind this sheet stays usable.

<button type="button" onclick="document.getElementById('sheet').showModal()">
  Show modal
</button>

<dialog id="sheet" class="bottom-sheet" aria-labelledby="open-file-title">
  <h2 id="open-file-title">Open file</h2>
  <div>…</div>
  <form method="dialog">
    <button type="submit" class="text" value="cancel">Cancel</button>
  </form>
</dialog>

Pick the variant by calling showModal() or show(). The modal variant has no extra class. A ::before pseudo-element draws the handle. If you want a real element in that slot, put a drag handle first.

Handle

Drag the top 48dp (or an explicit drag handle) down to dismiss the sheet. A recent flick counts even if the pointer stops moving at release. On a short drag, pausing for at least 100ms or reversing direction lets the sheet snap back. If the body overflows, the rest of the sheet scrolls.

On touch screens, give the sheet an explicit drag handle. The handle sets touch-action: none. Without it, the browser takes a touch drag on the built-in band as a scroll.

<dialog class="bottom-sheet" aria-labelledby="title-title">
  <span class="drag-handle" aria-hidden="true"></span>
  <h2 id="title-title">Title</h2>
  …
</dialog>

A <span> in that slot is decoration and does nothing on its own. Keyboard users do not need the bar. Dragging is a pointer gesture, and Esc already dismisses the sheet from the keyboard.

For a visible dismiss control, make the handle a <button> and give it a name. Activating it with a pointer or with Enter closes the sheet, so dragging is not the only way to use it. A drag that snaps back does not also dismiss the sheet. The component tells the click that ends a drag apart from a tap, so the sheet stays open.

<dialog class="bottom-sheet" aria-labelledby="title-title">
  <button type="button" class="drag-handle" aria-label="Dismiss"></button>
  <h2 id="title-title">Title</h2>
  …
</dialog>

Add expandable to give the sheet two heights. It opens partly expanded, at --md-comp-bottom-sheet-partial-height (50% of the viewport). .expanded grows it to the full height, 72dp short of the top. The height moves on the sheet's spring.

A drag up on the handle, or a flick up, expands the sheet. A drag down from expanded collapses it. A drag down from the partial height dismisses it as usual. Closing the sheet puts it back to partial.

A handle <button> switches between the heights instead of closing. The component keeps its aria-expanded current, so name the button for the height, not for dismissal. Because the handle no longer closes the sheet, give an expandable sheet an explicit close control.

<dialog class="bottom-sheet expandable" aria-labelledby="filters-title">
  <button type="button" class="drag-handle" aria-label="Sheet height" aria-expanded="false"></button>
  <h2 id="filters-title">Filters</h2>
  <div>…</div>
  <form method="dialog">
    <button type="submit" class="text">Done</button>
  </form>
</dialog>

Layout

On compact windows (below 600px) the sheet is full-bleed and flush to the bottom. From the Medium breakpoint the sheet is at most 640dp wide with 56dp side inset. It always stops 72dp short of the top. On large screens a side sheet or a basic dialog is usually a better fit. The bottom sheet still lays out if you open one there.

The sheet slides in, and snaps back after a short drag, on the default spatial spring. --md-comp-bottom-sheet-motion and --md-comp-bottom-sheet-motion-duration read --md-sys-motion-spring-default-spatial and its -duration token, so motion="expressive" gives it a slight bounce. The sheet extends 24px below the viewport, so the bounce never uncovers the page under it. With reduced motion requested, it appears without moving.

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow