More Modals Snippets
Bottom Sheet — Free HTML CSS JS Mobile Snippet
Bottom Sheet · Modals · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Bottom Sheet — Slide-Up Mobile Panel, Drag Handle, Backdrop & Share/Filter Content Variants

A bottom sheet is a mobile-first UI pattern where a panel slides up from the bottom edge of the screen to present options, actions, or secondary content without navigating away from the current page. It is the mobile equivalent of a modal or popover — less disruptive than a full-screen overlay, more prominent than a tooltip. This snippet provides a complete bottom sheet with backdrop, a drag handle, ESC-to-close, two content variants (share actions and filter chips), and a smooth cubic-bezier slide animation.
The slide animation
The sheet starts at transform: translateY(100%) — fully below the viewport. Adding .open changes it to translateY(0). The transition uses cubic-bezier(0.32, 0.72, 0, 1) — a spring-like curve that accelerates sharply then decelerates slowly, mimicking iOS sheet physics. The same animation reverses on close. The backdrop fades from rgba(0,0,0,0) to rgba(0,0,0,0.4) in sync.
The drag handle
The 40×4px pill at the top of the sheet is a universal visual signal that the panel is draggable. This snippet shows the visual handle without drag-to-dismiss JavaScript — add touchmove/touchend listeners to implement drag-to-dismiss where the sheet follows the finger and closes if dragged more than 40% of its height.
Two content variants
Clicking "Share options" shows a 4-column action grid (Twitter, Facebook, WhatsApp, Copy link) with coloured icon blocks. The copy link button uses the Clipboard API and shows a "✓ Copied!" feedback state. Clicking "Filter results" shows a filter sheet with price range and rating chip groups — toggleChip() manages single-selection within each group.
ESC key and backdrop close
The keydown listener (added on open, removed on close) closes the sheet on Escape. The backdrop div has onclick="closeSheet()" directly. Both patterns are identical to the modal close mechanisms.
Max-width centering
On desktop, the sheet has max-width: 600px and margin: 0 auto so it does not stretch edge-to-edge. On mobile, it fills the full viewport width. The border-radius: 20px 20px 0 0 gives the characteristic rounded top corners of a native mobile sheet.
When to use a bottom sheet versus a modal
Use a bottom sheet when: the action is context-preserving (users can still see the page behind the backdrop), the content is secondary to the page (share options, filter settings, action menus), or the interaction is common on mobile native apps (iOS action sheets, Android bottom sheets). Use a standard modal when: the action requires full attention (delete confirmation, critical error, onboarding step), or the content is substantial (form, detail view, settings panel). The bottom sheet signals "quick action" to users through its position and size; the modal signals "stop and decide".
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to decode the cubic-bezier control points by hand to understand the feel of this sheet. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why cubic-bezier(0.32,0.72,0,1) produces the iOS-style fast-open-then-settle motion, and how openSheet swaps between the sc-share and sc-filter content blocks purely with inline display toggles. The same assistant can help optimize it — asking whether the keydown listener added on every open call could leak if openSheet is called twice without closing, or how to add real drag-to-dismiss without fighting the existing transform-based transition. It's also useful for extending the pattern: ask it to add a third content variant, snap points at partial heights, or a scrollable body with overscroll-behavior contain so background scroll never leaks through. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a mobile "bottom sheet" component in plain HTML, CSS, and JavaScript that slides up from the bottom of the screen over a dimming backdrop, with two swappable content variants — no libraries.
Requirements:
- A sheet element fixed to the bottom of the viewport, starting fully offscreen via transform: translateY(100%), that slides to translateY(0) when an "open" class is added, using a cubic-bezier easing that accelerates quickly at the start and decelerates gradually at the end (an iOS-style spring feel), with rounded top corners only.
- A separate backdrop element that fades its background from fully transparent to a dark translucent overlay in sync with the sheet's open state, and is not clickable at all while closed (pointer-events none) but closes the sheet when clicked while open.
- A small pill-shaped drag handle centered at the top of the sheet as a purely visual affordance.
- Two distinct content blocks inside the sheet, only one visible at a time via inline display toggling, selected by which trigger button opened the sheet: a grid of share action buttons with colored icon swatches, and a filter panel with two independently-toggling groups of selectable chip buttons (only one chip active per group at a time).
- Closing must be triggered three ways: clicking the backdrop, pressing Escape, and a button inside the sheet — and the Escape key listener must be added only while the sheet is open and removed the moment it closes, so it never fires while the sheet is already closed.
- One action button must copy the current page URL to the clipboard using the Clipboard API and show temporary "Copied!" text feedback that reverts after a couple of seconds.Step by step
How to Use
- 1Click the trigger buttons to open each sheet variantClick "Share options" to see the social action grid with copy link feedback. Click "Filter results" to see the chip-based filter sheet. Press ESC or click the backdrop to close.
- 2Add your own sheet contentCopy the .sc div structure and add a new id (e.g., sc-menu). In the HTML, add a new trigger button with onclick="openSheet('menu')". In the JS openSheet() function, add your new content type — the function shows/hides the matching .sc div automatically.
- 3Implement drag-to-dismissAdd touchstart to the .drag-handle: record startY = e.touches[0].clientY. In touchmove on the sheet: sheet.style.transform = "translateY("+Math.max(0,e.touches[0].clientY-startY)+"px)". In touchend: if dragged > 40% of sheet height, closeSheet(), else reset transform.
- 4Change the animation curveEdit cubic-bezier(0.32,0.72,0,1) on the .sheet transition to change the slide physics. Use ease-out for a simpler deceleration. Increase the 0.35s duration to 0.5s for a slower, more dramatic slide.
- 5Add more filter groupsDuplicate a .filter-group div inside sc-filter. Each group has a .filter-label and a .filter-chips row. The toggleChip() function manages active state within the parent .filter-chips container, so each group selects independently.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component using useState for open state and content type, or "Tailwind" for a React + Tailwind CSS version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
CSS transition timing cubic-bezier(x1,y1,x2,x2) defines a Bezier curve between (0,0) and (1,1). The four values are the two control point coordinates. cubic-bezier(0.32, 0.72, 0, 1) places the first control point at (0.32, 0.72) — far right, high up — creating a very fast initial movement. The second control point (0, 1) — far left, at the top — creates a slow final approach. The result feels like a spring: fast start, gradual settle. This approximates the iOS UISheetPresentationController animation in CSS.
Add to the .sheet element: let startY = 0; sheet.addEventListener("touchstart", e => { startY = e.touches[0].clientY; }); sheet.addEventListener("touchmove", e => { const dy = e.touches[0].clientY - startY; if (dy > 0) { sheet.style.transition = "none"; sheet.style.transform = "translateY("+dy+"px)"; } }); sheet.addEventListener("touchend", e => { const dy = e.changedTouches[0].clientY - startY; sheet.style.transition = ""; if (dy > sheet.offsetHeight * 0.4) closeSheet(); else sheet.style.transform = ""; });
Add overflow-y: auto; max-height: 80vh to .sheet. Add overscroll-behavior: contain to prevent the scroll from propagating to the body behind the sheet (which would scroll the page). For a snap-to-size sheet that grows with content up to 80vh: set max-height: 80vh and let the content push the sheet height naturally. For a fixed-height sheet with internal scroll, set height: 60vh; overflow-y: auto.
Click "JSX" to download. Use useState(null) for the open content type — null means closed. Conditionally render the backdrop and sheet based on the state. Pass the content type as a prop to the Sheet component to control which content block renders. Add useEffect to manage the ESC key listener: attach when open state is not null, detach in the cleanup return. Use a portal (ReactDOM.createPortal) to render the sheet at document.body level to avoid z-index stacking context issues.