Source Code

<div class="as-demo">
  <button type="button" class="as-open" id="asOpen">Show actions</button>

  <div class="as-root" id="asRoot" aria-hidden="true">
    <div class="as-backdrop" id="asBackdrop"></div>
    <div class="as-sheet" id="asSheet" role="dialog" aria-modal="true" aria-label="Actions">
      <div class="as-grab" aria-hidden="true"></div>
      <p class="as-title">Photo options</p>
      <div class="as-group">
        <button class="as-item">📷 Take photo</button>
        <button class="as-item">🖼 Choose from library</button>
        <button class="as-item">🔗 Copy link</button>
        <button class="as-item as-danger">🗑 Delete photo</button>
      </div>
      <button class="as-item as-cancel" id="asCancel">Cancel</button>
    </div>
  </div>
</div>

Action Sheet — Free HTML CSS JS iOS Bottom Sheet Snippet

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

What's included

Features

iOS slide-up
Bottom-anchored sheet with an ease-out curve.
Grouped options
Rounded group with a detached Cancel.
Destructive styling
Danger actions tinted red.
Swipe-to-dismiss
Drag down, decided by distance and velocity.
Snap-back
A short drag returns the sheet to open.
Full dismissal
Cancel, backdrop, and Escape.
Modal focus
Focus stored and restored on close.
Safe-area padding
Clears the home indicator on notched phones.

About this UI Snippet

Action Sheet — iOS-Style Slide-Up Options With Swipe Dismiss

Screenshot of the Action Sheet snippet rendered live

The action sheet is the iOS-style contextual menu that slides up from the bottom of the screen with a grouped list of options and a separate cancel button — the native way mobile apps present "what do you want to do with this?" choices. This snippet builds it with plain HTML, CSS, and vanilla JavaScript, including the swipe-down-to-dismiss gesture that makes it feel native.

The slide-up transition

The sheet is anchored to the bottom with position: absolute; bottom: 0 and is hidden by translating it fully below the screen (translateY(100%)). Adding an .open class transitions it to translateY(0), sliding it up into view, while the backdrop fades in. The transition uses cubic-bezier(.32, .72, 0, 1) — a decisive ease-out that mimics the iOS sheet feel, fast at first and gently settling. A visibility toggle on the root keeps the whole thing non-interactive when closed.

Grouped options and a separate cancel

The layout follows the platform convention: primary options sit together in one rounded group with hairline separators, and Cancel is a visually detached button below it. Destructive actions like Delete are tinted red. This grouping is purely structural CSS, but it is what makes the sheet read as a familiar action sheet rather than a generic menu — the separation signals that Cancel is distinct from the choices.

Swipe-down to dismiss

The native-feeling part is the drag gesture. On pointerdown the sheet captures the pointer and disables its transition; on pointermove it follows the finger but only downward (Math.max(0, deltaY)), so you can pull it toward dismissal. On release, it decides using both distance and velocity: if you have dragged past 110px or flicked quickly (computed from drag distance over time), it closes; otherwise it snaps back to fully open by restoring the transition. Honoring velocity as well as distance is why a quick flick dismisses even a short drag — exactly how iOS sheets behave. A grab handle at the top hints that the sheet is draggable.

Full dismissal and focus

Beyond the swipe, the sheet closes via the Cancel button, a tap on the dimmed backdrop, and the Escape key. It is marked up as role="dialog" with aria-modal="true", and it stores the previously focused element on open and restores focus to it on close — the correct focus handling for a modal. The root toggles aria-hidden so assistive tech ignores it while closed.

Safe-area aware

The sheet pads its bottom with env(safe-area-inset-bottom) so its content clears the home indicator on notched phones, a detail that separates a real mobile sheet from a desktop-only one.

Customizing it

Change the options and their icons, restyle or recolor the groups, adjust the slide easing and the dismissal thresholds (distance and velocity), or add a title and message. Replace the demo options with real handlers. Pair it with a bottom sheet for taller content or a snackbar undo to confirm a destructive choice.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to work through the pointer-event math yourself — paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the swipe-to-dismiss handler combines drag distance and velocity (drag distance divided by elapsed time) to decide between closing and snapping back, and why the transition is disabled during the drag but restored afterward. The same assistant is useful for optimizing it — asking whether setPointerCapture could be replaced or supplemented to better handle multi-touch edge cases, or whether the velocity threshold needs tuning for slower devices. It's just as good for extending the sheet: ask it to add a second, taller "expanded" snap position like a real bottom sheet, support keyboard arrow-key navigation between the listed actions, or generalize the options list to be data-driven instead of hardcoded markup. 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 an iOS-style "action sheet" in plain HTML, CSS, and JavaScript using the Pointer Events API for the drag gesture — no libraries, no touch-event-only fallback.

