Skip to content
animorio
IT
All components

The bottom sheet

Dialogs, Physics, TransitionsGSAPAdvancedCompressed weight: Demo 7.3 KBGSAP 28 KBFreeDownload the zip

A sheet that rises from the bottom of the screen and stops halfway: you drag it with a finger, past the top it stretches like rubber and, let go, it chooses where to stop from where it is and how fast it was going; thrown down, it closes.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The bottom sheet</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/plus-jakarta-sans@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <button class="open" type="button" aria-haspopup="dialog" data-open data-autoclic>
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 15V4.5M7.5 8.5 12 4l4.5 4.5M5 12.5v5.5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-5.5"/></svg>
      <span>Share</span>
    </button>
  </main>

  <!-- the sheet: a real dialog (focus kept inside, Escape, the page behind out of reach) whose panel rises from the
       bottom. It can be dragged: it stops at half height or at full height, stretches a little past the top and
       closes when thrown down. The script moves it with a spring -->
  <dialog class="sheet" aria-labelledby="sheet-title" data-sheet>
    <div class="sheet__shade" data-shade></div>
    <div class="sheet__panel" tabindex="-1" data-panel>
      <button class="sheet__grab" type="button" aria-label="Show more" data-grab><span></span></button>
      <div class="sheet__head">
        <div>
          <h2 class="sheet__title" id="sheet-title">Share this stay</h2>
          <p class="sheet__sub">Two nights in the hills, from 14 November</p>
        </div>
        <button class="sheet__close" type="button" aria-label="Close" data-close>
          <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7l10 10M17 7 7 17"/></svg>
        </button>
      </div>
      <div class="sheet__apps" data-half>
        <button class="app" type="button" data-copy>
          <span class="app__ring"><svg viewBox="0 0 24 24" aria-hidden="true" data-copy-icon><path d="M10 14a4.5 4.5 0 0 0 6.4 0l3-3a4.5 4.5 0 0 0-6.4-6.4l-1.2 1.2M14 10a4.5 4.5 0 0 0-6.4 0l-3 3a4.5 4.5 0 0 0 6.4 6.4l1.2-1.2"/></svg></span>
          <span class="app__name" data-copy-label>Copy link</span>
        </button>
        <button class="app" type="button" data-action>
          <span class="app__ring"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 18.5V7a2.5 2.5 0 0 1 2.5-2.5h10A2.5 2.5 0 0 1 19.5 7v7a2.5 2.5 0 0 1-2.5 2.5H8.5z"/></svg></span>
          <span class="app__name">Message</span>
        </button>
        <button class="app" type="button" data-action>
          <span class="app__ring"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6.5h16v11H4zM4.5 7l7.5 6 7.5-6"/></svg></span>
          <span class="app__name">E-mail</span>
        </button>
        <button class="app" type="button" data-action>
          <span class="app__ring"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h5v5H5zM14 5h5v5h-5zM5 14h5v5H5zM14 14h5v5h-5z"/></svg></span>
          <span class="app__name">More</span>
        </button>
      </div>
      <ul class="sheet__rows">
        <li><button class="row" type="button" data-action><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4.5h10v15l-5-3.5-5 3.5z"/></svg>Add to a list</button></li>
        <li><button class="row" type="button" data-action><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 3.5h7l4 4v13H7zM14 3.5v4h4M9.5 13h6M9.5 16.5h6"/></svg>Save as PDF</button></li>
        <li><button class="row" type="button" data-action><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7.5 9V4h9v5M7.5 17H5v-6.5a1.5 1.5 0 0 1 1.5-1.5h11a1.5 1.5 0 0 1 1.5 1.5V17h-2.5M7.5 14h9v6h-9z"/></svg>Print</button></li>
        <li><button class="row" type="button" data-action><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 4.5h6v6h-6zM13.5 4.5h6v6h-6zM4.5 13.5h6v6h-6zM14 14h2.5v2.5H14zM17 17h2.5v2.5H17z"/></svg>Show a QR code</button></li>
        <li><button class="row row--quiet" type="button" data-action><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5.5 20V4.5M5.5 5h11l-2 4 2 4h-11"/></svg>Report a problem</button></li>
      </ul>
    </div>
  </dialog>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

25 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The bottom sheet” from Animorio and fit it into my site

What it does

