Every product eventually needs to interrupt the user — a confirmation before a delete, a settings drawer, a "saved" toast, a command palette for power users. These overlays are deceptively hard to get right: get the animation wrong and it feels janky, forget to trap focus and keyboard users get lost outside the dialog, skip the escape hatch and you've built a trap instead of a modal. Below are 10 free overlay snippets — a confirm dialog, a swipeable action sheet, a Cmd-K command palette, an undo-able snackbar — you can preview live and copy in seconds. Pure HTML, CSS and a few lines of JS, no framework required.
As with every roundup in this series, this isn't just a copy-paste dump. For each snippet you'll find how it actually works under the hood, when to reach for it, and a quick tip for keeping it accessible. By the end you'll know the small toolkit — escape-hatch handling, focus and scroll management, transform-based enter/exit animation, self-cleaning DOM nodes — behind nearly every overlay on the modern web.
Every snippet below is a live, interactive preview — click, type and tab through it right in the article. When you find one you like, hit View / Edit Code to grab the HTML/CSS/JS or export it to React, Vue, Angular or Tailwind.
The golden rule: give every overlay an escape hatch
Overlays have a famous failure mode: they're easy to open and easy to forget to close properly. A modal that only closes via its own "X" button strands keyboard users and mobile users who expect a backdrop tap to work. The snippets below all follow the same three rules instead:
- Three ways out, always — Escape key, backdrop click, and an explicit close action. All of the dialogs and sheets below wire up
keydownfor Escape and comparee.target === overlayon click so only a genuine backdrop tap closes it, not a click that bubbled up from inside. - Open/close as a symmetric pair — every snippet has a matching
open()/close()(or equivalent) that adds a listener on open and removes it on close, so nothing leaks when the overlay is dismissed and reopened repeatedly. - Animate the transform, not the display — sheets and drawers slide in with
translateX/translateY, dialogs and popovers scale in from 0.95, and only the final step flipsdisplay: none. That's what makes them glide instead of pop.
Get this pattern once and you can build almost any overlay without a library. Now the snippets.
1. Modal / Dialog — the overlay every other pattern here builds on
How it works: a fixed-position .overlay and a centred .modal box both sit hidden until a .show class is toggled by openModal()/closeModal(). That single class change drives everything — the overlay fades in via opacity, and the modal itself scales up from transform: scale(0.95) to scale(1) on a short transition, which reads as the dialog "arriving" rather than snapping into place. Closing reverses the same transition before the box is removed from the layout.
When to use it: anything that needs the page behind it to visually recede — settings panels, image previews, forms that don't deserve their own route. Tip: keep the scale transition under 200ms; anything longer and the modal feels like it's fighting the user's click instead of responding to it.
Try it: Modal / Dialog
2. Confirm Dialog — the type-to-confirm delete guard
How it works: the Delete button starts disabled and stays that way until checkPhrase() confirms the text field's value matches a target phrase pulled from the DOM on every keystroke. Committing runs a small fake-progress sequence — the button swaps its label to "Deleting…" then to a checkmark and "Deleted" after a setTimeout — so the destructive action gets its own moment of feedback instead of just vanishing. The backdrop-click handler compares e.target === overlay so clicks inside the dialog never accidentally close it mid-type.
When to use it: irreversible actions — deleting an account, a repo, a production database. A plain "Are you sure?" gets clicked through on autopilot; typing the resource's name forces a half-second of genuine attention. Tip: only reach for type-to-confirm on truly destructive, hard-to-undo actions — using it for routine deletes trains users to type without reading, which defeats the point.
Try it: Confirm Dialog
3. Side Drawer — off-canvas panels with spring easing
How it works: a left navigation drawer and a right settings panel share one open(side)/close() pair. Each panel starts off-screen at translateX(-100%) or translateX(100%) and slides to translateX(0) using cubic-bezier(0.32, 0.72, 0, 1) — the same overshoot-free spring curve iOS uses for its own sheets, which is what makes the slide feel weighted instead of linear. Opening a drawer also attaches an Escape keydown listener that close() removes again, so held-open state never leaks a stray handler.
When to use it: primary navigation on mobile, or secondary panels (filters, settings, cart) you don't want competing with the main content for screen space. Tip: lock body scroll while a drawer is open — a background page that keeps scrolling behind a fixed panel is one of the most common overlay bugs, and it's a one-line fix (overflow: hidden on open, removed on close).
Try it: Side Drawer
4. Bottom Sheet — the mobile-native pattern for the web
How it works: the same spring easing as the drawer, applied vertically — the panel rests at translateY(100%) and rises to 0 when openSheet(type) runs. That one function does double duty: it also swaps which content block is visible, toggling between a share grid and a filter-chip list by flipping their display property, so one sheet shell serves two different jobs. The share variant wires a "Copy link" button to the Clipboard API and flips its label to "Copied!" briefly for confirmation.
When to use it: mobile-width layouts where a centred modal would look stranded — share sheets, filters, quick actions. Rising from the bottom keeps the action within thumb reach. Tip: cap the sheet's height and let its content scroll internally; a bottom sheet that grows past the viewport just becomes a worse modal.
Try it: Bottom Sheet
5. Action Sheet — swipe-to-dismiss with real velocity
How it works: an iOS-style grouped-options sheet, but with genuine swipe physics instead of a fake drag handle. Pointer Events (pointerdown/pointermove/pointerup) track the vertical drag distance, and on release the sheet computes velocity as dy / (Date.now() - t0) — distance over time. If you've dragged past 110px or flicked fast regardless of distance, it dismisses; otherwise it snaps back to rest. That velocity check is what separates "feels responsive" from "feels sticky."
When to use it: grouped destructive or contextual actions on touch devices — share targets, "more options" menus. Tip: Pointer Events (not separate touch/mouse handlers) is the detail worth stealing — one set of listeners handles mouse, touch and pen input identically, so you're not duplicating drag logic per input type.
Try it: Action Sheet
6. Popover — one system, three anchored positions
How it works: every popover on the page is tracked in a single openPopovers Set, so one global click-outside listener — checking !e.target.closest('.pop-wrap') — and one Escape handler can close all of them at once, instead of each popover needing its own outside-click logic. Visually there are three CSS-positioned variants (.bottom, .top, .right), each with a matching rotated-square arrow element pointing back at its trigger, and each entrance is a scale(0.96 → 1) plus opacity fade rather than a hard cut.
When to use it: contextual info or a short menu tied to a specific trigger — user menus, info icons, inline pickers. Unlike a modal, a popover doesn't dim the page, so it reads as "extra detail" rather than "interruption." Tip: the shared openPopovers Set matters most once you have more than one popover on a page — without it, opening a second popover while the first is still open is a bug users find immediately.
Try it: Popover
7. Toast Notification — self-cleaning DOM nodes
How it works: each call creates a fresh toast element with document.createElement rather than reusing a hidden template, slides it in from translateX(110%) with a slideIn keyframe, and after 2800ms triggers a matching slideOut. The node removes itself from the DOM on the animationend event rather than a hardcoded timeout — so the element only ever disappears once its exit animation has actually finished playing, with no orphaned nodes accumulating if toasts fire in quick succession.
When to use it: one-off, non-critical confirmations — "Saved", "Copied", "Message sent." Toasts should never block interaction or require a click to dismiss. Tip: tying removal to animationend instead of setTimeout is the detail that keeps this bulletproof — a timeout-based version can remove the node mid-animation if timing drifts, causing a visible jump.
Try it: Toast Notification
8. Toast Queue — the production-grade multi-toast manager
How it works: where the single toast above handles one notification, this one handles a stream of them. A queue is capped at MAX = 5 visible toasts — pushing a sixth force-dismisses the oldest rather than letting the stack grow unbounded. Each toast carries its own CSS progress bar animating width: 100% → 0% over its lifetime; hovering sets animationPlayState: paused so a toast you're reading doesn't vanish mid-read, and leaving resumes it with a shortened remaining timer rather than restarting from full.
When to use it: any app where multiple notifications can fire close together — bulk actions, background sync status, multi-user activity feeds. A single-toast system either overwrites or overlaps in these cases; a queue doesn't. Tip: the pause-on-hover behaviour is worth copying even into simpler toast systems — nothing erodes trust in a notification faster than it disappearing while the user's eyes are still on it.
Try it: Toast Queue
9. Snackbar with Undo — deleting without really deleting
How it works: clicking delete removes the row from view immediately, but not for good — its node, parent and next sibling are stashed in a pending object first. If Undo is clicked, insertBefore puts the exact same element back in its exact original position, siblings and all, so nothing reflows unexpectedly. If four seconds pass with no undo, a finalize() timeout commits the delete for real. The countdown bar resets on repeat deletes using a void bar.offsetWidth reflow trick — forcing the browser to acknowledge the reset before re-triggering the animation, since just changing the class again wouldn't restart a CSS animation already at 0%.
When to use it: any delete that's cheap to reverse — archiving an email, removing a list item, dismissing a card. Deferred deletion turns a scary irreversible action into a safe, reversible one, which means you can skip the confirm dialog entirely. Tip: this pairs naturally with the confirm dialog above — reserve the type-to-confirm pattern for truly permanent actions, and use undo-snackbars for everything reversible instead of interrupting the user twice.
Try it: Snackbar with Undo
10. Command Palette — the Cmd-K power-user shortcut
How it works: a static COMMANDS array holds every action grouped by category; typing runs filter(query), which does a simple substring match against each command's name and description and re-groups the survivors. render() then rebuilds the visible list from that filtered set. Arrow keys move an activeIdx pointer up and down the flattened list, and the active item calls scrollIntoView({ block: 'nearest' }) so keyboard navigation never scrolls the list further than it has to — a detail most from-scratch implementations get wrong on the first try.
When to use it: any app with more actions than fit in a toolbar — dashboards, editors, admin panels. A command palette turns "where is that setting" into "type three letters." Tip: group results by category even before the user types anything — an unfiltered palette that shows structure (Navigation, Actions, Settings) teaches users what's searchable, which is exactly what gets them to use it a second time.
Try it: Command Palette
The techniques you just collected
Ten overlays, five reusable ideas — learn these and you can build nearly any dialog, sheet or notification without a library:
- Three escape hatches, every time — Escape key, backdrop click compared with
e.target === overlay, and an explicit close control. Skip any one and you've built a trap, not a dialog. - Symmetric open/close functions — attach a listener (Escape, drag tracking) when something opens, remove it when it closes. Powers the drawer, the popover set, and the action sheet's swipe tracking without leaking handlers.
- Transform-based motion, display last — slide with
translateX/translateY, scale in from 0.95–0.96, and only toggledisplay: noneonce the exit transition has actually finished. - Self-cleaning ephemeral nodes — toasts create their own element and remove it on
animationendrather than a timer, so the DOM never fills with orphaned nodes during a burst of notifications. - Defer the destructive part — the undo-snackbar's stash-then-
finalize()pattern and the confirm dialog's typed-phrase gate are two different answers to the same question: how do you make an irreversible action forgiving?
Final Thought
Here's a challenge for your next project: before you reach for a modal library, try wiring up the escape-hatch pattern from these ten snippets yourself. It's maybe 15 lines of JavaScript per overlay, you'll know exactly why it closes when it does, and — the part most teams skip — you'll actually test what happens when a user hits Escape mid-drag or clicks the backdrop twice fast, instead of trusting a library to have handled it.
Every snippet here is part of the free modals & overlays collection at FWD Tools, alongside a fullscreen menu, an onboarding tour, a cookie consent manager and a product quick-view that didn't make this list — all editable in the browser and exportable to React, Vue, Angular or Tailwind. Earlier roundups in this series cover buttons, hover cards, text effects, loading states and form inputs.


