Skip to main content

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

Installation

npm install @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

PropertyMarkupTypeDefaultDescription
lightDismisslight-dismissbooleantrueBottom sheets light-dismiss by default. Set `light-dismiss="false"` as a property from JavaScript when a sheet must be closed only by explicit actions.
resizeresize'none' | 'both' | 'horizontal' | 'vertical''vertical'Bottom sheets resize vertically from the top edge by default.
resizeOriginresize-originResizeOrigin'top'Top-origin resizing keeps the sheet docked to the viewport bottom.
resizeHandleresize-handlestring'[data-rc-bottom-sheet-handle]'Default optional handle selector. Falls back to top-edge resize when absent.
snapPointssnap-pointsstring''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.
swipeDismissswipe-dismissbooleantrueWhether a downward pointer resize of at least 96 pixels requests close. When false, a decisive downward swipe settles at the first snap point.
swipeVelocityswipe-velocitynumber500Minimum 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.
movablemovablebooleanfalseAllow the dialog to be moved by dragging. Use `move-handle` to restrict dragging to a specific child element (e.g. a titlebar).
moveHandlemove-handlestring''CSS selector (within the inner `<dialog>`) for the drag handle element.
moveBoundsmove-bounds'viewport' | 'parent''viewport'Bounds constraint for movement.
moveStepmove-stepnumber4Keyboard arrow-key step in px for moving.
resizeThresholdresize-thresholdnumber8Edge hit-test thickness in px for resize detection.
resizeStepresize-stepnumber4Keyboard arrow-key step in px for resizing.
closedByclosed-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.
modalJS property onlybooleantrueWhether to open as modal with controlled open. Default: `true`.
openopenbooleanNot specifiedWhether the inner `<dialog>` is currently open. Host writes update silently.
defaultOpendefault-openbooleanNot specifiedInitial uncontrolled open state.
returnValueJS property onlystringNot specifiedThe return value set when the dialog was closed.

Methods

MethodDescription
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

EventDetail typeDescription
rc-bottom-sheet-snapCustomEventFires when a drag release or `snapTo()` call selects a snap target. `detail: { index, height, trigger }`
rc-dialog-openCustomEventInherited from `rc-dialog`; fired when the sheet opens.
rc-dialog-toggleCustomEventInherited from `rc-dialog`; fired when user/native interaction changes open state.
rc-dialog-request-closeNo detail type documentedInherited from `rc-dialog`; cancelable close request.
rc-dialog-cancelNo detail type documentedInherited from `rc-dialog`; backward-compatible cancel alias.
rc-dialog-closeNo detail type documentedInherited from `rc-dialog`; fired after the sheet closes.

Slots

NameDescription
(default)Place a `<dialog>` element with the sheet content and optional `[data-rc-bottom-sheet-handle]` resize handle here.

CSS Custom Properties

PropertyDefaultDescription
--rc-bottom-sheet-bgCanvasSheet surface background.
--rc-bottom-sheet-colorCanvasTextSheet text color.
--rc-bottom-sheet-radius1rem 1rem 0 0Sheet corner radius.
--rc-bottom-sheet-shadownoneSheet elevation shadow.
--rc-bottom-sheet-positionfixedPositioning scheme. Use `absolute` with `show()` to dock a non-modal sheet to a positioned ancestor.
--rc-bottom-sheet-inset-block-end0pxDistance from the positioning container's block-end edge.
--rc-bottom-sheet-inset-inline0pxInline-axis inset used with automatic margins.
--rc-bottom-sheet-max-inline-size40remMaximum sheet width.
--rc-bottom-sheet-max-block-size70dvhMaximum sheet height.
--rc-bottom-sheet-padding0 1rem 1remSheet padding.
--rc-bottom-sheet-scrimvar(--rc-dialog-scrim)Modal backdrop color.
--rc-bottom-sheet-handle-colorGrayTextResize handle color.
--rc-bottom-sheet-handle-inline-size2remResize handle width.
--rc-bottom-sheet-handle-block-size0.25remResize handle height.
--rc-bottom-sheet-handle-radius999pxResize handle corner radius.
--rc-bottom-sheet-handle-target-inline-size100%Pointer target width for a direct-child handle.
--rc-bottom-sheet-handle-target-block-size3remPointer target height for a direct-child handle.
--rc-bottom-sheet-handle-target-margin0Margin around a direct-child handle target.
--rc-bottom-sheet-snap-duration300msDuration of the settle animation after a drag release or `snapTo()` call.
--rc-bottom-sheet-snap-easingcubic-bezier(0.4, 0, 0.2, 1)Easing for the settle animation after a drag release or `snapTo()` call.
--rc-dialog-scrimcolor-mix(in srgb, CanvasText 32%, transparent)Modal backdrop color.

CSS Parts

No CSS parts are documented in the custom elements manifest.