Skip to content

Containment

Side sheets

A surface at the side of the window for supplementary content, standard beside the page or modal over it.

Examples

Modal side sheet

At the window's end edge over a scrim, with actions at the bottom.

The live demo runs with JavaScript. Its code is below.

React HTML
import { useState } from 'react';
import { Button, Checkbox, SideSheet } from '@kyralabs/m3exp-web/react';

export default function Modal() {
  const [open, setOpen] = useState(false);
  const close = () => {
    setOpen(false);
  };
  return (
    <>
      <Button
        variant="tonal"
        onPress={() => {
          setOpen(true);
        }}
      >
        Filters
      </Button>
      <SideSheet
        variant="modal"
        isOpen={open}
        onOpenChange={setOpen}
        headline="Filters"
        divider
        actions={
          <>
            <Button variant="filled" onPress={close}>
              Apply
            </Button>
            <Button variant="outlined" onPress={close}>
              Cancel
            </Button>
          </>
        }
      >
        <Checkbox defaultSelected>Free shipping</Checkbox>
        <Checkbox>In stock</Checkbox>
      </SideSheet>
    </>
  );
}
<button type="button" class="m3e-button m3e-button--tonal" id="open-filters">Filters</button>
<m3e-side-sheet id="filters" variant="modal" headline="Filters" divider>
  <m3e-checkbox checked>Free shipping</m3e-checkbox>
  <m3e-checkbox>In stock</m3e-checkbox>
  <button type="button" class="m3e-button m3e-button--filled" slot="actions">Apply</button>
  <button type="button" class="m3e-button m3e-button--outlined" slot="actions">Cancel</button>
</m3e-side-sheet>

<script type="module">
  const sheet = document.getElementById('filters');
  document.getElementById('open-filters').addEventListener('click', () => sheet.show());
  sheet
    .querySelectorAll('[slot="actions"]')
    .forEach((b) => b.addEventListener('click', () => sheet.close()));
</script>

Run the HTML version (custom elements, no framework).

Standard side sheet

At the end of a flex row; the content beside it narrows while it is open.

The live demo runs with JavaScript. Its code is below.

React HTML
import { useState } from 'react';
import { Button, SideSheet } from '@kyralabs/m3exp-web/react';

export default function Standard() {
  const [open, setOpen] = useState(true);
  return (
    <div
      style={{
        display: 'flex',
        inlineSize: '100%',
        blockSize: 'var(--m3e-sys-window-height-medium)',
      }}
    >
      <div style={{ flex: '1', minInlineSize: '0', padding: 'var(--m3e-sys-space-200)' }}>
        <Button
          variant="outlined"
          onPress={() => {
            setOpen((o) => !o);
          }}
        >
          {open ? 'Close details' : 'Show details'}
        </Button>
      </div>
      <SideSheet isOpen={open} onOpenChange={setOpen} headline="Details" divider>
        <p>The content beside a standard sheet narrows while it is open.</p>
      </SideSheet>
    </div>
  );
}
<div style="display: flex; inline-size: 100%; block-size: var(--m3e-sys-window-height-medium)">
  <div style="flex: 1; min-inline-size: 0; padding: var(--m3e-sys-space-200)">
    <button type="button" class="m3e-button m3e-button--outlined" id="toggle-details">
      Toggle details
    </button>
  </div>
  <m3e-side-sheet id="details" headline="Details" divider open>
    <p>The content beside a standard sheet narrows while it is open.</p>
  </m3e-side-sheet>
</div>

<script type="module">
  const sheet = document.getElementById('details');
  document.getElementById('toggle-details').addEventListener('click', () => sheet.toggle());
</script>

Run the HTML version (custom elements, no framework).

Usage

Supplementary content anchored to the window's end edge (the left edge in RTL). The standard sheet sits beside the main content and narrows it; the modal sheet is a dialog over a scrim; adaptive picks modal below the medium window size class and standard from it (owner decision M9 #17).

