Bottom Sheet
import { BottomSheet } from '@absoluteui/components/bottom-sheet';import '@absoluteui/components/bottom-sheet/style.css';Using Angular? See Bottom Sheet for Angular.
What it does
Section titled “What it does”A panel attached to the bottom edge of the viewport, built on native <dialog> like Dialog and Drawer.
- Modal (default):
showModal()gives top-layer rendering, an inert background, a backdrop, and native focus containment. AbsoluteUI adds scroll lock and focus restoration. - Non-modal (
modal: false):show()keeps the page interactive. Focus moves into the sheet on open, and Escape closes it when it is the topmost overlay.
Optional snap points set the height as fractions of the viewport height. The drag handle resizes the sheet and, dragged down, dismisses it. Dragging is never the only way: with two or more snap points the handle is a keyboard-operable slider and a single click cycles the heights (WCAG 2.5.7), and Escape and the close button dismiss. Close reasons and cancelable close events mirror Drawer.
When to use
Section titled “When to use”- Short, contextual tasks on small screens, such as share targets, quick actions, or a filter panel.
- Supplementary detail that users resize while keeping page context visible, such as a map result list (non-modal, with snap points).
- Mobile-first flows where a panel reachable with the thumb is preferable to a centered dialog.
When not to use
Section titled “When not to use”- Short confirmations or destructive prompts: use Dialog (role=alertdialog).
- Long forms or multi-step flows: use a page or a Drawer.
- Content anchored to a trigger: use a popover or menu.
- Menus of commands on desktop layouts: use Menu.
- Non-modal sheets for tasks that must be completed before continuing: use a modal sheet.
Examples
Section titled “Examples”Modal share sheet
Section titled “Modal share sheet”import { BottomSheet, type BottomSheetCloseDetail } from '@absoluteui/components/bottom-sheet';
const targets = document.createElement('ul');const sheet = BottomSheet.create({ title: 'Share', description: 'Choose where to send the link.', content: targets });sheet.element.addEventListener('aui-close', (event) => { const { reason } = (event as CustomEvent<BottomSheetCloseDetail>).detail; if (reason === 'drag') { // the user dragged the sheet away }});document.querySelector('#share')?.addEventListener('click', () => { sheet.open();});Non-modal sheet with snap points
Section titled “Non-modal sheet with snap points”import { BottomSheet, type BottomSheetSnapChangeDetail } from '@absoluteui/components/bottom-sheet';
const results = BottomSheet.create({ title: 'Nearby places', modal: false, snapPoints: [0.25, 0.5, 0.9], snap: 1, closeOnDragDown: false,});results.element.addEventListener('aui-snap-change', (event) => { const { value } = (event as CustomEvent<BottomSheetSnapChangeDetail>).detail; results.body.classList.toggle('is-expanded', value >= 0.9);});results.open();results.snapTo(2);Keep the sheet open with unsaved input
Section titled “Keep the sheet open with unsaved input”import { BottomSheet, type BottomSheetCloseDetail } from '@absoluteui/components/bottom-sheet';
let dirty = false;const form = document.createElement('form');form.addEventListener('input', () => { dirty = true;});const sheet = BottomSheet.create({ title: 'Add note', content: form, closeOnBackdrop: false });sheet.element.addEventListener('aui-cancel', (event) => { const { reason } = (event as CustomEvent<BottomSheetCloseDetail>).detail; // Accidental dismissals (Escape, drag) are blocked; the close button still closes. if (dirty && reason !== 'close-button') event.preventDefault();});sheet.open();import { BottomSheet, type BottomSheetSnapChangeDetail } from '@absoluteui/components/bottom-sheet';import '@absoluteui/components/button/style.css';import '@absoluteui/components/bottom-sheet/style.css';
const results = BottomSheet.create({ title: 'Nearby places', modal: false, snapPoints: [0.25, 0.5, 0.9], snap: 1 });results.element.addEventListener('aui-snap-change', (event) => { const { value } = (event as CustomEvent<BottomSheetSnapChangeDetail>).detail; results.body.classList.toggle('is-expanded', value >= 0.9);});document.querySelector('#show-results')?.addEventListener('click', () => { results.open();});More usage
Section titled “More usage”import { BottomSheet } from '@absoluteui/components/bottom-sheet';
const sheet = BottomSheet.create({ title: 'Share', description: 'Choose where to send the link.' });sheet.body.append(document.createElement('ul'));sheet.open();Anatomy
Section titled “Anatomy”<dialog class="aui-bottom-sheet aui-bottom-sheet--{modal|non-modal} [aui-bottom-sheet--snapped] [aui-bottom-sheet--dragging] [aui-bottom-sheet--static]" aria-labelledby aria-describedby style="--aui-bottom-sheet-snap (CSSOM)"> <header class="aui-bottom-sheet__header"> <h2 class="aui-bottom-sheet__title">title</h2> <button class="aui-button aui-bottom-sheet__close" aria-label="Close">icon</button> </header> <div class="aui-bottom-sheet__handle" (shown first through CSS order: -1) role="slider" tabindex="0" aria-label="Sheet height" aria-orientation="vertical" aria-valuemin aria-valuemax aria-valuenow aria-valuetext> (two or more snap points) | aria-hidden="true"> (otherwise) <span class="aui-bottom-sheet__grip"></span> </div> <p class="aui-bottom-sheet__description" hidden?>description</p> <div class="aui-bottom-sheet__body">content</div></dialog>The header precedes the handle in DOM order so the native initial focus lands on the close button, not the handle. The element is appended to document.body at creation.
API reference
Section titled “API reference”Factory
Section titled “Factory”BottomSheet.create(options: BottomSheetOptions, doc?: Document): BottomSheetGlobal configuration key: bottomSheet (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
title |
string |
Accessible name, rendered as the heading (text only). Must be non-empty after trimming. Required. | |
description |
string |
Referenced by aria-describedby (text only). Hidden when empty. | |
content |
Node | string |
Body content; strings render as text. Inserted at construction and again only when update() passes content. |
|
snapPoints |
readonly number[] |
Heights as fractions of the viewport height, strictly ascending, each in (0, 1]. Without snap points the sheet fits its content (capped at 90dvh). Two or more make the handle a slider. | |
snap |
number |
0 |
Index into snapPoints used when the sheet opens (0 = lowest). Must be 0 without snap points. When set, open() re-applies it. |
modal |
boolean |
true |
true: showModal() with inert background, backdrop and scroll lock. false: show(); the page stays interactive (supplementary panels only). Set at creation only. |
icons |
Partial<{ close: IconRef }> |
Icon slot overrides. | |
closeOnEscape |
boolean |
true |
Escape requests close (cancelable, reason ‘escape’) when the sheet is the topmost overlay. Globally configurable. |
closeOnBackdrop |
boolean |
true |
Backdrop click requests close (cancelable, reason ‘backdrop’). Modal only. Globally configurable. |
showCloseButton |
boolean |
true |
Render the header close button (reason ‘close-button’). Globally configurable. |
closeOnDragDown |
boolean |
true |
Dragging the handle down past min(120px, 25% of the lowest height) requests close (cancelable, reason ‘drag’). Globally configurable. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
element |
HTMLDialogElement |
The native dialog element (appended to document.body at creation). |
body |
HTMLDivElement |
Consumer content area. |
handle |
HTMLDivElement |
The drag handle strip: role=slider with two or more snap points, otherwise aria-hidden. |
isOpen |
boolean |
Whether the sheet is open. |
modal |
boolean |
Whether the sheet was created modal. |
snap |
number |
Current snap point index (0 without snap points). |
Methods
Section titled “Methods”| Method | Description |
|---|---|
open(): void |
Open (modal: showModal() and scroll lock; non-modal: show()); moves focus inside, captures focus for restoration, registers with the overlay stack, emits aui-open. No-op when open. |
close(): void |
Close programmatically (not cancelable, reason ‘programmatic’). No-op when closed. |
snapTo(index: number): void |
Move to a snap point programmatically; no event. Throws BOTTOM_SHEET_OPTIONS for an index outside snapPoints. |
update(options: BottomSheetUpdateOptions): void |
Merge and apply options (all except modal). Validates the merged options first; a failed update keeps the previous state. |
destroy(): void |
Close if open (restoring focus and scroll), then remove the element, listeners, drag tracking, overlay registration, and subscriptions. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-open |
null |
No | No | After the sheet opened. |
aui-cancel |
{ reason: 'escape' | 'backdrop' | 'close-button' | 'drag' } |
No | Yes | Before a user-initiated close; preventDefault keeps it open (a dismissing drag snaps back). |
aui-close |
{ reason: BottomSheetCloseReason } |
No | No | After the sheet closed. |
aui-snap-change |
{ index: number; value: number } |
No | No | The user changed the snap point (keyboard, handle click, or drag release). value is the viewport fraction. Not emitted by snapTo() or update(). |
Content areas
Section titled “Content areas”| Area | Description |
|---|---|
body |
BottomSheet.body (or the content option). |
CSS classes
Section titled “CSS classes”All rules live in the aui.components cascade layer, so application styles override them without specificity battles.
| Class | Purpose |
|---|---|
aui-bottom-sheet |
Native <dialog> element. |
aui-bottom-sheet--modal | --non-modal |
Modality modifiers. |
aui-bottom-sheet--snapped |
A snap point is active; block-size is calc(100dvh * –aui-bottom-sheet-snap). |
aui-bottom-sheet--dragging |
A handle drag is in progress; transitions are off. |
aui-bottom-sheet--static |
Set on open when motion is disabled (reduced motion or animation ‘none’); removes transitions. |
aui-bottom-sheet__handle |
Full-width drag strip (shown first through CSS order). |
aui-bottom-sheet__grip |
Visual grip bar inside the handle. |
aui-bottom-sheet__header |
Title row with close button. |
aui-bottom-sheet__title |
Heading (h2). |
aui-bottom-sheet__close |
Close button (Button, ghost, sm). |
aui-bottom-sheet__description |
Description paragraph. |
aui-bottom-sheet__body |
Scrollable content area. |
--aui-bottom-sheet-snap |
Custom property set through the CSSOM: active snap fraction. |
--aui-bottom-sheet-offset |
Custom property set through the CSSOM during a drag: downward slide in px. |
--aui-bottom-sheet-live-height |
Custom property set through the CSSOM during a drag with snap points: live height in px. |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
close |
close |
Design tokens
Section titled “Design tokens”Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.
--aui-border-width | --aui-color-backdrop | --aui-color-border | --aui-color-border-strong | --aui-color-focus-ring | --aui-color-surface | --aui-color-text | --aui-color-text-muted | --aui-control-height-sm | --aui-drawer-width-lg | --aui-duration-fast | --aui-duration-normal | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-lg | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-semibold | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-lg | --aui-radius-pill | --aui-shadow-lg | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-space-5
TypeScript definitions
Section titled “TypeScript definitions”type BottomSheetCloseReason = 'escape' | 'backdrop' | 'close-button' | 'drag' | 'programmatic';interface BottomSheetConfig { closeOnEscape: boolean; closeOnBackdrop: boolean; showCloseButton: boolean; closeOnDragDown: boolean }interface BottomSheetIcons { close: IconRef }interface BottomSheetOptions extends Partial<BottomSheetConfig> { title: string; description?: string; content?: Node | string; snapPoints?: readonly number[]; snap?: number; modal?: boolean; icons?: Partial<BottomSheetIcons>;}type BottomSheetUpdateOptions = Partial<Omit<BottomSheetOptions, 'modal'>>;interface BottomSheetCloseDetail { reason: BottomSheetCloseReason }interface BottomSheetSnapChangeDetail { index: number; value: number }Accessibility
Section titled “Accessibility”Pattern: WAI-ARIA APG Dialog (Modal) pattern implemented with native <dialog>.showModal(); non-modal sheets use <dialog>.show(). With two or more snap points the drag handle follows the APG Slider pattern.
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Tab / Shift+Tab | Modal sheet open | Moves focus among sheet controls (close button, handle slider, content); the background is inert. |
| Tab / Shift+Tab | Non-modal sheet open | Follows document order; focus can leave the sheet and reach the page. |
| Escape | Sheet is the topmost overlay, closeOnEscape=true | Emits cancelable aui-cancel (reason ‘escape’), then closes and restores focus. |
| Escape | closeOnEscape=false | No effect; the native close request is prevented, including repeated presses. |
| Escape | Handle drag in progress | Cancels the drag only; the sheet returns to its height and stays open. |
| Escape | Nested overlay open | Closes only the topmost overlay. |
| ArrowUp / ArrowRight / PageUp | Handle slider focused | Next larger snap point (stops at the largest); emits aui-snap-change. |
| ArrowDown / ArrowLeft / PageDown | Handle slider focused | Next smaller snap point (stops at the smallest); emits aui-snap-change. Never closes the sheet. |
| Home / End | Handle slider focused | Smallest / largest snap point. |
| Enter / Space | Close button or content button focused | Activates the button. |
| Element | Role |
|---|---|
| <dialog> | dialog (native) |
| title | heading level 2 |
| handle (two or more snap points) | slider |
| handle (fewer than two snap points) | none (aria-hidden, decorative) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-labelledby |
<dialog> | id of the title heading |
aria-describedby |
<dialog> | id of the description (when provided) |
aria-modal |
<dialog> | Implicit true for modal sheets (native showModal()); absent for non-modal sheets |
aria-label |
close button | Localized ‘Close’ |
aria-label |
handle slider | Localized ‘Sheet height’ |
aria-orientation |
handle slider | vertical |
aria-valuemin / aria-valuemax |
handle slider | Lowest / highest snap point as a whole percent of the viewport height |
aria-valuenow |
handle slider | Current snap point as a whole percent |
aria-valuetext |
handle slider | Localized ‘{percent}% of screen height’ |
tabindex |
handle slider | 0 |
aria-hidden |
handle (fewer than two snap points) | true (no role, not focusable) |
On open, focus moves to the first focusable element (native dialog focusing steps): the close button when shown, since it precedes the handle in DOM order. A non-modal sheet with no focusable content focuses the dialog itself (tabindex -1). On close, a modal sheet returns focus to the element focused before opening; a non-modal sheet does so only when focus was inside the sheet or on the body. Hiding the focused close button through configuration moves focus to the dialog.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”| Criterion | Level | Note |
|---|---|---|
| 1.3.1 Info and Relationships | A | |
| 2.1.1 Keyboard | A | |
| 2.1.2 No Keyboard Trap | A | |
| 2.4.3 Focus Order | A | |
| 2.4.7 Focus Visible | AA | |
| 2.5.7 Dragging Movements | AA | |
| 2.5.8 Target Size (Minimum) | AA | |
| 4.1.2 Name, Role, Value | A |
Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- Dragging is never the only way to operate the sheet (WCAG 2.5.7): with two or more snap points a single click on the handle cycles the heights and the handle is keyboard-operable; dismissal is also available through Escape and the close button.
- The handle strip spans the full width at control-height-sm height, a large touch target (WCAG 2.5.8).
- Background inertness and top-layer rendering of modal sheets are provided by the browser.
- Slide motion is removed under prefers-reduced-motion and data-aui-animation=‘none’.
- The handle is shown first visually but follows the header in DOM and focus order.
- Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
- Snap changes are conveyed through the slider’s aria-valuetext only; there is no separate live-region announcement.
- When closeOnEscape, showCloseButton, and closeOnDragDown are all false, the sheet has no built-in dismissal; the consumer must provide one.
Behavior
Section titled “Behavior”States
Section titled “States”closed, opening (slide-in), open, open at snap point N, dragging, closing (an aui-cancel listener is pending), and closing (slide-out).
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| closed | open() |
open at snap (focus moves inside; modal: scroll locked, background inert) |
| open | Escape / backdrop (modal) / close button | aui-cancel -> closed, unless prevented |
| open | handle pointer moves more than 4 px | dragging (transitions off) |
| dragging | release, slide below the lowest height > min(120px, 25% of that height), closeOnDragDown: true |
aui-cancel (reason drag) -> closed; if prevented, back to open |
| dragging | other release, two or more snap points | open at the nearest snap point by height (aui-snap-change when it changed) |
| dragging | other release, fewer than two snap points | open (height unchanged) |
| dragging | Escape, pointercancel, lost pointer capture |
open (drag canceled, height unchanged) |
| open at N | arrow / Page / Home / End on the handle, or a handle click | open at M (aui-snap-change) |
| open | close() / destroy() |
closed (no aui-cancel) |
| open | consumer <form method="dialog"> submit |
closed (reason programmatic) |
Accessibility notes
Section titled “Accessibility notes”- Initial focus follows the native dialog focusing steps: the first focusable element, which is the close button when shown. A non-modal sheet with nothing focusable focuses the dialog (
tabindex="-1"). - A modal sheet returns focus to the element focused before
open(). A non-modal sheet returns focus only when focus was inside the sheet (or on the body) at close. - Hiding the focused close button through configuration moves focus to the dialog.
- Single-pointer alternative to dragging (WCAG 2.5.7): a click on the handle cycles the snap points; the click after a drag is suppressed.
- The handle strip is full-width and
control-height-smtall (WCAG 2.5.8). - The handle is shown first visually but follows the header in focus order.
- Slide motion is removed under
prefers-reduced-motionanddata-aui-animation='none'. - Manual screen-reader verification is pending.
Responsive behavior
Section titled “Responsive behavior”Inline size min(100vw, drawer-width-lg), centered with margin: 0 auto; top corners use radius-lg. Without snap points the sheet fits its content up to max-block-size: 90dvh; with snap points block-size: calc(100dvh * snap) (up to 100dvh). The body scrolls with overscroll-behavior: contain. Horizontal padding is max(space-5, env(safe-area-inset-left|right)) and the body’s bottom padding is max(space-5, env(safe-area-inset-bottom)), so content clears notches and the home indicator. The handle uses touch-action: none.
Right-to-left
Section titled “Right-to-left”No direction option. The sheet is centered and slides vertically, so the writing direction does not change placement. Header layout uses logical properties; the close button sits at the inline end. Slider arrow keys are not mirrored: ArrowRight and ArrowUp always enlarge the sheet.
Theming
Section titled “Theming”Token-driven surface, border (no bottom border), top radius, shadow, backdrop, and grip color. Forced-colors mode uses CanvasText for the border and the grip.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { bottomSheet: { closeOnBackdrop: false, closeOnDragDown: false } } });Globally configurable: closeOnEscape, closeOnBackdrop, showCloseButton, closeOnDragDown. Changes apply to existing instances immediately.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. All options are dynamic through update(); modal is init-only.
icons.close (default close), resolved through the global icon provider/registry. The grip is a CSS bar, not an icon.
Security
Section titled “Security”Title, description, and string content use textContent. No HTML parsing. Drag and snap geometry is written as CSS custom properties through the CSSOM, never as markup style attributes, so the component is CSP-safe (style-src without 'unsafe-inline').
Data handling
Section titled “Data handling”No persistence, network access, or telemetry. The snap index lives in memory.
Performance
Section titled “Performance”Constant work per open/close: one showModal()/show() call, one overlay-stack entry, one scroll-lock reference (modal), and one focus restore point. During a drag, each pointermove reads the viewport height and writes at most two custom properties; the sheet height is measured once at drag start. Budget: open() under 16 ms (one frame) in headless Chromium.
Supported data scale
Section titled “Supported data scale”Not data-driven. Body content size is limited only by the consumer; the body scrolls. Snap point count is unbounded, but more than three or four heights are hard to distinguish by dragging.
Edge cases
Section titled “Edge cases”closeOnEscape: falsealso blocks repeated Escape presses (the native close request is always prevented).- A non-modal sheet yields Escape to a modal dialog opened elsewhere, because it is inert behind that dialog.
- Escape during a handle drag is consumed by the drag and never reaches the overlay stack.
- With exactly one snap point the height is fixed at that fraction; the handle is decorative and dragging only dismisses.
- Without snap points the dismiss threshold is
min(120px, 25% of the sheet height at drag start); with snap points it uses the lowest snap height. - A prevented
aui-cancelafter a dismissing drag returns the sheet to its current snap point. open()re-applies thesnapoption when it is set; without it, the last snap index is kept across reopening.update({ snapPoints })withoutsnapclamps both the current index and a keptsnapoption to the new list; an explicit out-of-rangesnapthrows.- Consumer changes to
bodysurvive re-renders; onlyupdate({ content })replaces the body. - Destroying an open sheet releases the scroll lock and overlay registration, and restores focus.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
BOTTOM_SHEET_TITLE |
configuration | Missing or blank title. |
BOTTOM_SHEET_OPTIONS |
configuration | Invalid modal, content, snapPoints (not strictly ascending numbers in (0, 1]), snap, or snapTo() index. |
CFG_INVALID_VALUE / CFG_UNKNOWN_OPTION |
configuration | Invalid or unknown configuration value, such as closeOnEscape: 'yes'. |
A failed construction leaves nothing behind: no element in the document, listeners, overlay registration, or subscriptions. A failed update() keeps the previous options.
Known limitations
Section titled “Known limitations”- Non-modal sheets are not in the top layer. They use
z-index: 1000, so page content with a higher stacking context can cover them. - Only the handle strip starts a drag; the header and body never drag the sheet.
- Snap points are viewport fractions only.
- Slide animations need
@starting-styleandtransition-behavior: allow-discrete. Older browsers open and close without animation. - A
<form method="dialog">submit in the content closes with reasonprogrammatic(there is noreturnValuein the detail). - Manual screen-reader verification is pending.
Unsupported scenarios
Section titled “Unsupported scenarios”- Switching
modalafter creation. - Sheets on the top or side edges: use Drawer.
- Pixel or content-measured snap heights.
- HTML strings as content: pass a
Nodeinstead.