TLDR: If your custom modal doesn't have max-h-[90vh] overflow-y-auto on the content element, tall forms will push the Save button off-screen — and users literally can't reach it.

the report

My client messaged in: "The facility name edits won't save, no save button, screen frozen."

This is the admin panel for a cancer care directory that routes patients to the right facilities, so "can't save" is not a minor inconvenience.

My first instinct was JavaScript. Broken server action. Form not submitting. Something wrong with the wiring.

Wrong.

what was actually broken

The admin edit modal is a custom dialog component at src/components/ui/dialog.tsx — hand-rolled, not Radix UI (the popular headless component library that handles this stuff for you).

The overlay wrapper: fixed inset-0 flex items-center justify-center.

Sounds reasonable. But here's the trap nobody warns you about.

When you vertically center a flex child that's taller than the viewport, the overflow spills off both the top and the bottom simultaneously.

Not just the bottom.

Both.

Because centering is equidistant from each edge — so when the child overflows, it clips equally on both ends. You can't scroll to the top. You can't scroll to the bottom. The browser registers the CONTENT as non-scrollable because nothing on the content element says it should scroll.

The facility edit form had 10+ field groups, a webhook-recipient editor (where my client configures who gets referral notifications), and the Update button at the very bottom.

No max-height. No overflow-y: auto.

The whole modal extended past the screen in both directions, and nothing was scrollable.

My client wasn't wrong. There WAS no Save button. The CSS had erased it.

the fix

One line. Two classes:

- "relative z-50 w-full max-w-lg rounded-2xl bg-background p-6 shadow-2xl border"
+ "relative z-50 w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-2xl bg-background p-6 shadow-2xl border"

max-h-[90vh] caps the dialog at 90% of the viewport height.

overflow-y-auto lets the content scroll within that cap.

Commit 0d47f67. Done.

why the abstraction level matters

Because dialog.tsx is a shared primitive — every modal in the app uses it.

One fix. Every long modal, unblocked in one shot.

That's the real lesson here. It's not just "remember to add overflow." It's: if you're hand-rolling a dialog, these two classes are not optional. They're the difference between a modal and a broken trap.

The minimal viable custom dialog is:

  • fixed inset-0 flex items-center justify-center on the overlay ✓
  • max-h-[90vh] overflow-y-auto on the content element

Next-level version has a sticky header + sticky footer (Save button never scrolls away), with an overflow-y-auto flex-1 middle section. We'll get there. For now, my client can save her facilities.

And I'll never look at a flex items-center overlay the same way again.