UI Components Medium
Sheet / Drawer
Slide-in sheet panels from all four edges — bottom sheet (mobile-first), right sidebar drawer, left nav drawer, and top notification drawer.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #050910;
--card: #0d1117;
--border: rgba(255, 255, 255, 0.08);
--text: #f2f6ff;
--muted: #475569;
--accent: #38bdf8;
--danger: #f87171;
--success: #4ade80;
--warning: #fbbf24;
--info: #60a5fa;
--radius: 12px;
--transition: 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* ── Demo center ── */
.demo-center {
text-align: center;
max-width: 420px;
padding: 2rem;
}
.demo-title {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.demo-subtitle {
color: var(--muted);
margin-bottom: 2rem;
font-size: 0.9rem;
}
.trigger-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.trigger-btn {
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: center;
background: var(--card);
border: 1px solid var(--border);
color: var(--text);
padding: 0.75rem 1rem;
border-radius: var(--radius);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: border-color var(--transition), background var(--transition);
}
.trigger-btn:hover {
border-color: var(--accent);
background: rgba(56, 189, 248, 0.06);
}
.trigger-icon {
font-size: 1rem;
color: var(--accent);
}
/* ── Backdrop ── */
.backdrop {
position: fixed;
inset: 0;
background: rgba(5, 9, 16, 0.7);
backdrop-filter: blur(2px);
opacity: 0;
pointer-events: none;
transition: opacity var(--transition);
z-index: 100;
}
.backdrop.active {
opacity: 1;
pointer-events: all;
}
/* ── Drawers base ── */
.drawer {
position: fixed;
background: var(--card);
z-index: 200;
display: flex;
flex-direction: column;
transition: transform var(--transition);
border-color: var(--border);
}
.drawer__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.125rem 1.25rem;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.drawer__title {
font-size: 1rem;
font-weight: 600;
}
.drawer__logo {
font-size: 1.1rem;
font-weight: 700;
color: var(--accent);
}
.drawer__close {
background: none;
border: none;
color: var(--muted);
font-size: 1rem;
cursor: pointer;
width: 28px;
height: 28px;
border-radius: 6px;
display: grid;
place-items: center;
transition: color 0.2s, background 0.2s;
}
.drawer__close:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.06);
}
.drawer__content {
flex: 1;
overflow-y: auto;
padding: 1.25rem;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.drawer__content--horizontal {
display: flex;
gap: 1rem;
overflow-x: auto;
overflow-y: hidden;
align-items: flex-start;
}
.drawer__desc {
color: var(--muted);
font-size: 0.875rem;
line-height: 1.6;
margin-bottom: 1.25rem;
}
.drawer__footer {
padding: 1rem 1.25rem;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
/* ── Bottom sheet ── */
.drawer--bottom {
bottom: 0;
left: 0;
right: 0;
border-radius: var(--radius) var(--radius) 0 0;
border-top: 1px solid var(--border);
max-height: 80vh;
transform: translateY(100%);
}
.drawer--bottom.open {
transform: translateY(0);
}
.drawer__handle-bar {
width: 36px;
height: 4px;
background: rgba(255, 255, 255, 0.18);
border-radius: 2px;
margin: 0.625rem auto 0;
flex-shrink: 0;
cursor: grab;
}
/* ── Right drawer ── */
.drawer--right {
top: 0;
right: 0;
bottom: 0;
width: 320px;
border-left: 1px solid var(--border);
transform: translateX(100%);
}
.drawer--right.open {
transform: translateX(0);
}
/* ── Left drawer ── */
.drawer--left {
top: 0;
left: 0;
bottom: 0;
width: 260px;
border-right: 1px solid var(--border);
transform: translateX(-100%);
}
.drawer--left.open {
transform: translateX(0);
}
/* ── Top drawer ── */
.drawer--top {
top: 0;
left: 0;
right: 0;
border-bottom: 1px solid var(--border);
transform: translateY(-100%);
}
.drawer--top.open {
transform: translateY(0);
}
/* ── Drawer list ── */
.drawer__list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.drawer__list-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s;
}
.drawer__list-item:hover {
background: rgba(255, 255, 255, 0.04);
}
.drawer__list-item .danger {
color: var(--danger);
}
.drawer__list-icon {
font-size: 1.1rem;
width: 24px;
text-align: center;
}
/* ── Settings controls ── */
.drawer__section {
margin-bottom: 1.5rem;
}
.drawer__label {
display: block;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 0.625rem;
}
.drawer__toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0;
font-size: 0.875rem;
}
.toggle {
width: 40px;
height: 22px;
border-radius: 11px;
background: rgba(255, 255, 255, 0.1);
cursor: pointer;
position: relative;
transition: background 0.2s;
flex-shrink: 0;
}
.toggle::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--muted);
transition: transform 0.2s, background 0.2s;
}
.toggle.active {
background: rgba(56, 189, 248, 0.25);
}
.toggle.active::after {
transform: translateX(18px);
background: var(--accent);
}
.drawer__select {
width: 100%;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
color: var(--text);
padding: 0.5rem 0.75rem;
border-radius: 8px;
font-size: 0.875rem;
cursor: pointer;
}
/* ── Nav items ── */
.drawer__nav {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.drawer__nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 0.75rem;
border-radius: 8px;
color: var(--muted);
text-decoration: none;
font-size: 0.9rem;
transition: background 0.2s, color 0.2s;
}
.drawer__nav-item:hover,
.drawer__nav-item.active {
background: rgba(56, 189, 248, 0.08);
color: var(--text);
}
.drawer__nav-item.active {
color: var(--accent);
}
.drawer__nav-icon {
font-size: 1rem;
width: 20px;
text-align: center;
}
.drawer__nav-divider {
height: 1px;
background: var(--border);
margin: 0.5rem 0;
}
/* ── Notifications ── */
.notification {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.875rem;
border-radius: 8px;
border: 1px solid var(--border);
min-width: 220px;
background: rgba(255, 255, 255, 0.02);
}
.notification p {
font-size: 0.8rem;
color: var(--muted);
margin-top: 0.2rem;
}
.notification strong {
font-size: 0.875rem;
}
.notification__icon {
font-size: 1rem;
flex-shrink: 0;
width: 28px;
height: 28px;
border-radius: 50%;
display: grid;
place-items: center;
}
.notification--success .notification__icon {
background: rgba(74, 222, 128, 0.12);
color: var(--success);
}
.notification--warning .notification__icon {
background: rgba(251, 191, 36, 0.12);
color: var(--warning);
}
.notification--info .notification__icon {
background: rgba(96, 165, 250, 0.12);
color: var(--info);
}
/* ── Buttons ── */
.btn-primary {
display: block;
width: 100%;
padding: 0.625rem;
background: var(--accent);
color: #050910;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.btn-primary:hover {
opacity: 0.85;
}(function () {
"use strict";
const backdrop = document.getElementById("backdrop");
let activeDrawer = null;
// ── Open / Close ────────────────────────────────────────────────────────────
function openDrawer(side) {
if (activeDrawer) closeDrawer(activeDrawer, false);
const el = document.getElementById("drawer-" + side);
if (!el) return;
el.classList.add("open");
backdrop.classList.add("active");
activeDrawer = side;
trapFocus(el);
if (side === "bottom") initDragToDismiss(el);
}
function closeDrawer(side, clearActive) {
const el = document.getElementById("drawer-" + side);
if (!el) return;
el.classList.remove("open");
backdrop.classList.remove("active");
if (clearActive !== false) activeDrawer = null;
releaseFocus();
}
function closeActiveDrawer() {
if (activeDrawer) closeDrawer(activeDrawer);
}
// Expose to inline onclick attributes
window.openDrawer = openDrawer;
window.closeDrawer = closeDrawer;
// ── Escape key ──────────────────────────────────────────────────────────────
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && activeDrawer) closeActiveDrawer();
});
// ── Focus trap ──────────────────────────────────────────────────────────────
const FOCUSABLE =
'a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])';
let prevFocused = null;
function trapFocus(el) {
prevFocused = document.activeElement;
const focusable = Array.from(el.querySelectorAll(FOCUSABLE));
if (focusable.length) focusable[0].focus();
el._trapHandler = function (e) {
if (e.key !== "Tab") return;
const items = Array.from(el.querySelectorAll(FOCUSABLE));
if (!items.length) {
e.preventDefault();
return;
}
const first = items[0];
const last = items[items.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
document.addEventListener("keydown", el._trapHandler);
}
function releaseFocus() {
if (activeDrawer) {
const el = document.getElementById("drawer-" + activeDrawer);
if (el && el._trapHandler) document.removeEventListener("keydown", el._trapHandler);
}
if (prevFocused) prevFocused.focus();
prevFocused = null;
}
// ── Drag-to-dismiss (bottom sheet) ─────────────────────────────────────────
function initDragToDismiss(el) {
const handle = el.querySelector(".drawer__handle-bar");
if (!handle) return;
let startY = 0;
let currentY = 0;
let dragging = false;
const THRESHOLD = 0.4; // 40% of sheet height
function onStart(e) {
dragging = true;
startY = getClientY(e);
el.style.transition = "none";
document.addEventListener("mousemove", onMove);
document.addEventListener("touchmove", onMove, { passive: false });
document.addEventListener("mouseup", onEnd);
document.addEventListener("touchend", onEnd);
}
function onMove(e) {
if (!dragging) return;
e.preventDefault();
currentY = getClientY(e);
const dy = Math.max(0, currentY - startY);
el.style.transform = "translateY(" + dy + "px)";
}
function onEnd() {
dragging = false;
el.style.transition = "";
const dy = Math.max(0, currentY - startY);
if (dy > el.offsetHeight * THRESHOLD) {
el.style.transform = "";
closeDrawer("bottom");
} else {
el.style.transform = "";
}
document.removeEventListener("mousemove", onMove);
document.removeEventListener("touchmove", onMove);
document.removeEventListener("mouseup", onEnd);
document.removeEventListener("touchend", onEnd);
}
function getClientY(e) {
return e.touches ? e.touches[0].clientY : e.clientY;
}
handle.addEventListener("mousedown", onStart);
handle.addEventListener("touchstart", onStart, { passive: true });
}
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sheet / Drawer</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo-center">
<h2 class="demo-title">Sheet / Drawer</h2>
<p class="demo-subtitle">Slide-in panels from any edge</p>
<div class="trigger-grid">
<button class="trigger-btn" onclick="openDrawer('bottom')">
<span class="trigger-icon">↑</span>
Bottom Sheet
</button>
<button class="trigger-btn" onclick="openDrawer('right')">
<span class="trigger-icon">←</span>
Right Drawer
</button>
<button class="trigger-btn" onclick="openDrawer('left')">
<span class="trigger-icon">→</span>
Left Drawer
</button>
<button class="trigger-btn" onclick="openDrawer('top')">
<span class="trigger-icon">↓</span>
Top Drawer
</button>
</div>
</div>
<!-- Backdrop -->
<div class="backdrop" id="backdrop" onclick="closeActiveDrawer()"></div>
<!-- Bottom Sheet -->
<div class="drawer drawer--bottom" id="drawer-bottom" role="dialog" aria-modal="true" aria-label="Bottom sheet">
<div class="drawer__handle-bar"></div>
<div class="drawer__header">
<h3 class="drawer__title">Bottom Sheet</h3>
<button class="drawer__close" onclick="closeDrawer('bottom')" aria-label="Close">✕</button>
</div>
<div class="drawer__content">
<p class="drawer__desc">Mobile-first bottom sheet with drag-to-dismiss. Drag the handle bar down or swipe to close.</p>
<ul class="drawer__list">
<li class="drawer__list-item">
<span class="drawer__list-icon">📁</span>
<span>Save to Files</span>
</li>
<li class="drawer__list-item">
<span class="drawer__list-icon">🔗</span>
<span>Copy Link</span>
</li>
<li class="drawer__list-item">
<span class="drawer__list-icon">✉️</span>
<span>Share via Email</span>
</li>
<li class="drawer__list-item">
<span class="drawer__list-icon">🗑️</span>
<span class="danger">Delete</span>
</li>
</ul>
</div>
</div>
<!-- Right Drawer -->
<div class="drawer drawer--right" id="drawer-right" role="dialog" aria-modal="true" aria-label="Right sidebar">
<div class="drawer__header">
<h3 class="drawer__title">Settings</h3>
<button class="drawer__close" onclick="closeDrawer('right')" aria-label="Close">✕</button>
</div>
<div class="drawer__content">
<p class="drawer__desc">Right sidebar drawer — great for settings panels, detail views, and filters.</p>
<div class="drawer__section">
<label class="drawer__label">Theme</label>
<div class="drawer__toggle-row">
<span>Dark mode</span>
<div class="toggle active"></div>
</div>
</div>
<div class="drawer__section">
<label class="drawer__label">Notifications</label>
<div class="drawer__toggle-row">
<span>Email alerts</span>
<div class="toggle active"></div>
</div>
<div class="drawer__toggle-row">
<span>Push notifications</span>
<div class="toggle"></div>
</div>
</div>
<div class="drawer__section">
<label class="drawer__label">Language</label>
<select class="drawer__select">
<option>English</option>
<option>Spanish</option>
<option>French</option>
</select>
</div>
</div>
<div class="drawer__footer">
<button class="btn-primary" onclick="closeDrawer('right')">Save Settings</button>
</div>
</div>
<!-- Left Drawer -->
<div class="drawer drawer--left" id="drawer-left" role="dialog" aria-modal="true" aria-label="Navigation drawer">
<div class="drawer__header">
<span class="drawer__logo">✦ App</span>
<button class="drawer__close" onclick="closeDrawer('left')" aria-label="Close">✕</button>
</div>
<div class="drawer__content">
<nav class="drawer__nav">
<a href="#" class="drawer__nav-item active">
<span class="drawer__nav-icon">⬚</span> Dashboard
</a>
<a href="#" class="drawer__nav-item">
<span class="drawer__nav-icon">◫</span> Projects
</a>
<a href="#" class="drawer__nav-item">
<span class="drawer__nav-icon">◈</span> Analytics
</a>
<a href="#" class="drawer__nav-item">
<span class="drawer__nav-icon">◎</span> Team
</a>
<div class="drawer__nav-divider"></div>
<a href="#" class="drawer__nav-item">
<span class="drawer__nav-icon">⚙</span> Settings
</a>
<a href="#" class="drawer__nav-item">
<span class="drawer__nav-icon">?</span> Help
</a>
</nav>
</div>
</div>
<!-- Top Drawer -->
<div class="drawer drawer--top" id="drawer-top" role="dialog" aria-modal="true" aria-label="Top notification drawer">
<div class="drawer__header">
<h3 class="drawer__title">Notifications</h3>
<button class="drawer__close" onclick="closeDrawer('top')" aria-label="Close">✕</button>
</div>
<div class="drawer__content drawer__content--horizontal">
<div class="notification notification--success">
<span class="notification__icon">✓</span>
<div>
<strong>Deployment complete</strong>
<p>v2.4.1 is live — 2 minutes ago</p>
</div>
</div>
<div class="notification notification--warning">
<span class="notification__icon">⚠</span>
<div>
<strong>High memory usage</strong>
<p>Server at 87% — 15 minutes ago</p>
</div>
</div>
<div class="notification notification--info">
<span class="notification__icon">ℹ</span>
<div>
<strong>New team member</strong>
<p>Alex joined the workspace — 1 hour ago</p>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Sheet / Drawer
A set of slide-in sheet panels that animate in from any of the four screen edges. Built with pure CSS transforms and vanilla JS — no dependencies.
Variants
- Bottom sheet — mobile-first, drag-to-dismiss with touch and mouse
- Right drawer — sidebar pattern, slides in from the right
- Left drawer — navigation drawer, slides in from the left
- Top drawer — notification or search bar, slides down from top
How it works
- Each drawer is
position: fixedand starts off-screen viatransform: translateY/translateX(100% / -100%) - Adding the
.openclass transitions it totransform: none - A translucent backdrop sits behind the active drawer; clicking it closes the drawer
- The bottom sheet supports drag-to-dismiss: tracking
touchstart/mousemovedelta and dismissing if dragged past 40% of its height - Pressing
Escapecloses the active drawer
Accessibility
- Drawer panels use
role="dialog"andaria-modal="true" - Focus is trapped inside the open drawer
- Close button always present and keyboard-reachable