Lightweight, Touch-Enabled Bottom Sheet with Vanilla JS

Category: Javascript , Modal & Popup , Recommended | November 10, 2025
Authorjqueryscript
Last UpdateNovember 10, 2025
LicenseMIT
Views149 views
Lightweight, Touch-Enabled Bottom Sheet with Vanilla JS

This is a responsive, draggable, iOS-inspired bottom sheet that slides up from the screen’s bottom edge and responds to touch and mouse gestures.

Features:

  • Vanilla JavaScript: Pure JavaScript with zero dependencies.
  • Touch and Mouse Support: Uses pointer events API for unified handling across desktop and mobile devices.
  • Drag-to-Dismiss: Users can pull the sheet down to dismiss it naturally, with intelligent height thresholds that determine whether to hide, restore, or expand to fullscreen.
  • Hardware-Accelerated Animations: Leverages CSS transforms and transitions for 60fps performance.
  • Fullscreen Expansion: Automatically expands to fullscreen when dragged above 75% viewport height, with proper handling for content overflow and scrolling.

Use Cases:

  • Mobile-First Modals: Replace traditional modal dialogs with bottom-aligned sheets that feel native on mobile devices.
  • Action Sheets: Implement iOS-style action pickers for mobile web applications.
  • Detail Panels: Create expandable information panels that users can quickly access and dismiss.
  • Settings Menus: Build collapsible settings or filters that don’t obstruct the main content.

How To Use It:

1. Create a trigger button that will open the bottom sheet when clicked.

<button class="show-modal">Show Bottom Sheet</button>

2. Create the bottom sheet structure with a sheet overlay for the backdrop, content wrapper for the main container, and a draggable header with the drag handle icon.

<div class="bottom-sheet">
  <div class="sheet-overlay"></div>
  <div class="content">
    <div class="header">
      <div class="drag-icon"><span></span></div>
    </div>
    <div class="body">
      <h2>Bottom Sheet Modal</h2>
      <p>Any Content Here</p>
    </div>
  </div>
</div>

3. Add the CSS to style the component and control its states. The key parts are the position: fixed for the main container and the transform: translateY() on the .content to handle the sliding animation. The .show class makes the sheet visible.

.bottom-sheet {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  opacity: 0;
  pointer-events: none;
  align-items: center;
  flex-direction: column;
  justify-content: flex-end;
  transition: 0.1s linear;
}
.bottom-sheet.show {
  opacity: 1;
  pointer-events: auto;
}
.bottom-sheet .sheet-overlay {
  position: fixed;
  inset: 0;
  z-index: -1;
  opacity: 0.5;
  background: #000;
}
.bottom-sheet .content {
  width: 100%;
  position: relative;
  background: #fff;
  max-height: 100vh;
  height: 50vh;
  max-width: 1024px;
  padding: 24px;
  transform: translateY(100%);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.03);
  transition: 0.3s ease;
}
.bottom-sheet.show .content {
  transform: translateY(0%);
}
.bottom-sheet.dragging .content {
  transition: none;
}
.bottom-sheet.fullscreen .content {
  border-radius: 0;
  overflow-y: hidden;
}
.bottom-sheet .header {
  display: flex;
  justify-content: center;
}
.header .drag-icon {
  cursor: grab;
  user-select: none;
  padding: 15px;
  margin-top: -15px;
}
.header .drag-icon span {
  height: 4px;
  width: 40px;
  display: block;
  background: #c7d0e1;
  border-radius: 50px;
}
.bottom-sheet .body {
  height: 100%;
  overflow-y: auto;
  padding: 15px 0 40px;
  scrollbar-width: none;
}
.bottom-sheet .body::-webkit-scrollbar {
  width: 0;
}
.bottom-sheet .body h2 {
  font-size: 1.8rem;
}
.bottom-sheet .body p {
  margin-top: 20px;
  font-size: 1.05rem;
}
/* prevent mobile reload on scroll outside bottom sheet */
body:has(.bottom-sheet.show) {
  pointer-events: none;
  overflow: hidden;
  height: 100dvh;
}

4. The JavaScript ties everything together. It handles opening and closing the sheet, tracking the drag gesture, and updating the sheet’s height accordingly.

// Select DOM elements
const showModalBtn = document.querySelector(".show-modal");
const bottomSheet = document.querySelector(".bottom-sheet");
const sheetOverlay = bottomSheet.querySelector(".sheet-overlay");
const sheetContent = bottomSheet.querySelector(".content");
const dragIcon = bottomSheet.querySelector(".drag-icon");
let isDragging = false;
let startY;
let startHeight;
// Show the bottom sheet, hide body vertical scrollbar, and call updateSheetHeight
const showBottomSheet = () => {
  bottomSheet.classList.add("show");
  updateSheetHeight(50);
};
const updateSheetHeight = (height) => {
  sheetContent.style.height = `${height}vh`; //updates the height of the sheet content
  // Toggles the fullscreen class to bottomSheet if the height is equal to 100
  bottomSheet.classList.toggle("fullscreen", height === 100);
};
// Hide the bottom sheet and show body vertical scrollbar
const hideBottomSheet = () => {
  bottomSheet.classList.remove("show");
};
// Sets initial drag position, sheetContent height and add dragging class to the bottom sheet
const dragStart = (e) => {
  isDragging = true;
  startY = e.pageY || e.touches?.[0].pageY;
  startHeight = parseInt(sheetContent.style.height);
  bottomSheet.classList.add("dragging");
};
// Calculates the new height for the sheet content and call the updateSheetHeight function
const dragging = (e) => {
  if (!isDragging) return;
  const delta = startY - (e.pageY || e.touches?.[0].pageY);
  const newHeight = startHeight + (delta / window.innerHeight) * 100;
  updateSheetHeight(newHeight);
};
// Determines whether to hide, set to fullscreen, or set to default
// height based on the current height of the sheet content
const dragStop = () => {
  isDragging = false;
  bottomSheet.classList.remove("dragging");
  const sheetHeight = parseInt(sheetContent.style.height);
  sheetHeight < 25
    ? hideBottomSheet()
    : sheetHeight > 75
    ? updateSheetHeight(100)
    : updateSheetHeight(50);
};
dragIcon.addEventListener("pointerdown", dragStart);
document.addEventListener("pointermove", dragging);
document.addEventListener("pointerup", dragStop);
dragIcon.addEventListener("touchstart", dragStart);
document.addEventListener("touchmove", dragging);
document.addEventListener("touchend", dragStop);
sheetOverlay.addEventListener("click", hideBottomSheet);
showModalBtn.addEventListener("click", showBottomSheet);

FAQs

Q: The dragging feels laggy on a content-heavy page. What can I do?
A: The script is already optimized to disable CSS transitions during a drag by adding the .dragging class. If you still experience lag, check for other expensive JavaScript operations or complex animations running on your page that could be competing for resources.

Q: How can I load dynamic content into the bottom sheet before it appears?
A: Before you call the showBottomSheet() function, you can fetch your data and then inject it into the DOM. For example: document.querySelector('.bottom-sheet .body').innerHTML = yourDynamicHTML; then call showBottomSheet();.

Q: How do I change the default height or the snap points?
A: You can adjust the logic in the dragStop function. The values 25 and 75 represent the percentage of the sheet’s height that trigger the hide and fullscreen actions. You can change these thresholds. To alter the default open height, modify the 50 in both the showBottomSheet and dragStop functions to your desired viewport height percentage.

Q: Can I close the sheet programmatically?
A: Yes. Call the hideBottomSheet() function from anywhere in your script to close it.

You Might Be Interested In:


Leave a Reply