rc-bottom-sheet
Modal bottom-sheet wrapper for a native <dialog>, built on rc-dialog.
- Package
@rcarls/rc-bottom-sheet- Element
<rc-bottom-sheet>- Native dependency
- Requires a direct child native dialog
- State model
- Open state follows the native dialog
- Main events
rc-dialog-openrc-dialog-togglerc-dialog-request-closerc-dialog-cancelrc-dialog-closerc-bottom-sheet-snap- Related
- rc-dialogTheme previews
Installation
- npm
- Yarn
npm install @rcarls/rc-bottom-sheet
yarn add @rcarls/rc-bottom-sheet
import '@rcarls/rc-bottom-sheet/define';
Usage
Place a <dialog> element directly inside <rc-bottom-sheet>. The inner
dialog must have aria-labelledby or aria-label.
<rc-bottom-sheet id="filters" snap-points="40dvh 70dvh 100dvh">
<dialog aria-label="Filter projects">
<button
type="button"
data-rc-bottom-sheet-handle
data-rc-dialog-resize-axis="y"
data-rc-dialog-resize-origin="top"
aria-label="Resize sheet"
></button>
<button value="done" formmethod="dialog">Done</button>
</dialog>
</rc-bottom-sheet>
The element inherits rc-dialog methods and events, including showModal(),
close(), requestClose(), rc-dialog-toggle, and
rc-dialog-request-close. Bottom sheets default to light dismiss, vertical
top-origin resize, downward swipe-dismiss, and fixed positioning at the
viewport's bottom edge. Add an optional
[data-rc-bottom-sheet-handle] element inside the dialog for a themed resize
handle. Use snap-points to snap the sheet to declared CSS heights after
resize gestures.
Without a theme, the authored handle uses a 32 by 4 pixel pill indicator, centered inside a full-width, 48 pixel-high target.
The Substrate theme replaces that pill with the same three-dot visual language as its splitter handle while preserving the full-width pointer target. Material keeps the Material 3 pill indicator.
For an embedded non-modal sheet, place the component in a positioned
container, set --rc-bottom-sheet-position: absolute, and open it with
show(). The sheet then docks to that container's block-end edge rather than
the viewport.
Declare snap points as CSS heights in ascending order. Slow releases settle at
the nearest point. A decisive upward or downward swipe (500 pixels per second
by default) settles at the last or first point, respectively. When
swipe-dismiss is enabled, dragging downward by at least 96 pixels requests
close before velocity-based snapping applies.
const sheet = document.querySelector('#filters');
sheet.showModal();
sheet.snapTo(1);
sheet.addEventListener('rc-bottom-sheet-snap', (event) => {
console.log(event.detail);
});
snapTo() clamps finite indices to the available range and ignores calls when
there are no valid snap points. Pass 'instant' as its second argument to
skip animation. The component also skips animation when the user has enabled
reduced motion. Snap events report the effective rendered height after CSS
minimum and maximum size constraints, and constrained targets remain docked to
the sheet's block-end edge.
Live demo
When to use
Bottom sheets are a mobile-friendly alternative to rc-dialog for
non-blocking, dismissible prompts, such as a newsletter/mailing-list
signup or a general on-visit call-to-action. Docking to the bottom edge and
supporting swipe-dismiss keeps the prompt reachable by thumb and easy to
brush aside, where a centered rc-dialog modal would feel more disruptive
on a small screen.
Accessibility
The direct child <dialog> owns dialog semantics, focus, labeling, and form
behavior. Give it an accessible name with aria-labelledby or aria-label.
Use showModal() when interaction outside the sheet must be unavailable and
show() for a non-modal surface.
The optional drag handle must be a focusable control with an accessible name. Keep it as the dialog's first direct child and avoid adding dialog top padding so its 48 pixel target reaches the sheet's top edge.
rc-dialog supplies keyboard resizing for the authored handle, so pointer
dragging is not the only way to resize the sheet.
Theming
The component docks the dialog to the bottom edge and centers authored
[data-rc-bottom-sheet-handle] handles by default. rc-theme-material
applies the Material surface, level-one elevation, scrim, 32 by 4 pixel drag
indicator within a 48 pixel target, and text-button treatment to direct
authored action buttons. Wrap actions nested in sheet content with rc-button,
as in the live demo, so they
receive the shared Material button treatment without affecting component-owned
buttons such as those inside rc-chip.
Set --rc-bottom-sheet-snap-duration to a millisecond duration to customize
settling. The default is 300ms. Set --rc-bottom-sheet-snap-easing to a CSS
easing function to customize the settle curve. The theme-free fallback is
cubic-bezier(0.4, 0, 0.2, 1); themes can map both properties to their spatial
motion scheme.
API
Properties
| Property | Markup | Type | Default | Description |
|---|---|---|---|---|
lightDismiss | light-dismiss | boolean | true | Bottom sheets light-dismiss by default. Set `light-dismiss="false"` as a property from JavaScript when a sheet must be closed only by explicit actions. |
resize | resize | 'none' | 'both' | 'horizontal' | 'vertical' | 'vertical' | Bottom sheets resize vertically from the top edge by default. |
resizeOrigin | resize-origin | ResizeOrigin | 'top' | Top-origin resizing keeps the sheet docked to the viewport bottom. |
resizeHandle | resize-handle | string | '[data-rc-bottom-sheet-handle]' | Default optional handle selector. Falls back to top-edge resize when absent. |
snapPoints | snap-points | string | '' | Whitespace-separated CSS heights in ascending order. Each height becomes an addressable snap target. Slow releases choose the nearest target, while decisive swipes choose the first or last target. |
swipeDismiss | swipe-dismiss | boolean | true | Whether a downward pointer resize of at least 96 pixels requests close. When false, a decisive downward swipe settles at the first snap point. |
swipeVelocity | swipe-velocity | number | 500 | Minimum release velocity, in pixels per second, that counts as a decisive swipe rather than a slow deliberate drag. A swipe jumps straight to the extreme `snap-points` entry in its direction; below this, the release settles to the nearest point instead. |
movable | movable | boolean | false | Allow the dialog to be moved by dragging. Use `move-handle` to restrict dragging to a specific child element (e.g. a titlebar). |
moveHandle | move-handle | string | '' | CSS selector (within the inner `<dialog>`) for the drag handle element. |
moveBounds | move-bounds | 'viewport' | 'parent' | 'viewport' | Bounds constraint for movement. |
moveStep | move-step | number | 4 | Keyboard arrow-key step in px for moving. |
resizeThreshold | resize-threshold | number | 8 | Edge hit-test thickness in px for resize detection. |
resizeStep | resize-step | number | 4 | Keyboard arrow-key step in px for resizing. |
closedBy | closed-by | 'any' | 'closerequest' | 'none' | '' | '' | Proxied to the inner `<dialog closedby="...">` attribute. Progressive enhancement, no-op in older browsers. - `'any'` — Escape OR backdrop click closes the dialog. - `'closerequest'` — Escape only (browser default for modal dialogs). - `'none'` — Only programmatic `close()` / `requestClose()`. - `''` — Attribute absent; use `light-dismiss` for JS fallback. |
modal | JS property only | boolean | true | Whether to open as modal with controlled open. Default: `true`. |
open | open | boolean | Not specified | Whether the inner `<dialog>` is currently open. Host writes update silently. |
defaultOpen | default-open | boolean | Not specified | Initial uncontrolled open state. |
returnValue | JS property only | string | Not specified | The return value set when the dialog was closed. |
Methods
| Method | Description |
|---|---|
snapTo(index: number, behavior: 'animated' | 'instant') | Snaps to a declared target by zero-based index. Finite fractional indices are truncated and out-of-range indices clamp to the nearest endpoint. Non-finite indices and calls without valid snap points have no effect. |
showModal() | Opens the inner `<dialog>` as a modal and fires `rc-dialog-open`. |
show() | Opens the inner `<dialog>` (non-modal) and fires `rc-dialog-open`. |
close(returnValue?: string) | Closes the inner `<dialog>`, optionally setting a return value. |
requestClose(returnValue?: string) | Requests the inner `<dialog>` to close, firing a cancelable `cancel` event first. |
Events
| Event | Detail type | Description |
|---|---|---|
rc-bottom-sheet-snap | CustomEvent | Fires when a drag release or `snapTo()` call selects a snap target. `detail: { index, height, trigger }` |
rc-dialog-open | CustomEvent | Inherited from `rc-dialog`; fired when the sheet opens. |
rc-dialog-toggle | CustomEvent | Inherited from `rc-dialog`; fired when user/native interaction changes open state. |
rc-dialog-request-close | No detail type documented | Inherited from `rc-dialog`; cancelable close request. |
rc-dialog-cancel | No detail type documented | Inherited from `rc-dialog`; backward-compatible cancel alias. |
rc-dialog-close | No detail type documented | Inherited from `rc-dialog`; fired after the sheet closes. |
Slots
| Name | Description |
|---|---|
(default) | Place a `<dialog>` element with the sheet content and optional `[data-rc-bottom-sheet-handle]` resize handle here. |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--rc-bottom-sheet-bg | Canvas | Sheet surface background. |
--rc-bottom-sheet-color | CanvasText | Sheet text color. |
--rc-bottom-sheet-radius | 1rem 1rem 0 0 | Sheet corner radius. |
--rc-bottom-sheet-shadow | none | Sheet elevation shadow. |
--rc-bottom-sheet-position | fixed | Positioning scheme. Use `absolute` with `show()` to dock a non-modal sheet to a positioned ancestor. |
--rc-bottom-sheet-inset-block-end | 0px | Distance from the positioning container's block-end edge. |
--rc-bottom-sheet-inset-inline | 0px | Inline-axis inset used with automatic margins. |
--rc-bottom-sheet-max-inline-size | 40rem | Maximum sheet width. |
--rc-bottom-sheet-max-block-size | 70dvh | Maximum sheet height. |
--rc-bottom-sheet-padding | 0 1rem 1rem | Sheet padding. |
--rc-bottom-sheet-scrim | var(--rc-dialog-scrim) | Modal backdrop color. |
--rc-bottom-sheet-handle-color | GrayText | Resize handle color. |
--rc-bottom-sheet-handle-inline-size | 2rem | Resize handle width. |
--rc-bottom-sheet-handle-block-size | 0.25rem | Resize handle height. |
--rc-bottom-sheet-handle-radius | 999px | Resize handle corner radius. |
--rc-bottom-sheet-handle-target-inline-size | 100% | Pointer target width for a direct-child handle. |
--rc-bottom-sheet-handle-target-block-size | 3rem | Pointer target height for a direct-child handle. |
--rc-bottom-sheet-handle-target-margin | 0 | Margin around a direct-child handle target. |
--rc-bottom-sheet-snap-duration | 300ms | Duration of the settle animation after a drag release or `snapTo()` call. |
--rc-bottom-sheet-snap-easing | cubic-bezier(0.4, 0, 0.2, 1) | Easing for the settle animation after a drag release or `snapTo()` call. |
--rc-dialog-scrim | color-mix(in srgb, CanvasText 32%, transparent) | Modal backdrop color. |
CSS Parts
No CSS parts are documented in the custom elements manifest.