Requirements:
- A sheet anchored to the bottom of the viewport with position: fixed/absolute, starting fully off-screen via transform: translateY(100%), and a semi-transparent backdrop behind it that fades in independently.
- Adding an "open" class must transition the sheet to translateY(0) using an ease-out cubic-bezier curve that feels fast-then-settling (not linear), while the backdrop opacity animates in parallel.
- Structure the options as one visually grouped block of buttons with hairline dividers between them, plus a visually separate "Cancel" button below the group, and support a distinct "danger" style for destructive actions like Delete.
- Implement swipe-down-to-dismiss using pointerdown/pointermove/pointerup: on pointerdown, capture the pointer and disable the CSS transition; on pointermove, clamp the vertical delta so the sheet can only be dragged downward (never upward past its resting position) and update the transform directly; on pointerup, decide whether to close based on BOTH how far it was dragged (a distance threshold) AND how fast (drag distance divided by elapsed time, a velocity threshold) — closing if either threshold is exceeded, otherwise re-enabling the transition and snapping back to fully open.
- Close the sheet via the Cancel button, a click on the backdrop, and the Escape key, and mark the sheet with role="dialog" and aria-modal="true", storing the previously focused element on open and restoring focus to it on close.
- Pad the sheet's bottom safe area using env(safe-area-inset-bottom) so it clears the home indicator on notched phones.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA button renders; clicking it slides up an action sheet.
  2. 2
    Review the optionsGrouped choices appear with a separate Cancel below.
  3. 3
    Swipe the sheet downDrag past a threshold or flick to dismiss it.
  4. 4
    Or tap the backdropTapping outside or pressing Escape also closes it.
  5. 5
    Edit the optionsChange the items, icons, and danger styling.
  6. 6
    Tune the gestureAdjust the dismissal distance and velocity.

Real-world uses

Common Use Cases

Mobile option menus
Present choices like a native iOS sheet.
Photo and file actions
Pair with a media upload grid.
Share and export
An alternative to a share modal.
Destructive confirmations
Follow with a snackbar undo.
Taller content
Step up to a bottom sheet when needed.
Gesture demos
A reference for velocity-aware swipe dismiss.
Related: Achievement Unlock Toast
See the Achievement Unlock Toast for a related modals pattern worth pairing with this one.
Related: Sign In / Sign Up Modal with Tabs
See the Sign In / Sign Up Modal with Tabs for a related modals pattern worth pairing with this one.
Related: Onboarding Checklist Modal with Progress Ring
See the Onboarding Checklist Modal with Progress Ring for a related modals pattern worth pairing with this one.
Related: Booking Date Range Picker Modal
See the Booking Date Range Picker Modal for a related modals pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

On release it checks both distance and velocity: if you have dragged the sheet past 110px or flicked it quickly — computed as drag distance over elapsed time — it closes; otherwise it snaps back to fully open. Honoring velocity as well as distance is why a fast flick dismisses even a short drag, matching how native iOS sheets behave.

On pointermove the offset is clamped with Math.max(0, deltaY), so dragging up does nothing and only downward movement translates the sheet. That mirrors the platform behavior where a sheet can be pulled toward dismissal but not lifted above its open position.

The sheet sits below the screen at translateY(100%) and transitions to translateY(0) when opened, using cubic-bezier(.32,.72,0,1) — a decisive ease-out that is fast at first and settles gently, like the native sheet. During a drag the transition is disabled so the sheet tracks the finger, then restored on release for the snap-back or close.

It is marked role="dialog" with aria-modal, stores and restores focus around open and close, toggles aria-hidden on the root, and closes on Escape and backdrop tap as well as the swipe. It also pads the bottom with env(safe-area-inset-bottom) so its content clears the home indicator on notched phones.

Drive the open state from component state and render the sheet conditionally or toggle the class. Implement the drag with pointer handlers writing the transform via a ref so it does not re-render each move, and keep the distance/velocity logic in the pointerup handler. Add Escape and focus restoration in effects with cleanup. In Tailwind, use translate-y utilities and an arbitrary cubic-bezier transition.