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";
This section outlines usage examples and configuration guidance for the components in this package.
NOTE: this page is presented in a nested iframe.
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.
<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>
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.
opening — Fired when the sheet begins opening. This event is cancelable, allowing you to prevent
the sheet from opening by calling event.preventDefault().
opened — Fired after the sheet finishes opening and reaches its initial detent.closing — Fired when the sheet begins closing. This event is also cancelable, allowing you to
intercept or block dismissal (for example, to confirm unsaved changes).
closed — Fired after the sheet finishes closing and is fully hidden.These events give you full control over the sheet's lifecycle, including validation, analytics, and custom behaviors during open and close transitions.
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.
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal>
<!-- Content omitted for brevity -->
</m3e-bottom-sheet>
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.
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").
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal handle>
<!-- Content omitted for brevity -->
</m3e-bottom-sheet>
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.
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal handle detents="fit half full">
<!-- Content omitted for brevity -->
</m3e-bottom-sheet>
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.
The initial height defines the sheet's entry point into the interaction model. For example:
collapsed creates a “peek” experience.fit shows content immediately.half or full creates a more immersive modal feel.
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.
<!-- 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.
<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>
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.
<!-- 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>
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.
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal handle hideable>
<!-- Content omitted for brevity -->
</m3e-bottom-sheet>
<!-- Trigger omitted for brevity -->
<m3e-bottom-sheet id="bottomSheet" modal handle hideable detents="fit half full">
<!-- Content omitted for brevity -->
</m3e-bottom-sheet>
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.
<!-- 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>
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.
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.
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.