/Documentation

Bottom sheet

PublishedUpdated

A bottom sheet holds secondary content anchored to the bottom of the screen: sharing options, filters, the details of a place on a map. A modal sheet covers the page with a scrim until it closes; a standard sheet leaves the page usable beside it. See the M3 bottom sheets guidelines.

Usage

An open sheet is partially expanded, showing its content up to half the screen; dragging its handle up expands it to its full height, and down closes it.

Vanilla
import { createBottomSheet } from 'mtrl';

const sheet = createBottomSheet({
  variant: 'modal',
  title: 'Share',
  content: 'Anyone with the link can view it.',
});
document.body.append(sheet.element);

function share() {
  sheet.open();
}
Web Components
<m-bottom-sheet modal headline="Share">
  <p>Anyone with the link can view it.</p>
</m-bottom-sheet>

<script type="module">
  const bottomSheet = document.querySelector('m-bottom-sheet');

  function share() {
    bottomSheet.expand();
  }
</script>
React
import { useState } from 'react';
import { BottomSheet } from 'mtrl/react';

export function Example() {
  const [open, setOpen] = useState(false);
  const share = () => setOpen(true);

  return (
    <BottomSheet open={open} onClose={() => setOpen(false)} modal headline="Share">
      <p>Anyone with the link can view it.</p>
    </BottomSheet>
  );
}
Vue
<script setup lang="ts">
import { ref } from 'vue';
import { MBottomSheet } from 'mtrl/vue';

const open = ref(false);

function share() {
  open.value = true;
}
</script>

<template>
  <MBottomSheet :open="open" @close="open = false" modal headline="Share">
    <p>Anyone with the link can view it.</p>
  </MBottomSheet>
</template>
Svelte
<script lang="ts">
  import { BottomSheet } from 'mtrl/svelte';

  let open = $state(false);

  function share() {
    open = true;
  }
</script>

<BottomSheet open={open} onclose={() => (open = false)} modal headline="Share">
  <p>Anyone with the link can view it.</p>
</BottomSheet>
SolidJS
import { createSignal } from 'solid-js';
import { BottomSheet } from 'mtrl/solid';

export function Example() {
  const [open, setOpen] = createSignal(false);
  const share = () => setOpen(true);

  return (
    <BottomSheet open={open()} onClose={() => setOpen(false)} modal headline="Share">
      <p>Anyone with the link can view it.</p>
    </BottomSheet>
  );
}

Examples

Standard

A standard sheet has no scrim, so the page behind it still takes clicks.

Vanilla
import { createBottomSheet } from 'mtrl';

const sheet = createBottomSheet({
  variant: 'standard',
  title: 'Nearby places',
  content: 'Three cafés within a five-minute walk.',
});
document.body.append(sheet.element);
Web Components
<m-bottom-sheet headline="Nearby places">
  <p>Three cafés within a five-minute walk.</p>
</m-bottom-sheet>
React
import { BottomSheet } from 'mtrl/react';

export function Example() {
  return (
    <BottomSheet headline="Nearby places">
      <p>Three cafés within a five-minute walk.</p>
    </BottomSheet>
  );
}
Vue
<script setup lang="ts">
import { MBottomSheet } from 'mtrl/vue';
</script>

<template>
  <MBottomSheet headline="Nearby places">
    <p>Three cafés within a five-minute walk.</p>
  </MBottomSheet>
</template>
Svelte
<script lang="ts">
  import { BottomSheet } from 'mtrl/svelte';
</script>

<BottomSheet headline="Nearby places">
  <p>Three cafés within a five-minute walk.</p>
</BottomSheet>
SolidJS
import { BottomSheet } from 'mtrl/solid';

export function Example() {
  return (
    <BottomSheet headline="Nearby places">
      <p>Three cafés within a five-minute walk.</p>
    </BottomSheet>
  );
}

Peek height

peekHeight sets the partially expanded height in pixels; expand() and collapse() move between the two heights.

Vanilla
import { createBottomSheet } from 'mtrl';

const sheet = createBottomSheet({
  variant: 'modal',
  title: 'Filters',
  peekHeight: 160,
  content: 'Price, distance and opening hours.',
});
document.body.append(sheet.element);
Web Components
<m-bottom-sheet peek-height="160" modal headline="Filters">
  <p>Price, distance and opening hours.</p>
</m-bottom-sheet>
React
import { BottomSheet } from 'mtrl/react';

export function Example() {
  return (
    <BottomSheet peekHeight={160} modal headline="Filters">
      <p>Price, distance and opening hours.</p>
    </BottomSheet>
  );
}
Vue
<script setup lang="ts">
import { MBottomSheet } from 'mtrl/vue';
</script>

<template>
  <MBottomSheet :peek-height="160" modal headline="Filters">
    <p>Price, distance and opening hours.</p>
  </MBottomSheet>
</template>
Svelte
<script lang="ts">
  import { BottomSheet } from 'mtrl/svelte';
</script>

<BottomSheet peekHeight={160} modal headline="Filters">
  <p>Price, distance and opening hours.</p>
</BottomSheet>
SolidJS
import { BottomSheet } from 'mtrl/solid';

export function Example() {
  return (
    <BottomSheet peekHeight={160} modal headline="Filters">
      <p>Price, distance and opening hours.</p>
    </BottomSheet>
  );
}

