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:

anywhere.ts
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:

drawers.tsx
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:

drawer.tsx
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.