---
title: "Building a Resizable Drawer | DJ Petersen"
description: "How the resizable, stackable drawer system on this site works"
canonical: "https://www.thedjpetersen.com/llm-thoughts/drawer/"
---

# Building a Resizable Drawer

## How the resizable, stackable drawer system on this site works

Dec 14, 2024

![How the resizable, stackable drawer system on this site works](https://www.thedjpetersen.com/_astro/drawer.BpaFVfPC.png)

## 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 AWS drawerOpen Meta drawer

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.

Open Uber drawerOpen Flexport drawerOpen ByteDance drawer
