Skip to content

Native Sheet Dialog

A sheet on the native dialog element — no hand-rolled focus trap, and the three things the platform leaves you fixed.

DOM + CSS0 deps1 file · 8 KB TSXreduced-motion aware

Prominimaleditorial
Preview

Props

280
Light dismiss
Lock page scroll

The source is part of Pro

$27/month or $179 once. See Pro

No runtime dependencies
"use client";

import { useEffect, useRef, useState } from "react";

/**
 * A sheet built on the NATIVE <dialog> element.
 *
 * The most important decision here is what is NOT in this file: there is no
 * hand-rolled focus trap. `showModal()` already traps focus, makes the rest of
 * the document inert, promotes the element to the top layer so no z-index can
 * bury it, and restores focus to the trigger on close. Every hand-written trap
 * eventually misses something — shadow roots, contenteditable, elements that
 * become disabled while open, the address bar.
 *
 * What the platform does NOT do, and this fixes, is the three things below.
 */
export function NativeSheetDialog({
  label = "Refine",

  // ---------------------------------------------------------------
  // Full source available with a viberdy Pro subscription.
  // https://viberdy.dev/pricing
  // ---------------------------------------------------------------
}
Details

About this pattern

Most modal components in the wild are several hundred lines of focus management that the browser has shipped natively for years. Calling showModal on a dialog element traps focus, marks the rest of the document inert, promotes the element into the top layer where no stacking context can bury it, and restores focus to the trigger on close — and a hand-written trap will eventually miss shadow roots, contenteditable regions, or controls that become disabled while the dialog is open. So the interesting content here is the small set of things the platform genuinely leaves to you. Locking the page with overflow hidden removes the scrollbar and makes the viewport a dozen or so pixels wider, which lurches the entire page sideways behind the sheet; measuring that width and adding it back as padding is two lines and is the most visible defect in hand-built modals. Closing on transitionend alone leaves the dialog permanently open for anyone with reduced motion enabled or any stylesheet that zeroes the duration, so a timer has to back it up. And light dismiss has to check both ends of the gesture, because a backdrop click reports the dialog itself as its target — check only the click and selecting text inside the sheet then releasing outside it closes the sheet and discards the selection. Escape also needs intercepting through the cancel event, or it closes instantly and skips the exit entirely.

Curator’s note

The best part of this component is the code that is not in it. showModal already gives you the trap, the inert background and the top layer.

Pairs well with

Pro