
Bottom sheets are used on mobile applications all the time. They offer a simple way to display extra information or actions when needed.
In this article, I will show you how to create a draggable, touch-enabled, iOS & Android inspired bottom sheet with smooth slide animations on the page by using plain JavaScript. Let’s get started.
How to use it:
1. The required HTML structure for the bottom sheet. Feel free to add or remove content by following the comments as follows:
<!-- The sheet component -->
<div id="sheet" class="column items-center justify-end" aria-hidden="true">
<!-- Dark background for the sheet -->
<div class="overlay"></div>
<!-- The sheet itself -->
<div class="contents column">
<!-- Sheet controls -->
<header class="controls">
<!-- The thing to drag if you want to resize the sheet -->
<div class="draggable-area">
<div class="draggable-thumb"></div>
</div>
<!-- Button to close the sheet -->
<button class="close-sheet" type="button" title="Close the sheet">×</button>
</header>
<!-- Body of the sheet -->
<main class="body fill column">
<h2>Hello, World!</h2>
</main>
</div>
</div>2. The core CSS styles for the bottom sheet. Feel free to override the CSS variables to create your own styles.
:root {
--background: #fff;
--foreground: #000;
--divider: #dcdcdc;
--overlay: #888;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #000;
--foreground: #fff;
--divider: #333;
}
}
#sheet {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
visibility: visible;
transition: opacity 0.5s, visibility 0.5s;
}
#sheet[aria-hidden="true"] {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
#sheet .overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
background: var(--overlay);
opacity: 0.5;
}
#sheet .contents {
border-radius: 1rem 1rem 0 0;
background: var(--background);
position: relative;
overflow-y: hidden;
--default-transitions: transform 0.5s, border-radius 0.5s;
transition: var(--default-transitions);
transform: translateY(0);
max-height: 100vh;
height: 30vh;
max-width: 70rem;
box-sizing: border-box;
padding: 1rem;
padding-top: 3rem;
}
#sheet .contents:not(.not-selectable) {
transition: var(--default-transitions), height 0.5s;
}
#sheet .contents.fullscreen {
border-radius: 0;
}
#sheet[aria-hidden="true"] .contents {
transform: translateY(100%);
}
#sheet .draggable-area {
position: absolute;
top: 0;
left: 0;
right: 0;
width: 3rem;
margin: auto;
padding: 1rem;
cursor: grab;
}
#sheet .draggable-thumb {
width: inherit;
height: 0.25rem;
background: var(--divider);
border-radius: 0.125rem;
}
#sheet .close-sheet {
position: absolute;
right: 0;
top: 0;
border: none;
}
#sheet .body {
height: 100%;
overflow-y: auto;
gap: 1rem;
}3. The main JavaScript to enable the bottom sheet.
const $ = document.querySelector.bind(document)
const sheet = $("#sheet")
const sheetContents = sheet.querySelector(".contents")
const draggableArea = sheet.querySelector(".draggable-area")
let sheetHeight // in vh
const setSheetHeight = (value) => {
sheetHeight = Math.max(0, Math.min(100, value))
sheetContents.style.height = `${sheetHeight}vh`
if (sheetHeight === 100) {
sheetContents.classList.add("fullscreen")
} else {
sheetContents.classList.remove("fullscreen")
}
}
const setIsSheetShown = (value) => {
sheet.setAttribute("aria-hidden", String(!value))
}
// Open the sheet when clicking the 'open sheet' button
setSheetHeight(Math.min(50, 720 / window.innerHeight * 100))
setIsSheetShown(true)
// Hide the sheet when clicking the 'close' button
sheet.querySelector(".close-sheet").addEventListener("click", () => {
setIsSheetShown(false)
})
// Hide the sheet when clicking the background
sheet.querySelector(".overlay").addEventListener("click", () => {
setIsSheetShown(false)
})
const touchPosition = (event) =>
event.touches ? event.touches[0] : event
let dragPosition
const onDragStart = (event) => {
dragPosition = touchPosition(event).pageY
sheetContents.classList.add("not-selectable")
draggableArea.style.cursor = document.body.style.cursor = "grabbing"
}
const onDragMove = (event) => {
if (dragPosition === undefined) return
const y = touchPosition(event).pageY
const deltaY = dragPosition - y
const deltaHeight = deltaY / window.innerHeight * 100
setSheetHeight(sheetHeight + deltaHeight)
dragPosition = y
}
const onDragEnd = () => {
dragPosition = undefined
sheetContents.classList.remove("not-selectable")
draggableArea.style.cursor = document.body.style.cursor = ""
if (sheetHeight < 25) {
setIsSheetShown(false)
} else if (sheetHeight > 75) {
setSheetHeight(100)
} else {
setSheetHeight(50)
}
}
draggableArea.addEventListener("mousedown", onDragStart)
draggableArea.addEventListener("touchstart", onDragStart)
window.addEventListener("mousemove", onDragMove)
window.addEventListener("touchmove", onDragMove)
window.addEventListener("mouseup", onDragEnd)
window.addEventListener("touchend", onDragEnd)