Only its content closes it

For a step that must be finished, the scrim, Escape and the drag handle can be kept from closing it.

Vanilla
import { createBottomSheet } from 'mtrl';

const sheet = createBottomSheet({
  variant: 'modal',
  title: 'Before you continue',
  content: 'Review the updated terms.',
  dragHandle: false,
  closeOnScrimClick: false,
  closeOnEscape: false,
});
document.body.append(sheet.element);
Web Components
<m-bottom-sheet modal headline="Before you continue" no-drag-handle no-close-on-scrim-click no-close-on-escape>
  <p>Review the updated terms.</p>
</m-bottom-sheet>
React
import { BottomSheet } from 'mtrl/react';

export function Example() {
  return (
    <BottomSheet modal headline="Before you continue" noDragHandle noCloseOnScrimClick noCloseOnEscape>
      <p>Review the updated terms.</p>
    </BottomSheet>
  );
}
Vue
<script setup lang="ts">
import { MBottomSheet } from 'mtrl/vue';
</script>

<template>
  <MBottomSheet modal headline="Before you continue" no-drag-handle no-close-on-scrim-click no-close-on-escape>
    <p>Review the updated terms.</p>
  </MBottomSheet>
</template>
Svelte
<script lang="ts">
  import { BottomSheet } from 'mtrl/svelte';
</script>

<BottomSheet modal headline="Before you continue" noDragHandle noCloseOnScrimClick noCloseOnEscape>
  <p>Review the updated terms.</p>
</BottomSheet>
SolidJS
import { BottomSheet } from 'mtrl/solid';

export function Example() {
  return (
    <BottomSheet modal headline="Before you continue" noDragHandle noCloseOnScrimClick noCloseOnEscape>
      <p>Review the updated terms.</p>
    </BottomSheet>
  );
}

API

Options

Option Type Default Description
variant 'standard' | 'modal' 'modal' Whether it covers the page; the web component is standard without modal
title string undefined The headline, which names the sheet
content string | HTMLElement undefined The body, as HTML or an element
dragHandle boolean true The handle, a button, and dragging with it
peekHeight number undefined The partially expanded height in pixels; without it, the content up to half the screen
maxWidth number 640 The widest it grows, in pixels, past which it is centered
initialState 'hidden' | 'partial' | 'expanded' 'hidden' How far open it starts
closeOnScrimClick boolean true Whether a click on the scrim closes a modal sheet
closeOnEscape boolean true Whether Escape closes it; a standard sheet only from inside it
layer 'top' undefined Shows a modal sheet in the top layer, as a native <dialog> with showModal(); the web component's modal sheet always is
container HTMLElement document.body Where it is mounted
on { open?, close?, stateChange?, dragStart?, dragEnd? } undefined Event handlers registered at creation
class string undefined Additional CSS classes

Methods

Method Parameters Returns Description
open() / close() none BottomSheetComponent Opens it partially expanded, or closes it
expand() / collapse() none BottomSheetComponent Moves it to its full or its partial height, opening it if it is closed
isOpen() none boolean Whether it is open at either height
getState() none 'hidden' | 'partial' | 'expanded' How far open it is
setTitle(title) / setContent(content) title: string, content: string | HTMLElement BottomSheetComponent The headline, the body
on(event, handler) / off(event, handler) event: string, handler: Function BottomSheetComponent Adds or removes a listener
destroy() none void Removes it

Events

Event Description Data
open / close It opened or closed none
stateChange It moved between hidden, partial and expanded { state, previous }
dragStart / dragEnd A drag on the handle began, or ended and settled none / { state, previous }

The web component dispatches open, close, expand and collapse, without a detail; its expanded attribute reflects the full height.

Accessibility

  • A modal sheet is a dialog with aria-modal, a standard one a region; the title names either. Without a title, give the web component an aria-label.
  • Opening a modal sheet focuses it, and closing it gives focus back. Tab stays inside it and the page behind it is inert, in the top layer or not.
  • Escape closes a modal sheet from anywhere, and a standard one from inside it; a click on the scrim closes a modal sheet. Each can be turned off.
  • The drag handle is a button, as in Compose: it expands a partially open sheet and closes an expanded one, and its name says which ("Expand sheet", "Close sheet").

Styling

.mtrl-bottom-sheet { }                     /* the fixed layer, holding the scrim and the sheet */
.mtrl-bottom-sheet--modal, .mtrl-bottom-sheet--standard { }
.mtrl-bottom-sheet--hidden, .mtrl-bottom-sheet--partial, .mtrl-bottom-sheet--expanded { }
.mtrl-bottom-sheet__scrim { }              /* a modal sheet's, outside the top layer */
.mtrl-bottom-sheet__container { }          /* the sheet */
.mtrl-bottom-sheet__handle, .mtrl-bottom-sheet__header, .mtrl-bottom-sheet__title { }
.mtrl-bottom-sheet__content { }

Measurements

Attribute Value
Container surface-container-low, 28dp top corners, elevation 1
Maximum width 640dp
Drag handle A 48dp button drawing a 32×4dp bar in on-surface-variant; focus ring secondary
Headline Headline Small, on-surface
Content Body Medium, on-surface-variant, 24dp at the sides
Scrim scrim at 32%
Settling a drag 56dp, or a flick of 125dp/s