Lightweight, Touch-Enabled Bottom Sheet with Vanilla JS
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); 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.
The post Lightweight, Touch-Enabled Bottom Sheet with Vanilla JS appeared first on CSS Script.
According to industry reports, the number of connected Internet of Things (IoT) devices reached 16.6…
Medical technology giant Stryker Corporation confirmed on March 11, 2026, that it suffered a significant…
GREELEY, Colo. (AP) — Thousands of workers for the world’s largest meatpacking company began a…
One of the state’s most unusual colleges, the aviation-heavy Daniel Webster College that lasted next…
Curled wood shavings sprinkled across Jim McLaughlin’s workspace, filling the cabin connected to the garage…
For more than 150 years, a small band of Loudon property owners who live along…
This website uses cookies.