On this page Bottom Sheet Bottom Sheet

The m3e-bottom-sheet component is a sheet used to show secondary content anchored to the bottom of the screen.

import "@m3e/web/bottom-sheet";
Usage

This section outlines usage examples and configuration guidance for the components in this package.

NOTE: this page is presented in a nested iframe.

Basic usage

Use the m3e-bottom-sheet to present content anchored to the bottom of the screen. Bottom sheets are closed by default. You can open a sheet using a m3e-bottom-sheet-trigger nested inside any clickable element. You can also open a sheet declaratively with the open attribute or programmatically with the show method. To close a sheet, use a m3e-bottom-sheet-action inside a clickable element, remove the open attribute, or call the hide method programmatically.

Open bottom sheet Google Keep Add to a note Google Docs Embed in a document
<m3e-button>
  <m3e-bottom-sheet-trigger for="bottomSheet">
    Open bottom sheet
  </m3e-bottom-sheet-trigger>
</m3e-button>
<m3e-bottom-sheet id="bottomSheet">
  <m3e-action-list>
    <m3e-list-action>
      <m3e-bottom-sheet-action>Google Keep</m3e-bottom-sheet-action>
      <span slot="supporting-text">Add to a note</span>
    </m3e-list-action>
    <m3e-list-action>
      <m3e-bottom-sheet-action>Google Docs</m3e-bottom-sheet-action>
      <span slot="supporting-text">Embed in a document</span>
    </m3e-list-action>
  </m3e-action-list>
</m3e-bottom-sheet>
Lifecycle events

Bottom sheets emit four lifecycle events that reflect their transition between open and closed states. These events fire whether the sheet is opened programmatically, by a trigger, by keyboard interaction, or through user gestures.

These events give you full control over the sheet's lifecycle, including validation, analytics, and custom behaviors during open and close transitions.

Variants

Bottom sheets come in two variants: standard and modal. Both share the same anatomy and resizing behavior, but they differ in how they interact with the rest of the page. A standard bottom sheet is non-blocking. It sits above the page without trapping focus or preventing interaction with the underlying UI. A modal bottom sheet blocks interaction with the rest of the page until dismissed. It presents a backdrop (scrim), traps focus, supports autofocus on open, and restores focus to the trigger when closed.

Bottom sheets are standard by default. Add the modal attribute to m3e-bottom-sheet to create a modal bottom sheet.

Open modal bottom sheet Google Keep Add to a note Google Docs Embed in a document
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal>
    <!-- Content omitted for brevity -->
</m3e-bottom-sheet>
Modal behavior

When opened in modal mode, the bottom sheet behaves like a dialog and takes exclusive control of user interaction. Focus moves into the sheet automatically, and if any element inside the sheet has the autofocus attribute, it receives focus first. While the sheet is open, the rest of the document becomes inert, preventing keyboard and assistive-technology users from interacting with content behind the sheet. Page scrolling is also locked to keep the underlying layout stable.

Modal sheets display a scrim behind the sheet. Activating the scrim triggers a cancelable cancel event, allowing the sheet to close unless the you intercept it. When the sheet closes, focus is restored to the trigger that opened it, ensuring users return to their previous position in the page without losing context.

Drag handles

Bottom sheets support an optional drag handle at the top edge of the sheet. The handle consists of two parts:

Add the handle attribute to m3e-bottom-sheet to display the drag handle and use the handle-label attribute to provide an accessible label (defaults to "Drag handle").

Open draggable modal bottom sheet Google Keep Add to a note Google Docs Embed in a document
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal handle>
    <!-- Content omitted for brevity -->
</m3e-bottom-sheet>
Detents

Detents define the vertical positions a bottom sheet can rest at. A sheet may have no detents, a single detent, or multiple detents. Detents control how the sheet resizes, how it responds to drag gestures, and how the handle button behaves.

Detents are declared using the detents attribute on m3e-bottom-sheet. Each detent represents a target height the sheet can snap to.

