
The slide-to-confirm button
Buttons, Physics, Transitions
NewFree
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.
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>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
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 siteThe 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.
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.
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.
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 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.
| Name | File | Default | What it changes |
|---|---|---|---|
SPRING | script.js | [420, 0.86] | The panel’s spring: how stiff it is and how much it is held back (1 = no bounce). |
THROW | script.js | 0.18 | How far ahead a throw counts when choosing where to stop, in seconds of its speed. |
STRETCH | script.js | 110 | The most the panel can be stretched past the top, in pixels. |
SHADE | script.js | 1 | How dark the page gets behind the panel, from 0 to 1. |
SINK | script.js | 0.06 | How much the Share button sinks back while the sheet is open. |
PEEK | script.js | 30 | How much of what follows the round buttons shows at half height, in pixels. |
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 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.
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.
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.
Tested on 3 October 2026
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.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.