Push Side Drawer
The navigation drawer slides in from the left and pushes the page aside, which shrinks and dims a little to show it is in the background.
Create a push-style side drawer in React and CSS. The drawer (180px wide) starts at translateX(-100%) on the left; the page content sits at rest. When open, slide the drawer to translateX(0) and move the page to translateX(140px) scale(0.92) with a rounded corner and reduced brightness, both over 420ms with cubic-bezier(0.16, 1, 0.3, 1), so the page reads as pushed back. Stagger the drawer links in with opacity and translateX(-12px) → 0 using transition-delay of 60ms × index. Clicking the dimmed page closes the drawer, as does Escape. The menu button carries aria-expanded and aria-controls, and the drawer is inert while closed. Under prefers-reduced-motion, show and hide the drawer without moving the page. The links read Overview, Projects, Team, and Settings.
Source
'use client'
import { useId, useState } from 'react'
import s from './demo.module.css'
export default function SideDrawer({ labels }: { labels: Record<string, string> }) {
const [open, setOpen] = useState(false)
const id = useId()
return (
<div className={s.shell} data-open={open || undefined} onKeyDown={(e) => e.key === 'Escape' && setOpen(false)}>
<nav id={id} className={s.drawer} inert={!open}>
{[labels.a, labels.b, labels.c, labels.d].map((t, i) => (
<a key={t} href="#" className={s.link} style={{ '--i': i } as React.CSSProperties} onClick={(e) => e.preventDefault()}>
{t}
</a>
))}
</nav>
<div className={s.page} onClick={() => open && setOpen(false)}>
<div className={s.bar}>
<button
type="button"
className={s.menu}
aria-expanded={open}
aria-controls={id}
onClick={(e) => {
e.stopPropagation()
setOpen(!open)
}}
>
☰ {labels.menu}
</button>
<strong className={s.title}>{labels.title}</strong>
</div>
<div className={s.grid}>
<span />
<span />
<span />
<span />
</div>
</div>
</div>
)
}
.shell {
position: absolute;
inset: 0;
overflow: hidden;
background: var(--stage-bg);
}
.drawer {
position: absolute;
inset: 0 auto 0 0;
display: grid;
align-content: start;
gap: 4px;
width: 180px;
padding: 56px 16px 16px;
transform: translateX(-100%);
transition: transform 420ms var(--ease-out);
}
.link {
padding: 8px 10px;
border-radius: 8px;
color: var(--stage-ink);
font: 500 var(--text-sm) / 1 var(--font-body);
text-decoration: none;
opacity: 0;
transform: translateX(-12px);
transition: opacity 260ms, transform 260ms var(--ease-out), background-color var(--dur-micro);
}
.link:hover { background: var(--stage-surface); }
.link:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 1px; }
.page {
position: absolute;
inset: 0;
padding: 14px 16px;
background: var(--stage-surface);
transform-origin: 0 50%;
transition: transform 420ms var(--ease-out), border-radius 420ms var(--ease-out), filter 420ms;
}
.shell[data-open] .drawer { transform: none; }
.shell[data-open] .link { opacity: 1; transform: none; transition-delay: calc(120ms + var(--i) * 60ms); }
.shell[data-open] .page {
transform: translateX(140px) scale(0.92);
border-radius: 16px;
filter: brightness(0.7);
cursor: pointer;
}
.bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.menu {
height: 32px;
padding: 0 10px;
border: 1px solid var(--stage-rule);
border-radius: 8px;
background: var(--stage-bg);
color: var(--stage-ink);
font: 500 var(--text-sm) / 1 var(--font-body);
cursor: pointer;
}
.menu:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.title { font: 600 var(--text-base) / 1 var(--font-display); color: var(--stage-ink); }
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.grid span { height: 64px; border-radius: 10px; background: var(--stage-bg); }
@media (prefers-reduced-motion: reduce) {
.drawer, .link, .page { transition: none; }
.shell[data-open] .page { transform: none; filter: brightness(0.5); }
}