Open draggable modal bottom sheet with detents Google Keep Add to a note Google Docs Embed in a document
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal handle detents="fit half full">
    <!-- Content omitted for brevity -->
</m3e-bottom-sheet>
Types of detents

Bottom sheets support two kinds of detents: named detents and unit-based detents. Both define the vertical positions the sheet can snap to, but they serve different purposes.

Named detents are semantic height presets including collapsed, fit, half, and full that map to familiar bottom-sheet behaviors. They provide predictable, expressive sizing without requiring you to think in pixels or percentages.

Unit-based detents use explicit measurements (percentages or pixels) to give you precise control over the sheet's height. These behave just like named detents but allow for custom layouts and fine-tuned sizing.

You can mix and match named and unit-based detents in any order. The sheet snaps between them based on drag gestures, velocity, and the order in which they are declared.

Initial height

The initial height defines the sheet's entry point into the interaction model. For example:

When a bottom sheet opens, it snaps to its initial detent. By default, this is the first detent listed in the detents attribute. You can override this by setting the detent attribute (zero-based index), which selects the detent the sheet should open to.

In the following example, the sheet opens at half instead of fit.

Open draggable modal bottom sheet with detents at half Google Keep Add to a note Google Docs Embed in a document
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal handle detents="fit half full" detent="1">
    <!-- Content omitted for brevity -->
</m3e-bottom-sheet>

You can also set the initial detent on a m3e-bottom-sheet-trigger using its detent attribute, allowing each trigger to open the sheet at a different height.

Open draggable modal bottom sheet with detents at half via trigger Google Keep Add to a note Google Docs Embed in a document
<m3e-button>
  <m3e-bottom-sheet-trigger for="bottomSheet" detent="1">
    Open draggable modal bottom sheet with detents at half via trigger
  </m3e-bottom-sheet-trigger>
</m3e-button>
<m3e-bottom-sheet id="bottomSheet" modal handle detents="fit half full">
  <!-- Content omitted for brevity -->
</m3e-bottom-sheet>
Collapsed height

The collapsed state always includes the sheet's header. This ensures the sheet remains visible, discoverable, and interactive even at its lowest detent. You can optionally reveal a portion of the content area beneath the header by setting the --m3e-bottom-sheet-peek-height CSS custom property. This value applies only to the content area and defaults to 0px, meaning the collapsed state shows the header alone.

When the sheet is collapsed, the content area is always marked inert. This applies whenever the sheet's rendered height falls at or below the combined height of the header and the configured peek height. In this state, only the header is interactive; the content cannot receive focus, pointer events, or screen-reader navigation. This prevents users from interacting with partially hidden elements and ensures the collapsed state behaves as a non-interactive preview. The content becomes interactive again only after the sheet expands above the collapsed height.

Open draggable modal bottom sheet with detents at collapsed with custom peek Google Keep Add to a note Google Docs Embed in a document
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet 
  id="bottomSheet"
  modal 
  handle 
  detents="collapsed fit half full"
  style="--m3e-bottom-sheet-peek-height: 36px">
  <!-- Content omitted for brevity -->
</m3e-bottom-sheet>
Hideability

A bottom sheet is hideable when it can be dismissed by dragging downward past its lowest detent. This allows the sheet to behave like a modal dialog or a temporary surface that can be fully removed from view. You enable this behavior by adding the hideable attribute to m3e-bottom-sheet.

Open hideable modal bottom sheet Google Keep Add to a note Google Docs Embed in a document
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal handle hideable>
    <!-- Content omitted for brevity -->
</m3e-bottom-sheet>
Open hideable modal bottom sheet with detents Google Keep Add to a note Google Docs Embed in a document
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal handle hideable detents="fit half full">
    <!-- Content omitted for brevity -->
</m3e-bottom-sheet>
Headers

The m3e-bottom-sheet provides a header named slot for supplying custom content within the sheet's draggable header region. When a header is provided, it appears directly below the optional drag handle. The handle, if enabled, is never replaced by custom header content.

