StatsHub Docs
UI

Overlays

Dialog is the web default and does not exist in the native kit — mobile uses sheets, and how a screen is presented lives in a route table rather than in the screen.

Both kits can put something over the page. They disagree about what that should be, and the disagreement is deliberate rather than an omission.

Web: Dialog is the default

@statshub/ui-web wraps Base UI's dialog. Reach for it when the user has to make one decision or fill one short form, and the page behind it is context they should not lose.

import { Dialog, DialogContent, DialogTitle } from "@statshub/ui-web/components/dialog";

The rest of the vocabulary, and when each one beats a dialog:

ComponentUse it when
dialogA focused decision or a short form. The default.
sheetA panel anchored to an edge — the mobile filter panel is one, side="left"
bottom-sheet, top-sheetEdge-anchored and draggable, on Silk
long-sheetThe content is taller than the viewport and scrolls
sheet-with-keyboardIt contains an input the on-screen keyboard would cover
drawerNavigation, not a task
popoverA small control anchored to the thing that opened it
commandA searchable list — palettes, pickers
hover-card, tooltipExplanation on hover. Never for anything a touch user needs

Two of those are traps. tooltip and hover-card do not exist on touch, so anything only reachable by hover is unreachable on a phone — hybrid-tooltip is the wrapper for when you need one anyway. And a drawer full of form fields is a dialog that opened from the wrong edge.

Mobile: there is no Dialog, on purpose

@statshub/ui-native ships no dialog, no modal and no alert component. That is not a gap to fill. iOS and Android both express "something over the page" as a sheet, and a centred box over a dimmed backdrop reads as a web page wearing a native costume.

Sheets live in sheet/:

FileWhat it is
sheet.tsxThe primitive
sheet-surface.tsxThe surface every sheet is drawn on
persistent-sheet.tsxOne that stays up while the board behind it is used
filter-sheet-screen.tsxThe shell a filters sheet is — see UI
tray.tsxThe screener's older stack-based tray
surface-style.tsThe store; the app owns reading and writing it

Presentation is a route table, not a prop

The native kit declares the vocabulary and the app decides which route gets which:

type RoutePresentation = "card" | "modal" | "fullScreenModal" | "formSheet";

A screen does not know how it is presented. lib/core/route-presentation.ts holds the mapping, because how a screen appears is something you change for navigation reasons, and grouping screens by role rather than by presentation is what lets you change it in one place. The mobile architecture page covers the two names every route has.

This is one of the three seams the kit leaves to the app

The kit owns the RoutePresentation vocabulary because sheets and headers read it. Which route gets which presentation stays in the app. A kit file reaching for @/lib/... means the component belongs in the app instead — see ui-native.

Choosing, on either platform

Ask what happens to the page underneath.

  • It stays relevant and the user comes back to it — dialog on web, sheet on mobile.
  • It is replaced for the duration — a route, not an overlay. fullScreenModal on mobile; a page on web.
  • It is a menu of destinationsdrawer on web, a pushed route on mobile.
  • It explains one controlpopover, and only if a touch user can reach it.

On this page