Building a Resizable Drawer
How the resizable, stackable drawer system on this site works
Why a drawer?
This site has a bunch of company blurbs I wanted people to read without leaving the page they were on. A modal felt too aggressive, a tooltip too small. A drawer that slides in from the right hit the sweet spot — you can read it, resize it, collapse it, or stack another one on top. Try it:
Open both — they stack. Drag the left edge to resize. Click the header to collapse it into a rail.
How it works
Three pieces: a DrawerManager that lives at the layout level and listens for custom events, a Drawer component that handles resizing and collapsing, and a registry that maps names to lazy-loaded React components.
Opening a drawer from anywhere on the site is one function call:
| 1 | import { openDrawer } from "./drawer/drawers"; |
| 2 | |
| 3 | // name must match a key in the registry |
| 4 | openDrawer("aws"); |
| 5 | |
| 6 | // with optional header and width |
| 7 | openDrawer("meta", {}, "Meta", 480); |
Under the hood that dispatches a CustomEvent.
The DrawerManager picks it up, pushes a new entry onto its state array,
and React renders the drawer into a portal on document.body
so it sits above everything without z-index headaches from parent stacking
contexts.
The registry
Every drawer component is registered behind a dynamic import so nothing loads until someone actually opens it:
| 1 | const drawerRegistry: DrawerRegistry = { |
| 2 | aws: () => import("../company-blurbs/aws-profile"), |
| 3 | bytedance: () => import("../company-blurbs/bytedance-profile"), |
| 4 | flexport: () => import("../company-blurbs/flexport-profile"), |
| 5 | meta: () => import("../company-blurbs/meta-profile"), |
| 6 | shift: () => import("../company-blurbs/shift-technology-profile"), |
| 7 | uber: () => import("../company-blurbs/uber-profile"), |
| 8 | }; |
Adding a new drawer means adding one line here and writing the component.
The DrawerName type
is derived from the registry keys, so TypeScript catches
typos at build time.
Resizing
A 2px strip on the left edge of the drawer acts as a resize handle.
Mousedown on it starts tracking mousemove
events, clamped between 200px and 1000px:
| 1 | const MIN_WIDTH = 200; |
| 2 | const MAX_WIDTH = 1000; |
| 3 | |
| 4 | const resize = (e: MouseEvent) => { |
| 5 | if (drawerRef.current) { |
| 6 | const newWidth = window.innerWidth - e.clientX; |
| 7 | if (newWidth > MIN_WIDTH && newWidth < MAX_WIDTH) { |
| 8 | drawerRef.current.style.width = `${newWidth}px`; |
| 9 | drawerWidth.current = newWidth; |
| 10 | } |
| 11 | } |
| 12 | }; |
Width is set directly on the DOM node via a ref rather than through React state to avoid re-rendering the content on every pixel of mouse movement. The ref tracks the current width so collapsing and re-expanding snaps back to wherever you left it.
Collapsing
Clicking the drawer header collapses it to a 60px rail with a vertical label. The content fades out via an opacity transition, the width animates down, and the collapsed state renders a completely different layout — vertical text, a "Tab" badge, and a close button at the bottom. Click the rail to expand it back to full width.
This is useful when you want to keep a drawer around for reference but need the screen space back temporarily.
Stacking
Multiple drawers stack horizontally from right to left. Each one gets an
incrementing z-index (1000 + index)
and they sit side by side in a flex row. Closing a drawer removes it from
the array and the others shift over. Try opening both buttons above to see
it in action.