Skip to main content
Graphite UI

Overlay

The behaviour Modal, Popover, Menu and Tooltip share: how they close, where focus goes, and when it is trapped. It is a hook with no markup, so you only reach for it when building a new overlay, never to style one.

Contract 2.0.0No kit page

Live preview

Rendered by the component itself from the same generated tokens as the rest of the site.

Focus is on nothing (the page). Open the panel, Tab through it, then press Escape or click away.

Installation

Install it with the shadcn CLI, along with everything it uses. New project? Set it up first, as Quick start shows.

npx shadcn@latest add https://www.graphite-ui.com/r/overlay.json
import { useOverlay } from '@/components/ui/overlay'
import type { DismissOptions } from '@/components/ui/overlay'

Anatomy

Overlay has no slots because it draws nothing. It is one hook, useOverlay, that each overlay calls with its own switches instead of wiring its own listeners, and this is what each one turns on. Notification’s contract cites the same base, but it sits inline with nothing to dismiss, so it does not call the hook.

Which dismissals each overlay honours
OverlayEscapeOutside pressFocus trap
ModalWhen dismissibleThe scrim, when dismissibleAlways
PopoverYesYes. Its own trigger toggles it insteadWhen modal
MenuYesYes. Its trigger toggles it, and choosing an item closes itNo
TooltipYesNo. It closes on pointer leave and blurNo

API reference

Generated from the contract, versioned with it, and checked by drift-check. This table cannot describe props the component does not have.

Overlay props
PropTypeDefaultNotes
dismissOn'escape' | 'outside' | 'close-control'—Which dismissals a given overlay honours. Every overlay honours at least one.
trapFocusboolean—Modal overlays trap; non-modal ones do not.

Design tokens

Every swatch is live. Change the source color in the header and this table repaints, because it reads the same roles the component does.

The contract declares no tokens: this component draws nothing of its own.

Usage

The contract's prohibitions, written as the choices you will actually face.

Do

  • Call useOverlay from any new floating surface, and pass it switches rather than adding listeners of your own.
  • Turn trapFocus on for anything that blocks the page, and leave it off for anything the reader can ignore.
  • Declare elevation and shadow in the new overlay’s own contract: elevation-01 for the surface and shadow-overlay for the lift, with no edge, as the kit draws its overlays.
  • Unmount the content on close, as the four existing overlays do. Their no-nesting checks depend on it.

Don’t

  • Give one instance its own way of closing. An overlay that needs a different pattern is a different component, not a variant.
  • Keep closed content mounted to animate it out. That would move the nesting checks onto the prerender path.
  • Animate transform on the way in. Tooltip carries its placement in transform, so the shared entrance is opacity only.
  • Send focus somewhere new on close. It goes back to whatever held it when the overlay opened, every time.

Accessibility

What the component does for you, and what it leaves to you.

Escape
Open overlays stack in the order they opened, and Escape closes only the top one, so each press peels off one layer. A Modal with dismissible off still takes the press, so nothing beneath it closes either.
Focus return
When an overlay closes, focus goes back to whatever held it when the overlay opened, whether it was trapped or not. The exception is focus the reader has already moved elsewhere: Tab away from a Tooltip’s trigger and focus stays where the Tab put it.
Trapping
With trapFocus on, the overlay takes focus as it opens, and Tab and Shift+Tab wrap between its first and last focusable elements. Only the topmost trap wraps Tab.
Outside press
With outside on, a press outside the overlay closes it. The control that opened it does not count as outside, so its own click can toggle it shut. A press inside an overlay stacked above does not count either, so a Menu in a Modal closes on a press elsewhere in the Modal while the Modal stays. Nothing reaches past a trapping overlay.
Roles
The hook sets no roles and no labels. Each overlay adds its own: dialog, menu or tooltip.
Re-renders
An open overlay can re-render freely. The hook reads the latest onDismiss when it needs it, so an inline one is fine, and focus stays where the reader put it.