API

React

<SideSheet>

Side sheet (COMPONENT_SPEC §31; D8: m3.material.io only). Driven by m3e-elements' SideSheetLayout (the code <m3e-side-sheet> runs): the navigation drawer's springs mirrored to the end edge (owner decision M9 #16), and for the modal sheet a native <dialog> (the page inert, scroll locked, Tab kept inside, focus moved in and returned to the opener). The close button, Escape and the scrim ask to close (onOpenChange(false)). Other props go to the root.

Prop Type Default Description
variant "standard" | "modal" | "adaptive" standard (default: an <aside> at the end of a flex row, narrowing the content beside it), modal (a <dialog> at the window's end edge over a scrim) or adaptive (modal below the medium window size class, 600dp, standard from it; owner decision M9 #17).
isOpen boolean Shown (controlled).
defaultOpen boolean Initially shown (uncontrolled).
headline ReactNode The title; it names the sheet.
aria-label string The name without a headline (default "Side sheet").
closeLabel string The close icon button's name (default "Close"). m3.material.io requires the close button.
backButton boolean Show the back icon button before the headline.
backLabel string The back icon button's name (default "Back").
detached boolean Inset by 16dp, all corners rounded (owner decision M9 #18).
divider boolean Standard: the line along its start edge; modal: the line above the actions.
actions ReactNode The bottom action buttons.
children ReactNode The content (it scrolls on its own).
onOpenChange (isOpen: boolean) => void Called with false when the sheet asks to close (the close button, Escape, the scrim, a browser close request). Controlled sheets close when isOpen does.
onBack () => void The back icon button was pressed.

Also accepts HTML attributes not listed here.

Custom elements

<m3e-side-sheet>

import '@kyralabs/m3e-elements/side-sheet.js';

<m3e-side-sheet variant="modal" headline="Filters" open>…</m3e-side-sheet> (COMPONENT_SPEC §31; D8: m3.material.io only). Renders the m3e-css .m3e-side-sheet contract inside itself (the host is display: contents) and runs SideSheetLayout (shared with m3e-react).

  • variant: standard (default: an <aside> at the end of a flex row; opening it narrows the content beside it), modal (a <dialog> at the window's end edge over a scrim) or adaptive (modal below the medium window size class, 600dp, standard from it; owner decision M9 #17).
  • open (reflected). headline (the title; it names the sheet), label (the name without a headline; default "Side sheet"), close-label (default "Close"), back-button (show the back icon button; back-label, default "Back"; back is its deprecated name, since 1.8, removed in 2.0), detached (inset 16dp, all corners rounded; owner decision M9 #18), divider (standard: the line along its start edge; modal: above the actions).
  • Children with slot="actions" are the bottom action buttons; the other child nodes are the content (it scrolls on its own).
  • Methods show(), close(), toggle(). Events open-change (detail: { open }; cancelable when the sheet asks: the close button, Escape, the scrim, a close request) and back.
Attribute Property Type Notes
variant variant string Reflected.
open open boolean Reflected.
headline headline string
label label string
close-label closeLabel string
back-button backButton boolean Shows the back icon button (React's backButton); it fires back.
back back boolean Deprecated. Since 1.8: use backButton (back-button); the back event stays. Removed in 2.0. Shows the back icon button.
back-label backLabel string
detached detached boolean
divider divider boolean
Event Detail Description
open-change OpenChangeDetail Opened or closed; cancelable when the sheet asks (close button, Escape, scrim, a close request).
back EmptyDetail The back icon button (back-button) was pressed.

CSS

side-sheet.css

@import '@kyralabs/m3e-css/side-sheet.css';

Side sheets (COMPONENT_SPEC §31; D8: Compose has no side sheet, m3.material.io is the only reference; the values it leaves open are the owner's picks, SOURCES register #78–#81 and D19). The standard sheet sits at the end of a flex row and narrows the content beside it; the modal sheet is a dialog at the window's end edge over a scrim. Static sheets need no JavaScript; the m3e-elements side sheet layout (<m3e-side-sheet>, m3e-react's <SideSheet>) animates them and runs the modal window.

<div style="display: flex">                                                    (standard)
  <main style="flex: 1; min-inline-size: 0">…</main>
  <aside class="m3e-side-sheet m3e-side-sheet--open" aria-labelledby="ss-title">
    <div class="m3e-side-sheet__sheet">
      <div class="m3e-side-sheet__header">
        <button class="m3e-icon-button m3e-side-sheet__back" aria-label="Back">   (optional)
          <span class="m3e-icon" aria-hidden="true">arrow_back</span></button>
        <h2 class="m3e-side-sheet__headline" id="ss-title">Filters</h2>          (optional)
        <button class="m3e-icon-button m3e-side-sheet__close" aria-label="Close">
          <span class="m3e-icon" aria-hidden="true">close</span></button>
      </div>
      <div class="m3e-side-sheet__content">…</div>
      <div class="m3e-side-sheet__actions">…buttons…</div>                     (optional)
    </div>
  </aside>
</div>

<dialog class="m3e-side-sheet m3e-side-sheet--modal m3e-side-sheet--open"      (modal)
  aria-labelledby="ss-title">
  <div class="m3e-side-sheet__scrim" aria-hidden="true"></div>
  <div class="m3e-side-sheet__sheet">…</div>
</dialog>
  • Width: --m3e-comp-side-sheet-container-width (256dp, the token DB's docked width), at most
--m3e-comp-side-sheet-max-width (400dp, the Specs tables) and the window (owner decision M9
#14). Set the container width to resize a sheet.
  • --detached: inset by 16dp from the end edge, top and bottom, all corners large (owner decision
M9 #18). --divider: the standard sheet's line along its start edge, the modal sheet's line
above its actions (the two anatomies; color outline-variant: md.comp.divider.color and the
standard color legend, D19 (16), register #79).
  • The header follows the Specs redlines: 24dp above the row; 24dp start / end padding (16dp before
a back icon) and 12dp between the elements, measured to the 24dp icons (the icon buttons' 40dp
containers sit 8dp outside them); the headline title large in on-surface-variant.
  • The content scrolls vertically on its own and never horizontally (Guidelines › Behavior).
  • JS hooks: [data-animating] while it moves (the layout writes --_m3e-ss-offset, the offset from
open: −width closed … 0 open, above 0 while the open spring overshoots; --_m3e-ss-stretch, the
overshoot; modal --_m3e-ss-fraction, the scrim alpha; standard --_m3e-ss-inline-size, the
space it takes); [data-open] while visible.
32 custom properties it reads
Custom property Default
--m3e-comp-divider-thickness 1px
--m3e-comp-side-sheet-actions-height 72px
--m3e-comp-side-sheet-actions-padding-bottom 24px
--m3e-comp-side-sheet-actions-padding-top 16px
--m3e-comp-side-sheet-actions-spacing 8px
--m3e-comp-side-sheet-container-width 256px
--m3e-comp-side-sheet-detached-container-shape var(--m3e-sys-shape-corner-large)
--m3e-comp-side-sheet-detached-margin 16px
--m3e-comp-side-sheet-divider-color var(--m3e-sys-color-outline-variant)
--m3e-comp-side-sheet-header-padding-top 24px
--m3e-comp-side-sheet-header-spacing 12px
--m3e-comp-side-sheet-headline-color var(--m3e-sys-color-on-surface-variant)
--m3e-comp-side-sheet-headline-font var(--m3e-sys-typescale-title-large-font)
--m3e-comp-side-sheet-headline-line-height var(--m3e-sys-typescale-title-large-line-height)
--m3e-comp-side-sheet-headline-size var(--m3e-sys-typescale-title-large-size)
--m3e-comp-side-sheet-headline-tracking var(--m3e-sys-typescale-title-large-tracking)
--m3e-comp-side-sheet-headline-weight var(--m3e-sys-typescale-title-large-weight)
--m3e-comp-side-sheet-icon-color var(--m3e-sys-color-on-surface-variant)
--m3e-comp-side-sheet-max-width 400px
--m3e-comp-side-sheet-modal-container-color var(--m3e-sys-color-surface-container-low)
--m3e-comp-side-sheet-modal-container-shadow var(--m3e-sys-elevation-level1-shadow)
--m3e-comp-side-sheet-modal-container-shape-end-end var(--m3e-sys-shape-corner-large-start-end-end)
--m3e-comp-side-sheet-modal-container-shape-end-start var(--m3e-sys-shape-corner-large-start-end-start)
--m3e-comp-side-sheet-modal-container-shape-start-end var(--m3e-sys-shape-corner-large-start-start-end)
--m3e-comp-side-sheet-modal-container-shape-start-start var(--m3e-sys-shape-corner-large-start-start-start)
--m3e-comp-side-sheet-padding-inline 24px
--m3e-comp-side-sheet-padding-inline-start-icon 16px
--m3e-comp-side-sheet-scrim-color var(--m3e-sys-color-scrim)
--m3e-comp-side-sheet-scrim-opacity 0.32
--m3e-comp-side-sheet-standard-container-color var(--m3e-sys-color-surface)
--m3e-comp-small-icon-button-container-height 40px
--m3e-comp-small-icon-button-icon-size 24px

Accessibility

modal = <dialog> (m3.material.io: "The accessibility role for a side sheet is Dialog"), standard = <aside> (a complementary landmark: the page stays usable beside it); both named by the headline (aria-labelledby) or label (default "Side sheet"); the close button "Close" (close-label; m3.material.io requires a close affordance), the back button "Back" (back-label); the scrim aria-hidden.

Every interactive part has a target of at least 48 × 48 px, a visible focus ring for keyboard focus only, and works under forced-colors and prefers-reduced-motion.

Differences from Compose

every value is single-source (D8; the owner's picks for the open ones SOURCES #78–#81, D19); the motion is the drawer's (#80); the header's vertical geometry comes from redlines only; the action spacing is the dialog's; focus follows the m3.material.io focus-order redline (Accessibility, mgamkh0k-03): ring 1 is on the back icon button beside the headline (the caption's "Headline" names that row; the headline text is not a tab stop and names the sheet), then Close, Cancel, Save, so the first stop is the back button, or Close without one, as shipped (owner decision D20 (8)); Escape closes both variants (APG dialog for the modal; cancelable, as the dismissible drawer, for the standard sheet; m3.material.io does not mention it); the redline numbers Cancel (3) before Save (4) although Save is drawn first, and m3e keeps DOM order; no predictive back, no user resizing; the overshoot fill is a shadow, as the drawer's; the adaptive switch remounts React's content (the element moves its nodes).

Sources

Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from m3.material.io, as cited below from COMPONENT_SPEC §31 and the m3e-css sources.

m3.material.io Side sheets (page 040dabe3-ed0f-4759-824d-812224e78af3: Overview, Specs tables / legends / redlines, Guidelines, Accessibility; token DB TOKEN_TABLE.06c2c925cf96adca, md.comp.sheet.side); NavigationDrawer.kt (motion and scrim analogue), AlertDialog.kt (action spacing), ScrimTokens.kt, DividerTokens.kt, SmallIconButtonTokens.kt (vendored).

side-sheet.css sources

(values: side-sheet-defaults.css, lines in PARITY.md): m3.material.io Side sheets (Specs, Guidelines, Accessibility; token DB md.comp.sheet.side); the motion and scrim of NavigationDrawer.kt (owner decision M9 #16, D19 (17): the drawer mirrored to the end edge, no drags).