On a white background a black Share button. Pressing it the page darkens, the button sinks back a little and a white sheet with round top corners rises from the bottom and stops halfway: a grey grip, Share this stay with Two nights in the hills, from 14 November under it and a little cross, then four round buttons popping up one after the other (Copy link, Message, E-mail, More) and the start of the first row. The sheet can be dragged from anywhere: it follows the finger, past the top it stretches less and less, like rubber, and the grip widens; let go, it goes to half height or to the top depending on where it is and how fast it was going, and thrown down it closes. At the top the rows Add to a list, Save as PDF, Print, Show a QR code and, in red, Report a problem show too. The grip is a button that shows more or less; Escape, the cross or a tap on the dark page close the sheet, which goes down as the page lightens. Copy link copies the address and for a moment turns blue, with a tick and the word Copied; the other buttons close the sheet.

How it works

A real dialog

The sheet sits in a dialog opened with showModal(): the focus stays inside (the script takes it to the panel, from where Tab reaches the buttons), Escape closes it and the page behind cannot be reached. The dialog fills the screen and is see-through; inside it are the dark shade and the panel. The cancel event is stopped and the sheet closes itself once it has gone down.

One spring

The panel has a single position, y, counted from the top, and a spring with the stiffness and damping of SPRING takes it to the chosen spot, worked out on GSAP’s ticker in steps of 4 milliseconds. On every frame the shade and the sinking button (SINK) follow how much of the sheet shows.

Dragging

Pressing on the panel stops it under the finger; after 6 pixels of movement it is a drag and the pointer is captured. Past the top the panel goes on as STRETCH · (1 − 1/(0.55 · past/STRETCH + 1)), less and less and never more than STRETCH; and so as not to leave a gap under it, it carries on 160 pixels below the edge of the screen.

Where to stop

Let go, the speed comes from the last 90 milliseconds of the drag: the script looks at where the panel would be after THROW seconds at that speed and picks the nearest stop among top, half and closed. The spring starts with the throw’s speed, so the gesture carries on into the landing.

The library’s preview

The preview clicks a single mark, data-autoclic, which the script moves: first on Share, with the sheet open on the grip (which takes it to the top), at the top on the cross. So the preview opens, expands and closes it by itself, over and over.

Values to change

NameFileDefaultWhat it changes
SPRINGscript.js[420, 0.86]The panel’s spring: how stiff it is and how much it is held back (1 = no bounce).
THROWscript.js0.18How far ahead a throw counts when choosing where to stop, in seconds of its speed.
STRETCHscript.js110The most the panel can be stretched past the top, in pixels.
SHADEscript.js1How dark the page gets behind the panel, from 0 to 1.
SINKscript.js0.06How much the Share button sinks back while the sheet is open.
PEEKscript.js30How much of what follows the round buttons shows at half height, in pixels.

Try them in the Playground

When to use it

For sharing, for a search’s filters, for an item’s options, for a basket or a summary: above all on phones, where the thumb easily reaches the bottom of the screen.

On phones

On a phone the sheet takes the full width and leaves room for the iPhone’s bottom bar; a finger drags it from anywhere, and two quick taps do not zoom the page. If the content is taller than the screen the sheet scrolls with the finger and is dragged only by the grip and the title.

With reduced motion

The sheet appears and disappears with a quick fade instead of rising and sinking, goes from half height to the top without moving and the round buttons do not pop; dragged, it still follows the finger.

Pitfalls

  • The native dialog opened with showModal keeps the focus inside, closes with Escape and puts the page behind out of reach, but it closes at once: the script stops the cancel event and closes the dialog itself when the sheet has gone down.
  • Stretched past the top, the sheet would leave a gap under it: that is why it carries on 160 pixels below the edge of the screen, in its own white.
  • A drag that ends on a button must not press it: after 6 pixels the script captures the pointer and for a moment stops the click that follows.
  • The first focus goes to the panel (tabindex −1, no outline) and the script puts it there: on the whole dialog the outline would go round the entire screen, a button focused by a script would show its outline in Safari even after a tap, and the autofocus attribute inside a page framed by another site would be blocked.
  • The panel has touch-action: none, so a finger drags it instead of scrolling the page; if the content is taller than the screen it scrolls again, and it is dragged from the top only.
  • The dialog must clip its content with overflow: clip, not hidden: a hidden box can still be scrolled, and on opening the dialog Safari would scroll it to show the first button, which at that moment is still below the screen.
  • When the sheet closes the focus goes back to Share, but after a tap Safari would show its outline: the script remembers whether the last gesture came from a finger, the mouse or the keys, and the outline shows only with the keys.

Libraries

GSAP 3.15.0, Plus Jakarta Sans (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 7.3 KB; the libraries: GSAP 28 KB.

Testing

Tested on 3 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: Done

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components