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-resize-startrc-bottom-sheet-snap

Installation​

npm install @rcarls/rc-webcomponents@next
import '@rcarls/rc-webcomponents/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 closed-by="any" 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. Set resize="none" to hold the sheet at its current snap point; the option is reactive, so restoring resize="vertical" re-enables dragging.

The sheet stays docked at its block-end edge: snapping sets only its height, and its position is left to CSS. A software keyboard that resizes the viewport (interactive-widget=resizes-content) re-applies the current snap point above the keyboard. When a field inside the sheet takes focus, the sheet scrolls it into view within its own scroll containers, and grows past its snap height and maximum block size only when scrolling cannot reveal it, returning to its snap point once focus leaves the sheet.

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. Any length works, including calc() and var(). Each point is resolved inside the sheet when a snap applies, so a custom property the page sets or animates, for example from a scroll timeline, is read at its current value. Percentages are of the viewport height. 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.

rc-bottom-sheet-resize-start fires before an interactive resize changes the sheet geometry. Consumers can use it to prepare content that an expansion drag will progressively reveal.

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. Animated snaps emit only after the settle motion finishes; an interrupted animation does not emit a stale target. This makes the event a safe point for discrete content or accessibility-state changes.

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
closedByclosed-by'any' | 'closerequest' | 'none' | '''any'Bottom sheets close on Escape or a backdrop click by default. Set `closed-by="closerequest"` when a backdrop click must not close the sheet, or `closed-by="none"` when only explicit actions may close it.
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. Any CSS length works, including `calc()` and `var()`, and is resolved in the sheet's own context when a snap is applied, so a custom property the page animates (for example from a scroll timeline) is read at its current value. Percentages are of the viewport height.
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). Reactive: changing this or any other `move-*` option after setup rebuilds the move behavior and re-queries `move-handle`.
moveHandlemove-handlestring''CSS selector (within the inner `<dialog>`) for the drag handle element.
moveBoundsmove-bounds'viewport' | 'parent''viewport'Area that moving and resizing stay inside: `viewport`, or `parent` for the element that contains `<rc-dialog>`.
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.
variantvariantRCDialogVariant'standard'Surface presentation. Fullscreen geometry follows the visual viewport while open; a side sheet docks to the inline end at full block size.
modalJS property onlybooleantrueWhether controlled open state uses `showModal()` instead of `show()`. JavaScript property only; no attribute is observed.
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-resize-startCustomEventFires when an interactive resize begins. `detail: { height, inputType }`
rc-bottom-sheet-snapCustomEventFires after a drag release or `snapTo()` call settles at its snap target. `detail: { index, height, trigger }`, with `height` in the sheet's own CSS pixels, the unit of its snap points.
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-offset50%Block-start position of the handle indicator's center within its target. Set a length to pin it above a title inside the handle.
--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-inline-sizefit-contentStandard surface preferred inline size. Set `100%` for a task dialog whose fluid content would otherwise shrink it to its minimum.
--rc-dialog-min-inline-sizeautoStandard surface minimum inline size.
--rc-dialog-max-inline-sizenoneStandard surface maximum inline size.
--rc-dialog-max-block-sizecalc(100dvb - 2rem)Standard surface block bound. The surface never scrolls itself, so a scrollable dialog should scroll a dedicated inner region.
--rc-dialog-padding1emStandard surface padding.
--rc-dialog-border0Surface border.
--rc-dialog-radius0Surface corner radius.
--rc-dialog-backgroundCanvasSurface background.
--rc-dialog-colorCanvasTextSurface foreground.
--rc-dialog-shadownoneSurface shadow.
--rc-dialog-scrimcolor-mix(in srgb, CanvasText 32%, transparent)Modal backdrop color.
--rc-dialog-fullscreen-padding-blockNot specifiedFullscreen block padding. Defaults to a safe-area-aware inset, which is the one value the fullscreen variant genuinely needs of its own.
--rc-dialog-fullscreen-padding-inlineNot specifiedFullscreen inline padding, safe-area aware.
--rc-dialog-fullscreen-backgroundNot specifiedFullscreen background. Defers to `--rc-dialog-background` when unset.
--rc-dialog-side-inline-sizemin(100%, 25rem)Side sheet inline size.
--rc-dialog-side-backgroundNot specifiedSide sheet background. Defers to `--rc-dialog-background` when unset.
--rc-dialog-side-radiusNot specifiedSide sheet start-corner radius. Defers to `--rc-dialog-radius` when unset.
--rc-dialog-side-shadowNot specifiedSide sheet shadow. Defers to `--rc-dialog-shadow` when unset.
--rc-dialog-visual-viewport-leftNot specifiedRead-only fullscreen visual viewport left edge.
--rc-dialog-visual-viewport-topNot specifiedRead-only fullscreen visual viewport top edge.
--rc-dialog-visual-viewport-widthNot specifiedRead-only fullscreen visual viewport width.
--rc-dialog-visual-viewport-heightNot specifiedRead-only fullscreen visual viewport height. Fullscreen geometry, meaning position, size, margin, radius, and shadow, is structural rather than themable: filling the visual viewport is what keeps the surface correct under browser zoom and a software keyboard. While a keyboard closes, the surface takes only growth, so the shorter transitional sizes some mobile browsers report do not flash through. Side sheet placement is structural too: the surface docks to the inline end at full block size, with physical safe-area padding on every edge; only its inline size, background, start-corner radius, and shadow are themable. Neither docked variant moves or resizes, so `movable` and `resize` apply to the standard variant only. This element also writes `--rc-dialog-visual-viewport-left`, `-top`, `-width`, and `-height` onto itself while a fullscreen dialog is open. Those are geometry outputs, not theme inputs: read them if something inside the dialog needs the same measurements, but setting them does not move the dialog, and the component overwrites them on the next viewport change. They are candidates for private `--_rc-dialog-*` names in the next compatible API revision.