Bottom sheet
Category: Overlays · Tags: bottom-sheet, sheet, mobile, dialog, actions
Shared album: Lake trip, 48 photos
Share album
How to use it
Use on small screens for a short list of actions or choices tied to the current screen, such as sharing or sorting options. Do not use it for long forms or content people need to compare with the page, and on wide screens prefer a menu or a popover. It is non-modal and stays inside its container; if you move it to cover the whole viewport, switch to a native modal <dialog>. After copying, rename the panel id and wire each option button to its action.
Accessibility
The sheet is a role="dialog" region named by its heading through aria-labelledby; the opener is a native <button> with aria-expanded and aria-controls. Opening moves focus to the first option, Escape or Cancel closes the sheet and returns focus to the opener, it also closes when focus moves outside it, and every control is a native button with a 2px focus outline; the slide-up animation is turned off for people who ask for reduced motion. Sheet text is #0f172a on #ffffff (17.85:1), option rows #0f172a on #f1f5f9 when hovered (16.30:1) and the status line #334155 on #f8fafc (9.90:1).
Source (copy-ready)
<div class="dcl-bottom-sheet">
<p class="dcl-bottom-sheet__intro">Shared album: Lake trip, 48 photos</p>
<button type="button" class="dcl-bottom-sheet__open" aria-haspopup="dialog" aria-expanded="false" aria-controls="dcl-bottom-sheet-panel">Share album</button>
<p class="dcl-bottom-sheet__status" role="status"></p>
<div class="dcl-bottom-sheet__panel" id="dcl-bottom-sheet-panel" role="dialog" aria-labelledby="dcl-bottom-sheet-title" hidden>
<span class="dcl-bottom-sheet__handle" aria-hidden="true"></span>
<h2 class="dcl-bottom-sheet__title" id="dcl-bottom-sheet-title">Share album</h2>
<ul class="dcl-bottom-sheet__list">
<li><button type="button" class="dcl-bottom-sheet__option">Copy link</button></li>
<li><button type="button" class="dcl-bottom-sheet__option">Send by email</button></li>
<li><button type="button" class="dcl-bottom-sheet__option">Invite people</button></li>
</ul>
<button type="button" class="dcl-bottom-sheet__cancel">Cancel</button>
</div>
</div>
<style>
.dcl-bottom-sheet { position: relative; overflow: hidden; max-width: 22rem; min-height: 19rem; box-sizing: border-box; padding: 1rem; border: 1px solid #cbd5e1; border-radius: 12px; font: inherit; color: #0f172a; background-color: #f8fafc; }
.dcl-bottom-sheet__intro { margin: 0 0 0.75rem; color: #0f172a; background-color: #f8fafc; }
.dcl-bottom-sheet__open { font: inherit; font-weight: 600; padding: 0.55rem 1.1rem; border: 1px solid #1d4ed8; border-radius: 6px; cursor: pointer; color: #ffffff; background-color: #1d4ed8; }
.dcl-bottom-sheet__open:hover { color: #ffffff; background-color: #1e40af; }
.dcl-bottom-sheet__status { min-height: 1.5em; margin: 0.75rem 0 0; color: #334155; background-color: #f8fafc; }
.dcl-bottom-sheet__panel { position: absolute; left: 0; right: 0; bottom: 0; z-index: 10; display: block; padding: 0.5rem 1rem 1rem; border-top: 1px solid #cbd5e1; border-radius: 14px 14px 0 0; box-shadow: 0 -6px 18px #cbd5e1; color: #0f172a; background-color: #ffffff; animation: dcl-bottom-sheet-up 220ms ease-out; }
.dcl-bottom-sheet__panel[hidden] { display: none; }
.dcl-bottom-sheet__handle { display: block; width: 2.5rem; height: 0.3rem; margin: 0 auto 0.5rem; border-radius: 999px; background-color: #94a3b8; }
.dcl-bottom-sheet__title { margin: 0 0 0.5rem; font-size: 1.05rem; color: #0f172a; background-color: #ffffff; }
.dcl-bottom-sheet__list { margin: 0 0 0.5rem; padding: 0; list-style: none; }
.dcl-bottom-sheet__option, .dcl-bottom-sheet__cancel { display: block; width: 100%; min-height: 2.75rem; padding: 0.6rem 0.75rem; border: none; border-radius: 8px; text-align: left; font: inherit; cursor: pointer; color: #0f172a; background-color: #ffffff; }
.dcl-bottom-sheet__option:hover, .dcl-bottom-sheet__cancel:hover { color: #0f172a; background-color: #f1f5f9; }
.dcl-bottom-sheet__cancel { text-align: center; font-weight: 600; border: 1px solid #94a3b8; }
.dcl-bottom-sheet__open:focus-visible, .dcl-bottom-sheet__option:focus-visible, .dcl-bottom-sheet__cancel:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 2px; }
@keyframes dcl-bottom-sheet-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { .dcl-bottom-sheet__panel { animation: none; } }
</style>
<script>
(function () {
document.querySelectorAll(".dcl-bottom-sheet").forEach(function (root) {
if (root.dataset.dclBound) { return; }
root.dataset.dclBound = "true";
var opener = root.querySelector(".dcl-bottom-sheet__open");
var panel = root.querySelector(".dcl-bottom-sheet__panel");
if (panel && opener) {
panel.addEventListener("focusout", function (event) {
if (event.relatedTarget === opener) { return; }
if (!panel.hidden && event.relatedTarget && !panel.contains(event.relatedTarget)) { panel.hidden = true; opener.setAttribute("aria-expanded", "false"); }
});
}
var status = root.querySelector(".dcl-bottom-sheet__status");
if (!opener || !panel) { return; }
var options = panel.querySelectorAll(".dcl-bottom-sheet__option");
var cancel = panel.querySelector(".dcl-bottom-sheet__cancel");
function open() {
panel.hidden = false;
opener.setAttribute("aria-expanded", "true");
var first = options[0] || cancel;
if (first) { first.focus(); }
}
function close() {
if (panel.hidden) { return; }
panel.hidden = true;
opener.setAttribute("aria-expanded", "false");
opener.focus();
}
opener.addEventListener("click", function () { if (panel.hidden) { open(); } else { close(); } });
if (cancel) { cancel.addEventListener("click", close); }
options.forEach(function (option) {
option.addEventListener("click", function () {
if (status) { status.textContent = "Chosen: " + option.textContent + "."; }
close();
});
});
panel.addEventListener("keydown", function (event) {
if (event.key === "Escape") { close(); }
});
});
})();
</script>
Was this useful? Send feedback.