A panel that slides in from the edge of the screen and floats over the map: the overview sheet and the detail panel on a wide screen, and the bottom sheet they become on a phone (Layout).
When to use
Use Sheet for a panel beside the map that stays open while the rider pans and picks vehicles, and the bottom sheet it becomes on a phone.
| Use instead | When |
|---|---|
| DetailPanel | The panel shows the vehicle or the stop a rider picked: a Sheet headed by a DetailHeader |
| Dialog | The panel asks for a decision, and holds focus until it is answered |
| Popover | A few sentences or settings sit against the button that opens them |
Kongens Nytorv
Metro M1, M2 and M3.
Props
| Name | Type | Default | Description |
|---|---|---|---|
anchor | 'left' | 'right' | 'bottom' | 'left' | The edge the sheet slides from and is swiped back to: left for the overview sheet, right for the detail panel, bottom for either on a phone. It sets data-anchor on the root and the viewport. |
title* | ReactNode | Required | The sheet's name, such as a stop or a destination, shown in the header and naming the dialog. Required, so every sheet draws its header with the title and the close button and is always named; a pattern with a header of its own (DetailPanel) passes that whole header as title, with slotProps.title.render a div, and names the dialog by its own aria-labelledby. |
children | ReactNode | — | The sheet's body, below the header. It scrolls when it is taller than the sheet. |
trigger | ReactElement<unknown, JSXElementConstructor<any> | string> | — | The button that opens the sheet, such as a Perron Button; it gets the trigger's ARIA and gets focus back on close. Without one, the sheet opens through open. |
open | boolean | — | Whether the sheet is open, for a controlled sheet. |
defaultOpen | boolean | false | Whether the sheet starts open, for an uncontrolled sheet. |
onOpen | (event, details) => void | — | Called when the sheet asks to open: from the trigger. |
onClose | (event, reason, details) => void | — | Called when the sheet asks to close, with Base UI's reason: escape-key, close-press, outside-press, swipe, trigger-press, focus-out, close-watcher, imperative-action or none. details.cancel() keeps it open. |
modal | boolean | false | Whether the sheet shuts out the rest of the page: focus trapped, the page's scroll locked, a scrim behind, closed by a press outside. Non-modal by default, so the map stays usable beside it; while non-modal it sets disablePointerDismissal, so a press on the map or focus leaving the sheet doesn't close it, and renders no backdrop. |
snapPoints | DrawerSnapPoint[] | With anchor 'bottom': size.sheet-peek and 1 | The heights a bottom sheet snaps to: a fraction of the viewport from 0 to 1, pixels above 1, or a px or rem string. Ignored on a side sheet. A bottom sheet with no snapPoints snaps to size.sheet-peek and to full height, 1, starting at the peek. |
snapPoint | null | number | string | — | The snap point a bottom sheet is at, for a controlled snap point. |
defaultSnapPoint | null | number | string | The first snap point | The snap point a bottom sheet opens at, for an uncontrolled snap point. |
onSnapPointChange | (event, snapPoint, details) => void | — | Called when a bottom sheet asks to move to another snap point, by a swipe. It takes MUI's shape, (event, snapPoint, details), with event being details.event. |
closeLabel | string | The locale's | Overrides "Close", the close button's label. |
slotProps | { trigger?: Props<unknown> | undefined; portal?: DrawerPortalProps | undefined; backdrop?: DrawerBackdropProps | undefined; viewport?: DrawerViewportProps | undefined; title?: DrawerTitleProps | undefined; close?: DrawerCloseProps | undefined; content?: DrawerContentProps | undefined; } | — | Props for each part: trigger, portal, backdrop, viewport, title, close, content. |
anchor'left' | 'right' | 'bottom', default'left'- The edge the sheet slides from and is swiped back to:
leftfor the overview sheet,rightfor the detail panel,bottomfor either on a phone. It setsdata-anchoron the root and the viewport. title (required)ReactNode, default Required- The sheet's name, such as a stop or a destination, shown in the header and naming the dialog. Required, so every sheet draws its header with the title and the close button and is always named; a pattern with a header of its own (DetailPanel) passes that whole header as
title, withslotProps.title.renderadiv, and names the dialog by its ownaria-labelledby. childrenReactNode, default —- The sheet's body, below the header. It scrolls when it is taller than the sheet.
triggerReactElement<unknown, JSXElementConstructor<any> | string>, default —- The button that opens the sheet, such as a Perron
Button; it gets the trigger's ARIA and gets focus back on close. Without one, the sheet opens throughopen. openboolean, default —- Whether the sheet is open, for a controlled sheet.
defaultOpenboolean, defaultfalse- Whether the sheet starts open, for an uncontrolled sheet.
onOpen(event, details) => void, default —- Called when the sheet asks to open: from the trigger.
onClose(event, reason, details) => void, default —- Called when the sheet asks to close, with Base UI's reason:
escape-key,close-press,outside-press,swipe,trigger-press,focus-out,close-watcher,imperative-actionornone.details.cancel()keeps it open. modalboolean, defaultfalse- Whether the sheet shuts out the rest of the page: focus trapped, the page's scroll locked, a scrim behind, closed by a press outside. Non-modal by default, so the map stays usable beside it; while non-modal it sets
disablePointerDismissal, so a press on the map or focus leaving the sheet doesn't close it, and renders no backdrop. snapPointsDrawerSnapPoint[], default With anchor 'bottom': size.sheet-peek and 1- The heights a bottom sheet snaps to: a fraction of the viewport from 0 to 1, pixels above 1, or a
pxorremstring. Ignored on a side sheet. A bottom sheet with nosnapPointssnaps tosize.sheet-peekand to full height,1, starting at the peek. snapPointnull | number | string, default —- The snap point a bottom sheet is at, for a controlled snap point.
defaultSnapPointnull | number | string, default The first snap point- The snap point a bottom sheet opens at, for an uncontrolled snap point.
onSnapPointChange(event, snapPoint, details) => void, default —- Called when a bottom sheet asks to move to another snap point, by a swipe. It takes MUI's shape,
(event, snapPoint, details), witheventbeingdetails.event. closeLabelstring, default The locale's- Overrides "Close", the close button's label.
slotProps{ trigger?: Props<unknown> | undefined; portal?: DrawerPortalProps | undefined; backdrop?: DrawerBackdropProps | undefined; viewport?: DrawerViewportProps | undefined; title?: DrawerTitleProps | undefined; close?: DrawerCloseProps | undefined; content?: DrawerContentProps | undefined; }, default —- Props for each part:
trigger,portal,backdrop,viewport,title,close,content.
Use it
import { Sheet } from '@mylder/perron'Ask your agent
get_reference({ library: "base-ui", component: "Drawer" })Keyboard and ARIA
From the Base UI reference for each part. "From" says who does it: Base UI, or Perron.
| Key | Does | From |
|---|---|---|
Enter, Space | On the trigger, opens the sheet and moves focus to the popup; Tab then reaches the close button | Base UI |
Enter, Space | On the close button, closes the sheet with reason close-press, and focus returns to the trigger or to the element focused before | Base UI |
Esc | Closes the topmost open sheet with reason escape-key, and focus returns as above | Base UI |
Tab, Shift+Tab | In a modal sheet, moves through the sheet's tabbable elements and wraps inside it | Base UI |
Tab, Shift+Tab | In a sheet that is not modal, moves on into the page; the sheet stays open | Base UI, with disablePointerDismissal set by Perron |
Tab, Shift+Tab | Stops on the body while it scrolls, and the arrow keys, PageUp, PageDown, Home and End scroll it | Perron, with tabIndex 0 while the body overflows |
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
trigger | button | The caller's element | aria-expanded, aria-haspopup="dialog", with aria-controls naming the popup |
root | dialog | title, through aria-labelledby | While modal, everything outside it is aria-hidden |
title | heading, level 2 | title | None |
close | button | The owned close string, or closeLabel | None |
backdrop | None: presentational | None | None |
What axe won't catch, from the contract, each with how this component meets it:
- Everything the eye is told, the ear is told, for every shape each prop accepts: the close button's icon is
aria-hiddenand theCloseButtonpart gives it the owned label; the body scrolls at every snap point and takes focus while it does, so everything in it can be reached by keys and screen readers without the grab; the close button and Esc dismiss it. - Every landmark and named region has an accessible name: the dialog is named by its required
title, through Base UI'saria-labelledby. - Every default label can be overridden:
closeLabel. - Every shape the type accepts renders, in every variant:
titleandchildrenshow a string, an element and a list alike, in eachanchor: theLeft,RightandBottomstories'playtests open a sheet with an element title and a string body, a list title and an element body, and a string title and a list body, each named by its title. - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: none here; a closed sheet is unmounted, orhiddenwhenslotProps.portal.keepMountedkeeps it. - Native semantics first:
<button>for the trigger and the close button,<h2>for the title, and Base UI'sdialogrole on the root. - Focus moves in reading order and returns on close: title, close button, then the content; Base UI returns focus to the trigger or to the element focused before.
- Every target is at least
size.target-minsquare: the close button,size.control. - Loops stop when the system asks for less motion: none here; the slide reads
duration.sheet, which the tokens shorten to 0.01 ms.
Owned strings
Every word the component shows or announces on its own. Reuse a key packages/perron/src/locale/en.ts already has; a new key gets its English here, and agents write the Danish, Swedish and Norwegian in the locale files.
| Key | English | Override | Seen or heard where |
|---|---|---|---|
close | Close | closeLabel | The close button in the header, announced as its name |
Do and don't
For a caller using the component, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.
| Rule | Level | Do | Don't | Checked by |
|---|---|---|---|---|
| DD-1 | should | Use a sheet for a panel beside the map, left open while the rider pans and picks vehicles: it is not modal by default | Use a modal sheet to ask for a decision; that is a Dialog, which needs no swipe or snap points (Base UI's own advice) | review |
| DD-2 | must | Pick anchor bottom on a phone, under breakpoint.phone, in the screen's code | Expect the sheet to turn into a bottom sheet on its own; it sizes to its viewport and holds no media query | review |
| DD-3 | must | Name the sheet with title: the stop, line or destination it shows | Leave the title out, or draw a heading of your own in children: the dialog takes its name from title | review |
| DD-4 | must | Handle onClose and let Esc close the topmost sheet, as Accessibility orders: a dialog, then a detail panel | Cancel every close of a sheet the rider can't otherwise leave; cancel only the reasons a screen must refuse, such as swipe on the overview sheet | review |
| DD-5 | must | Mark a horizontal carousel inside a bottom sheet with data-base-ui-swipe-ignore, so its drags don't swipe the sheet; put a Slider in as it is, since it marks its own control | Let a control that drags sideways fight the sheet's swipe | review |
| DD-6 | must | Keep one primary action per sheet | Put two brand buttons in one sheet; a screen has one brand action at most | review |