# Side drawer

A panel that comes in from the right over a scrim, carrying a page's worth
of content without taking the visitor off the page they were reading.

## It arrives tilted and straightens

That is the whole character of the move. A panel that slides in square is a
drawer; one that comes in leaning and settles reads as having been thrown
onto the page.

```css
.panel {
  --gap: 10px;
  position: absolute;
  top: var(--gap); right: var(--gap); bottom: var(--gap);
  width: min(54vw, 760px);
  border-radius: 6px;
  transform-origin: 0% 100%;
  transform: translateX(calc(104% + var(--gap))) rotate(6deg);
  transition: transform .6s cubic-bezier(.22,.61,.36,1);
}
[data-open='true'] .panel { transform: none; }
```

The origin is the bottom-left corner — the one nearest the page — so the
top edge swings in rather than the whole slab rocking about its middle.

**It must not touch the edges.** The gap on three sides is what makes it
read as a panel laid over the page rather than as a side of the window. The
slide-out has to clear the gap as well as the panel's own width.

## Closed means gone, not invisible

```css
.root { visibility: hidden; transition: visibility 0s linear .6s; }
.root[data-open='true'] { visibility: visible; transition-delay: 0s; }
```

`visibility` rather than `display`, so the panel can animate out instead of
vanishing — and rather than opacity alone, which would leave an invisible
sheet swallowing every click on the page beneath.

## What it owes the keyboard

- Escape closes it.
- Focus moves into the panel on open — otherwise the keyboard is still on
  the page underneath.
- The page behind is scroll-locked while it is open. Without that the wheel
  scrolls whichever of the two the pointer happens to be over, which feels
  broken rather than deliberate.
- `role="dialog"`, `aria-modal`, and a name.

```js
useEffect(() => {
  if (!open) return
  const onKey = (e) => { if (e.key === 'Escape') onClose() }
  const prev = document.body.style.overflow
  document.body.style.overflow = 'hidden'
  window.addEventListener('keydown', onKey)
  closeButton.current?.focus()
  return () => {
    document.body.style.overflow = prev
    window.removeEventListener('keydown', onKey)
  }
}, [open, onClose])
```

## Where it lives

**One per page, in the shell** — not inside whichever section wants to open
it. The header opens it on every page; a component-owned instance means two
copies of the same panel in the DOM the moment a second thing wants it.

Make it an *action* in the menu rather than a destination. You read it and
carry on from where you were, instead of being taken somewhere and having
to come back.

## Traps

**The close control needs a gap-sized hit area, not a pill.** A filled
capsule next to a 72px heading is the heaviest thing on the panel. A small
circle holding a hairline cross with the word beside it reads as a control
without competing.

**Pin the header and scroll only the body**, so the way out is on screen
however far down the content you are.

## Prompt

> Build a right-side drawer. It is inset from the top, right and bottom by
> a small gap, has a modest corner radius, and arrives translated off-screen
> and rotated 6 degrees about its bottom-left corner, straightening as it
> lands. Closed, the root is `visibility: hidden` with a delayed transition
> so the panel can animate out. Escape and a scrim click both close it,
> focus moves to the close control on open, and the body is scroll-locked
> while it is open. The header is pinned; only the content scrolls.
