Side sheet
A side sheet holds content that supports the page, docked to its side: filters beside a list of results, the details of a selected item. A modal sheet covers the page with a scrim until it closes; a standard sheet has none, and the page stays usable beside it. See the M3 side sheets guidelines.
Usage
close fires however it closed: its close button, the scrim, Escape or close().
import { createSideSheet } from 'mtrl';
const sheet = createSideSheet({
variant: 'modal',
title: 'Filters',
content: 'Narrow the results by price and distance.',
});
sheet.on('close', () => refreshResults());
document.body.append(sheet.element);
function showFilters() {
sheet.open();
}
<m-side-sheet modal headline="Filters">
<p>Narrow the results by price and distance.</p>
</m-side-sheet>
<script type="module">
const sideSheet = document.querySelector('m-side-sheet');
sideSheet.addEventListener('close', () => refreshResults());
function showFilters() {
sideSheet.show();
}
</script>
import { useState } from 'react';
import { SideSheet } from 'mtrl/react';
import { refreshResults } from './app';
export function Example() {
const [open, setOpen] = useState(false);
const showFilters = () => setOpen(true);
return (
<SideSheet open={open} onClose={() => { setOpen(false); refreshResults(); }} modal headline="Filters">
<p>Narrow the results by price and distance.</p>
</SideSheet>
);
}
<script setup lang="ts">
import { ref } from 'vue';
import { MSideSheet } from 'mtrl/vue';
import { refreshResults } from './app';
const open = ref(false);
function showFilters() {
open.value = true;
}
</script>
<template>
<MSideSheet :open="open" @close="open = false; refreshResults()" modal headline="Filters">
<p>Narrow the results by price and distance.</p>
</MSideSheet>
</template>
<script lang="ts">
import { SideSheet } from 'mtrl/svelte';
import { refreshResults } from './app';
let open = $state(false);
function showFilters() {
open = true;
}
</script>
<SideSheet open={open} onclose={() => { open = false; refreshResults(); }} modal headline="Filters">
<p>Narrow the results by price and distance.</p>
</SideSheet>
import { createSignal } from 'solid-js';
import { SideSheet } from 'mtrl/solid';
import { refreshResults } from './app';
export function Example() {
const [open, setOpen] = createSignal(false);
const showFilters = () => setOpen(true);
return (
<SideSheet open={open()} onClose={() => { setOpen(false); refreshResults(); }} modal headline="Filters">
<p>Narrow the results by price and distance.</p>
</SideSheet>
);
}
Examples
Standard, at the start
A standard sheet sits on surface without a scrim. position is logical: start is the left
edge in a left-to-right page and the right edge in a right-to-left one.
import { createSideSheet } from 'mtrl';
const sheet = createSideSheet({
variant: 'standard',
position: 'start',
title: 'Details',
content: 'Created on 3 September by Ada.',
});
document.body.append(sheet.element);
<m-side-sheet position="start" headline="Details">
<p>Created on 3 September by Ada.</p>
</m-side-sheet>
import { SideSheet } from 'mtrl/react';
export function Example() {
return (
<SideSheet position="start" headline="Details">
<p>Created on 3 September by Ada.</p>
</SideSheet>
);
}
<script setup lang="ts">
import { MSideSheet } from 'mtrl/vue';
</script>
<template>
<MSideSheet position="start" headline="Details">
<p>Created on 3 September by Ada.</p>
</MSideSheet>
</template>
<script lang="ts">
import { SideSheet } from 'mtrl/svelte';
</script>
<SideSheet position="start" headline="Details">
<p>Created on 3 September by Ada.</p>
</SideSheet>
import { SideSheet } from 'mtrl/solid';
export function Example() {
return (
<SideSheet position="start" headline="Details">
<p>Created on 3 September by Ada.</p>
</SideSheet>
);
}
Wider, without a close button
width is in pixels, up to 400. Without its close button, a sheet needs another way to close:
here the scrim and Escape.
import { createSideSheet } from 'mtrl';
const sheet = createSideSheet({
variant: 'modal',
width: 360,
closeButton: false,
title: 'Sections',
content: 'Overview, specs, accessibility.',
});
document.body.append(sheet.element);
<m-side-sheet width="360" modal headline="Sections" no-close-button>
<p>Overview, specs, accessibility.</p>
</m-side-sheet>
import { SideSheet } from 'mtrl/react';
export function Example() {
return (
<SideSheet width={360} modal headline="Sections" noCloseButton>
<p>Overview, specs, accessibility.</p>
</SideSheet>
);
}
<script setup lang="ts">
import { MSideSheet } from 'mtrl/vue';
</script>
<template>
<MSideSheet :width="360" modal headline="Sections" no-close-button>
<p>Overview, specs, accessibility.</p>
</MSideSheet>
</template>
<script lang="ts">
import { SideSheet } from 'mtrl/svelte';
</script>
<SideSheet width={360} modal headline="Sections" noCloseButton>
<p>Overview, specs, accessibility.</p>
</SideSheet>
import { SideSheet } from 'mtrl/solid';
export function Example() {
return (
<SideSheet width={360} modal headline="Sections" noCloseButton>
<p>Overview, specs, accessibility.</p>
</SideSheet>
);
}
toggle() opens a closed sheet and closes an open one. Recipes such as filters applied on
close are planned for Examples.
API
Options
| Option | Type | Default | Description |
|---|---|---|---|
variant |
'standard' | 'modal' |
'modal' |
Whether it covers the page; the web component is standard without modal |
position |
'start' | 'end' |
'end' |
The edge it docks to, in the reading direction |
title |
string |
undefined |
The headline, which names the sheet |
content |
string | HTMLElement |
undefined |
The body, as HTML or an element |
width |
number |
256 |
Its width in pixels |
maxWidth |
number |
400 |
The widest it grows, in pixels |
closeButton |
boolean |
true |
A close button in the header |
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 |
open |
boolean |
false |
Whether it starts open |
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? } |
undefined |
Event handlers registered at creation |
class |
string |
undefined |
Additional CSS classes |
Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
open() / close() / toggle() |
none | SideSheetComponent |
Opens or closes it |
isOpen() |
none | boolean |
Whether it is open |
setTitle(title) / setContent(content) |
title: string, content: string | HTMLElement |
SideSheetComponent |
The headline, the body |
on(event, handler) / off(event, handler) |
event: 'open' | 'close', handler: Function |
SideSheetComponent |
Adds or removes a listener |
destroy() |
none | void |
Removes it |
Events
| Event | Description | Data |
|---|---|---|
open / close |
It opened or closed | none |
The web component dispatches them too, without a detail.
Accessibility
- A modal sheet is a
dialogwitharia-modal, a standard onecomplementary; the title names either. Without a title, give the web component anaria-label. - Opening a modal sheet focuses it, and closing it gives focus back.
Tabstays inside it and the page behind it is inert, in the top layer or not. Escapecloses 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 close button is named "Close".
Styling
.mtrl-side-sheet, .mtrl-side-sheet--open { } /* the fixed layer, holding the scrim and the sheet */
.mtrl-side-sheet--modal, .mtrl-side-sheet--standard { }
.mtrl-side-sheet--start, .mtrl-side-sheet--end { }
.mtrl-side-sheet__scrim { } /* a modal sheet's, outside the top layer */
.mtrl-side-sheet__container { } /* the sheet */
.mtrl-side-sheet__header, .mtrl-side-sheet__title, .mtrl-side-sheet__close { }
.mtrl-side-sheet__content { }
Measurements
| Attribute | Value |
|---|---|
| Container | Standard surface, no elevation; modal surface-container-low, elevation 1 |
| Corners | Modal: 16dp on the side facing the page; standard: none |
| Width | 256dp by default, 400dp at most |
| Header | 72dp high, 16dp above and below, 24dp at the sides, 12dp gaps; Title Large, on-surface |
| Close button | 40dp, on-surface-variant icon |
| Content | Body Medium, on-surface-variant, 24dp at the sides |
| Scrim | scrim at 32% |