Bootstrap 5 Component
Touch-friendly bottom sheet with physics-based spring gestures, backdrop, focus trap, and full keyboard support.
Add data-bs-toggle="sheet" to any trigger element - no JavaScript required.
Close with the data-bs-dismiss="sheet" button, the drag handle, or Escape.
<button data-bs-toggle="sheet" data-bs-target="#mySheet">
Open sheet
</button>
<div class="sheet" id="mySheet">
<div class="sheet-handle"></div>
<div class="sheet-header">
<h5 class="sheet-title">Title</h5>
<button class="btn-close" data-bs-dismiss="sheet"></button>
</div>
<div class="sheet-body">...</div>
</div>
No JavaScript - just data attributes
All animations - show, hide, snap-back - are driven by a spring model.
springDampingRatio controls bounciness; springResponse controls speed.
Swipe down past 50% of sheet height to dismiss. Drag up against rubber-band resistance - release and the spring snaps you back.
new BootstrapSheet('#mySheet', {
gestures: true,
springDampingRatio: 0.8,
springResponse: 0.4,
});
Three behaviours with a single option.
'static' mode blocks dismissal and triggers a shake animation -
tactile feedback that the sheet requires explicit action.
// Dismissible (default)
new BootstrapSheet('#mySheet', { backdrop: true });
// Static - shake on outside click
new BootstrapSheet('#mySheet', { backdrop: 'static' });
// No backdrop
new BootstrapSheet('#mySheet', { backdrop: false });
Click outside → closes
Click outside → shakes
No overlay
slide.bs.sheet fires on every animation frame during drag.
detail.ratio runs from 1 (fully open) to 0 (dismissed),
letting you synchronize any custom visual to the gesture in real time.
const mySheet = document.getElementById('mySheet');
mySheet.addEventListener('slide.bs.sheet', (e) => {
const { ratio, velocity, deltaY } = e.detail;
// drive any visual with ratio 1→0
bar.style.width = (ratio * 100) + '%';
});
A detent is a height the sheet rests at, written as the fraction of its height that is visible. Release the sheet and it settles at whichever detent your gesture was heading for - a hard flick skips past the ones in between.
With undimmedDetent the sheet stays non-modal while collapsed: no dimming,
no focus trap, and the page behind stays clickable.
const sheet = new BootstrapSheet('#mySheet', {
detents: [0.35, 0.7, 1],
});
sheet.addEventListener('detentchange.bs.sheet', (e) => {
console.log(e.detail.detent, e.detail.previousDetent);
});
sheet.setDetent(0.7);
While the peek sheet is open, these buttons still work - it is not modal.
role="dialog" and aria-modal="true" applied
automaticallykeyboard option)
Opened with a data attribute - no JavaScript needed.
Close via the × button, the drag handle, or the Escape key.
Drag down past the midpoint to dismiss. Drag up - rubber-band resistance kicks in. Release below the midpoint and the spring snaps back.
Drag the handle and watch the ratio bar update live on the page behind this sheet.
Drag the sheet up and down: it settles at 35 %, 70 % or fully open. Below the largest detent a drag anywhere expands the sheet first, so this text only scrolls once you are at the top.
Every jump goes through the same spring the gestures use, and fires
detentchange.bs.sheet once it settles.
This sheet is resting at undimmedDetent, so it presents non-modally: page
scrolling is not locked, the backdrop is transparent to clicks, the page is not inert and
focus is not trapped. Try scrolling or clicking the page behind it.
Drag it up past the peek and the backdrop fades in - it becomes modal.