Bottom Sheet — Free HTML CSS JS Mobile Snippet

Bottom Sheet · Modals · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Slide-up via translateY(100%)→translateY(0) with cubic-bezier(0.32,0.72,0,1) spring curve
Backdrop: rgba fade from transparent to 0.4 black in sync with sheet open
Drag handle: 40×4px pill visual indicator at sheet top
Two content variants: share action grid and filter chip groups
Share actions: 4-column icon grid with Clipboard API copy link feedback
Filter chips: single-select within group via toggleChip() active class management
ESC keydown close: listener added on open, removed on close (no memory leak)
Max-width: 600px centred on desktop, full-width on mobile
ESC close, backdrop click close, button close — three dismiss patterns

About this UI Snippet

Bottom Sheet — Slide-Up Mobile Panel, Drag Handle, Backdrop & Share/Filter Content Variants

Screenshot of the Bottom Sheet snippet rendered live

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:

text
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

  1. 1
    Click 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.
  2. 2
    Add 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.
  3. 3
    Implement 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.
  4. 4
    Change 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.
  5. 5
    Add 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.
  6. 6
    Export 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

Mobile share sheets for social and copy-link actions
The share action grid (Twitter, Facebook, WhatsApp, copy link) is the universal mobile share pattern. Trigger from a share icon in a header or card. Wire each action button to the Web Share API (navigator.share()) if available, falling back to individual platform share URLs.
Filter and sort panels for listing and search result pages
The filter chip variant provides a mobile-optimised filter panel that slides up without leaving the results page. Trigger from a "Filter" button in the search bar. Apply filter state on the "Apply filters" button click and dismiss the sheet.
Action menus and context option sheets
Show a bottom sheet instead of a dropdown menu for mobile action menus — long-press on a list item, tap the three-dot menu icon, or tap a card footer action button. The sheet provides more space for action labels than a compact dropdown.
Settings and preferences panels on mobile web apps
Settings panels, notification preferences, and account actions translate naturally to a bottom sheet on mobile. Slide up from a "Settings" bottom nav tab and present a scrollable settings list inside the sheet.
Study the cubic-bezier spring animation and sheet pattern
The cubic-bezier(0.32,0.72,0,1) timing function is the iOS sheet physics approximation in CSS. Studying how the curve value creates the spring deceleration teaches the cubic-bezier coordinate system — values beyond [0,1] in the control points create overshoot effects.
Checkout and payment option sheets in e-commerce
Present payment method selection, delivery slot picker, or address chooser as a bottom sheet during checkout. The sheet stays in context — users see their cart or product behind the backdrop — reducing the mental load of navigating to a new page for each choice.

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.