Containment
Side sheets
A surface at the side of the window for supplementary content, standard beside the page or modal over it.
- COMPONENT_SPEC §31
- CSS · custom elements · React
- Open in the playground
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.
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.
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) oradaptive(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";backis 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(). Eventsopen-change(detail: { open }; cancelable when the sheet asks: the close button, Escape, the scrim, a close request) andback.
| 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).