Touch-friendly bottom sheet component for Bootstrap 5 with swipe gestures, backdrop support, and focus management.
Bootstrap Sheet is a mobile-first bottom sheet component that slides up from the bottom of the viewport. It supports touch gestures for intuitive interaction, keyboard navigation for accessibility, and provides a rich JavaScript API for programmatic control.
Key features include:
Click the button below to launch a basic sheet with header, body, and footer sections.
<!-- Button trigger -->
<button type="button" class="btn btn-primary" data-bs-toggle="sheet" data-bs-target="#basicSheet">
Launch basic sheet
</button>
<!-- Sheet -->
<div class="sheet" id="basicSheet">
<div class="sheet-header">
<h5 class="sheet-title">Sheet title</h5>
<button type="button" class="btn-close" data-bs-dismiss="sheet" aria-label="Close"></button>
</div>
<div class="sheet-body">
<p>Sheet body content goes here.</p>
</div>
<div class="sheet-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="sheet">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
Add a drag handle to enable swipe-to-dismiss gestures. Swipe down on the handle to close the sheet.
<div class="sheet" id="dragHandleSheet">
<div class="sheet-handle" data-bs-drag="sheet"></div>
<div class="sheet-header">
<h5 class="sheet-title">Swipe down to close</h5>
</div>
<div class="sheet-body">
<p>Try swiping down on the handle above to close this sheet.</p>
</div>
</div>
When content exceeds the maximum height, the sheet body becomes scrollable. This example demonstrates scrollspy integration.
<div class="sheet" id="scrollableSheet">
<div class="sheet-header">
<h5 class="sheet-title">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="sheet"></button>
</div>
<div class="sheet-body" data-bs-spy="scroll" data-bs-target="#navbar-scrollspy">
<!-- Long scrollable content -->
</div>
</div>
Use data-bs-backdrop="static" to prevent closing when clicking outside. Perfect for confirmation
dialogs.
<div class="sheet" id="confirmSheet" data-bs-backdrop="static">
<div class="sheet-header">
<h5 class="sheet-title">
<i class="bi bi-exclamation-triangle text-danger"></i> Confirm deletion
</h5>
</div>
<div class="sheet-body">
<p>Are you sure you want to delete your account? This action cannot be undone.</p>
</div>
<div class="sheet-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="sheet">Cancel</button>
<button type="button" class="btn btn-danger">Delete</button>
</div>
</div>
Set data-bs-backdrop="false" to disable the backdrop overlay.
<div class="sheet" id="noBackdropSheet" data-bs-backdrop="false">
<div class="sheet-header">
<h5 class="sheet-title">No backdrop</h5>
<button type="button" class="btn-close" data-bs-dismiss="sheet"></button>
</div>
<div class="sheet-body">
<p>This sheet has no backdrop overlay.</p>
</div>
</div>
<div class="sheet" id="actionSheet">
<div class="sheet-body">
<button type="button" class="btn btn-light w-100 mb-2">
<i class="bi bi-share"></i> Share
</button>
<button type="button" class="btn btn-light w-100 mb-2">
<i class="bi bi-download"></i> Download
</button>
<button type="button" class="btn btn-danger w-100 mb-2">
<i class="bi bi-trash"></i> Delete
</button>
<button type="button" class="btn btn-secondary w-100" data-bs-dismiss="sheet">
Cancel
</button>
</div>
</div>
Filter locations in real-time with a search input. This example demonstrates dynamic content filtering.
<div class="sheet" id="locationSheet" style="max-height: 80vh;">
<div class="sheet-header">
<h5 class="sheet-title">Select EU Country</h5>
<button type="button" class="btn-close" data-bs-dismiss="sheet"></button>
</div>
<div class="p-3 border-bottom">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="locationSearch" placeholder="Search countries...">
</div>
</div>
<div class="sheet-body" id="locationList">
<!-- Dynamically populated -->
</div>
</div>
Create wizard-like forms with multiple steps. Navigation between steps is handled with JavaScript.
<div class="sheet" id="formSheet">
<div class="sheet-header">
<h5 class="sheet-title">Registration Form</h5>
<button type="button" class="btn-close" data-bs-dismiss="sheet"></button>
</div>
<div class="sheet-body">
<!-- Step 1: Personal Info -->
<div class="form-step" id="step1">
<h6 class="mb-3">Step 1: Personal Information</h6>
<!-- Form fields -->
</div>
<!-- Step 2: Address -->
<div class="form-step d-none" id="step2">
<h6 class="mb-3">Step 2: Address</h6>
<!-- Form fields -->
</div>
</div>
<div class="sheet-footer">
<button type="button" class="btn btn-secondary" id="prevBtn">Previous</button>
<button type="button" class="btn btn-primary" id="nextBtn">Next</button>
</div>
</div>
Bootstrap Sheet fires several events during its lifecycle. Monitor them in real-time below.
const sheetEl = document.getElementById('eventsSheet');
sheetEl.addEventListener('show.bs.sheet', (e) => {
console.log('Sheet is about to show');
});
sheetEl.addEventListener('shown.bs.sheet', (e) => {
console.log('Sheet is now visible');
});
sheetEl.addEventListener('hide.bs.sheet', (e) => {
console.log('Sheet is about to hide');
});
sheetEl.addEventListener('hidden.bs.sheet', (e) => {
console.log('Sheet is now hidden');
});
sheetEl.addEventListener('slide.bs.sheet', (e) => {
console.log('Slide event:', e.detail);
});
Search GitHub users with live API integration. Results are displayed with avatars and user information.
<div class="sheet" id="githubSheet">
<div class="sheet-header">
<h5 class="sheet-title">GitHub User Search</h5>
<button type="button" class="btn-close" data-bs-dismiss="sheet"></button>
</div>
<div class="p-3 border-bottom">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="githubSearch" placeholder="Search users...">
</div>
</div>
<div class="sheet-body" id="githubResults">
<!-- Dynamically populated -->
</div>
</div>
Interactive shopping cart with quantity controls and live total calculation.
<div class="sheet" id="cartSheet">
<div class="sheet-header">
<h5 class="sheet-title">Shopping Cart</h5>
<button type="button" class="btn-close" data-bs-dismiss="sheet"></button>
</div>
<div class="sheet-body" id="cartItems">
<!-- Dynamically populated -->
</div>
<div class="sheet-footer">
<div class="d-flex justify-content-between align-items-center w-100">
<h5 class="mb-0">Total: <span id="cartTotal">$0.00</span></h5>
<button type="button" class="btn btn-success" id="checkoutBtn">Checkout</button>
</div>
</div>
</div>
Customize the sheet appearance by overriding these Sass variables:
| Variable | Default | Description |
|---|---|---|
$sheet-zindex |
1057 |
Z-index for the sheet |
$sheet-width |
100vw |
Sheet width |
$sheet-max-width |
100% |
Maximum sheet width |
$sheet-max-height |
90vh |
Maximum sheet height |
$sheet-bg |
var(--bs-body-bg, #fff) |
Background color |
$sheet-backdrop-bg |
rgba(0, 0, 0, 0.5) |
Backdrop background color |
$sheet-backdrop-backdrop-filter |
blur(2px) |
Backdrop blur effect |
$sheet-transition-duration |
0.3s |
Animation duration |
$sheet-transition-timing |
ease-out |
Animation timing function |
$sheet-handle-bg |
var(--bs-gray-400, #dee2e6) |
Handle background color |
$sheet-handle-hover-bg |
var(--bs-gray-500, #adb5bd) |
Handle hover background |
$sheet-handle-width |
3rem |
Handle width |
$sheet-handle-height |
0.25rem |
Handle height |
$sheet-handle-margin |
0.5rem auto |
Handle margin |
$sheet-padding-x |
1rem |
Horizontal padding |
$sheet-padding-y |
1rem |
Vertical padding |
$sheet-header-padding-y |
0.75rem |
Header vertical padding |
$sheet-body-padding-y |
1rem |
Body vertical padding |
$sheet-footer-padding-y |
0.75rem |
Footer vertical padding |
$sheet-box-shadow |
0 -2px 10px rgba(0, 0, 0, 0.1) |
Box shadow |
$sheet-border-width |
1px |
Border width |
$sheet-border-color |
var(--bs-border-color, #dee2e6) |
Border color |
$sheet-border-radius |
1rem 1rem 0 0 |
Border radius (top corners) |
$sheet-focus-ring-width |
0.25rem |
Focus ring width |
$sheet-focus-ring-color |
rgba(13, 110, 253, 0.25) |
Focus ring color |
$sheet-shake-distance |
10px |
Shake animation distance |
$sheet-disabled-opacity |
0.65 |
Disabled element opacity |
Activate a sheet without writing JavaScript. Set data-bs-toggle="sheet" on a controller element,
like a button, along with a data-bs-target="#foo" or href="#foo" to target a specific
sheet to toggle.
<button type="button" data-bs-toggle="sheet" data-bs-target="#mySheet">
Launch sheet
</button>
Create a sheet with JavaScript:
const mySheet = new BootstrapSheet('#mySheet', {
backdrop: true,
keyboard: true,
gestures: true
});
You can create a sheet instance with the constructor, for example:
const sheetElement = document.getElementById('mySheet');
const sheet = new BootstrapSheet(sheetElement, {
backdrop: 'static'
});
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to
data-bs-, as in data-bs-backdrop="static".
| Name | Type | Default | Description |
|---|---|---|---|
backdrop |
boolean or string 'static' |
true |
Includes a backdrop element. Alternatively, specify static for a backdrop which doesn't
close the sheet when clicked. |
keyboard |
boolean | true |
Closes the sheet when escape key is pressed. |
focus |
boolean | true |
Puts the focus on the sheet when initialized. |
gestures |
boolean | true |
Enable or disable swipe gestures. |
swipeThreshold |
number | 50 |
Minimum swipe distance in pixels to trigger close. |
velocityThreshold |
number | 0.5 |
Minimum velocity (px/ms) to trigger close. |
minCloseDistance |
number | 50 |
Minimum distance for velocity-based close. |
closeThresholdRatio |
number | 0.3 |
Ratio of sheet height (0-1) to trigger close. |
animationDuration |
number | 300 |
Animation duration in milliseconds. |
projectionTime |
number | 200 |
Time to project velocity in milliseconds. |
dragResistanceUp |
number | 0.75 |
Resistance when dragging up (0-1, higher = more resistance). |
dragResistanceDown |
number | 0.01 |
Resistance when dragging down (0-1, higher = more resistance). |
| Method | Description |
|---|---|
show() |
Manually opens a sheet. Returns to the caller before the sheet has actually been shown (i.e. before
the shown.bs.sheet event occurs). |
hide() |
Manually hides a sheet. Returns to the caller before the sheet has actually been hidden (i.e. before
the hidden.bs.sheet event occurs). |
toggle() |
Manually toggles a sheet. Returns to the caller before the sheet has actually been shown or hidden. |
dispose() |
Destroys an element's sheet. Removes stored data and event listeners. |
getInstance(element) |
Static method which allows you to get the sheet instance associated with a DOM element. |
getOrCreateInstance(element, config) |
Static method which returns a sheet instance associated to a DOM element or creates a new one in case it wasn't initialized. |
const sheet = BootstrapSheet.getInstance('#mySheet'); // Returns a Bootstrap sheet instance
sheet.show();
sheet.hide();
sheet.toggle();
sheet.dispose();
Bootstrap Sheet's event class exposes a few events for hooking into sheet functionality. All sheet events are
fired at the sheet itself (i.e. at the <div class="sheet">).
| Event type | Description |
|---|---|
show.bs.sheet |
This event fires immediately when the show instance method is called. If caused by a
click, the clicked element is available as the relatedTarget property of the event. |
shown.bs.sheet |
This event is fired when the sheet has been made visible to the user (will wait for CSS transitions to complete). |
hide.bs.sheet |
This event is fired immediately when the hide instance method has been called. |
hidden.bs.sheet |
This event is fired when the sheet has finished being hidden from the user (will wait for CSS transitions to complete). |
slide.bs.sheet |
This event fires continuously during drag/slide gestures. The event detail contains
velocity, adjustedY, deltaY, and ratio properties.
|
const mySheetEl = document.getElementById('mySheet');
mySheetEl.addEventListener('hidden.bs.sheet', (event) => {
// do something...
});
This example demonstrates key accessibility features including focus management and keyboard navigation.
The sheet component automatically applies the following ARIA attributes:
role="dialog" - Identifies the element as a dialogaria-modal="true" - Indicates the dialog is modaltabindex="-1" - Makes the sheet programmatically focusableWhen the sheet opens:
inert attribute where supported, or
aria-hidden as fallback)
When the sheet closes:
keyboard option is true)Screen reader users will hear the sheet announced as a dialog. Ensure you provide meaningful labels:
.sheet-title for a descriptive headingaria-label or aria-labelledby to the sheet if neededThe component respects the prefers-reduced-motion media query. When users have reduced motion
enabled in their operating system, all transitions are disabled for a more comfortable experience.
This is a basic sheet example with header, body, and footer sections.
You can add any Bootstrap components or custom content here.
Try swiping down on the handle above to close this sheet.
The drag handle provides a visual affordance for touch gestures. When you drag down past the threshold, the sheet will smoothly animate closed.
You can also customize the swipe sensitivity using the swipeThreshold and
velocityThreshold options.
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
Are you sure you want to delete your account?
This action cannot be undone. All your data, including:
will be permanently removed from our servers.
This sheet has no backdrop overlay, allowing you to interact with the page content behind it.
This can be useful for non-modal interactions where you want to display information without fully blocking the underlying content.
Open, close, or drag this sheet to see events being fired in real-time in the log above.
The following events are monitored:
show.bs.sheet - Fired when show() is calledshown.bs.sheet - Fired when sheet is visiblehide.bs.sheet - Fired when hide() is calledhidden.bs.sheet - Fired when sheet is hiddenslide.bs.sheet - Fired during drag (throttled)Type a username to search