If the handle is not enabled, the entire header region—including any slotted header content—remains draggable. This ensures the sheet always preserves a reliable drag surface regardless of whether a handle is present. If dragging is undesired, place header content in the default slot and omit the handle attribute.

Open hideable modal bottom sheet with detents and header Choose a destination Google Keep Add to a note Google Docs Embed in a document
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal handle hideable detents="collapsed fit half full">
  <m3e-heading id="sheetTitle" slot="header" variant="title" size="large">
    Choose a destination
  </m3e-heading>
    <!-- Content omitted for brevity -->
</m3e-bottom-sheet>
Accessibility

Standard bottom sheets use role="region", which identifies the sheet as a labeled section of the page. Because they are not modal, they do not trap focus, support autofocus, or block interaction with the rest of the document.

Bottom sheets are typically opened by a trigger element. The primary trigger exposes the correct ARIA relationships so assistive technologies understand that activating it will reveal the sheet. The trigger automatically receives aria-controls referencing the sheet's id, and its aria-expanded attribute is updated as the sheet opens and closes. Standard sheets are also labelled by their primary trigger when that trigger provides an id. Secondary triggers do not participate in ARIA ownership and receive no accessibility attributes; they simply open the sheet. Triggers are considered primary by default; use the secondary attribute on m3e-bottom-sheet-trigger to opt out of ARIA ownership when multiple triggers open the same sheet.

Modal bottom sheets use role="dialog" and aria-modal="true", which identify the sheet as a blocking interaction surface. Unlike standard sheets, modal sheets trap focus, prevent interaction with the rest of the page, and require an explicit action to dismiss. The sheet must have an accessible name, provided by either aria-labelledby or aria-label, so assistive technologies can announce its purpose when it opens.

Modal sheets are also opened by a trigger element. The trigger communicates that activating it will open a modal dialog by receiving aria-haspopup="dialog".

Modal sheets trap focus and keyboard navigation is contained within the sheet while it is open. Tabbing cycles through the sheet's focusable elements, wrapping from the last element back to the first. The rest of the page is made inert, preventing background content from receiving focus or pointer interaction. When the sheet is dismissed, focus returns to the trigger that opened it, preserving user context and aligning with established dialog accessibility patterns.

Native module support

The @m3e/web package uses JavaScript Modules. To use it directly in a browser without a bundler, use a module script similar to the following.

<script type="module" src="/node_modules/@m3e/web/dist/bottom-sheet.js"></script>

In addition, you must use an import map to include additional dependencies.

<script type="importmap">
  {
    "imports": {
      "tslib": "https://cdn.jsdelivr.net/npm/tslib@2.8.1/+esm",
      "lit": "https://cdn.jsdelivr.net/npm/lit@3.3.0/+esm",
      "lit/": "https://cdn.jsdelivr.net/npm/lit@3.3.0/",
      "lit-html": "https://cdn.jsdelivr.net/npm/lit-html@3.3.0/+esm",
      "lit-html/directive.js": "https://cdn.jsdelivr.net/npm/lit-html@3.3.0/directive.js",
      "lit-html/directives/if-defined.js": "https://cdn.jsdelivr.net/npm/lit-html@3.3.0/directives/if-defined.js",
      "lit-html/directives/class-map.js": "https://cdn.jsdelivr.net/npm/lit-html@3.3.0/directives/class-map.js",
      "@lit/reactive-element": "https://cdn.jsdelivr.net/npm/@lit/reactive-element@2.0.4/+esm",
      "@lit/reactive-element/": "https://cdn.jsdelivr.net/npm/@lit/reactive-element@2.0.4/",
      "@m3e/web/core": "/node_modules/@m3e/web/dist/core.js",
      "@m3e/web/core/a11y": "/node_modules/@m3e/web/dist/core-a11y.js"
    }
  }
</script>

For production builds, use the minified files to ensure optimal load performance.

API

The @m3e/web package includes a Custom Elements Manifest (custom-elements.json), which documents the properties, attributes, slots, events and CSS custom properties of each component.

You can explore the API below, or integrate the manifest into your own tooling.