Noctis

Dialog

A centered modal. A trigger opens a portalled surface in the middle of the viewport that traps focus and blurs and dims the page behind it — composed from a header, a scrollable body, and a pinned footer. Reach for it for a focused form or an open-ended task; for an edge-docked panel, use Sheet.

Basic

A Dialog.Trigger opens the Dialog.Content. Compose the trigger from a Button through render so it inherits the button's look, hover, and focus; the content holds a Dialog.Header (with a Title, Description, and a built-in Dialog.CloseButton), a scrollable Dialog.Body, and a Dialog.Footer whose actions close the panel through Dialog.Close.

Sizes

Set the dialog's maximum width with size on Dialog.Contentsm, md (the default), lg, or full. The panel hugs its content vertically and caps its width by size, with a min-width floor so a small dialog never collapses below a readable width; it shrinks to fit the viewport on narrow screens and scrolls its body if the content runs tall. full lifts the width cap for a near-fullscreen, region-inset panel.

A focused form

Because the dialog traps focus and locks page scroll, it is a natural host for a short form. Put the fields in the Dialog.Body and the submit/cancel actions in the Dialog.Footer — each Dialog.Close dismisses the panel after its action runs. On a real submit, keep the dialog open and surface any error in place rather than closing optimistically.

Initial focus

Where focus lands when a dialog opens is a deliberate choice, not an accident. The WAI-ARIA APG calls for a considered initial focus: a form dialog should focus its first field so the user starts typing immediately, rather than landing on the corner close. Pass initialFocus (a Base UI Dialog.Popup prop, forwarded through Dialog.Content) a ref to the element to focus; finalFocus likewise controls where focus returns on close — it returns to the trigger by default. Focus is trapped within the panel while open and restored on close.

Scrolling long content

Tall content scrolls one of two ways, set with scroll on Dialog.Content. The default, scroll="body", scrolls the inner Dialog.Body so the header and footer stay pinned. scroll="viewport" hands the overflow to an overlay-level scroll container (Dialog.Viewport): the whole panel scrolls inside the scrollable overlay, staying centered when it fits and growing past the viewport without the page scrolling. Either way, a scroll-boundary cue fades a shadow onto a clipped edge so it always reads as more content above or below.

Nested dialogs

A dialog can open another dialog over it. Base UI tracks the depth, and the backgrounded panel recedes — it scales back and dims behind the new layer — so the stack reads as depth rather than a flat pile. Both motions respect prefers-reduced-motion: the layer still recedes and dims, only the animation drops.

Anatomy

Compose a dialog from its parts. Dialog.Root owns the open state and surfaces Base UI's dismiss controls — it accepts every Dialog.Root prop, including open, defaultOpen, onOpenChange, modal (true | 'trap-focus'), and disablePointerDismissal (turn off backdrop-click dismissal). Modal by default: focus is trapped, the page is scroll-locked, and the rest of the document is inert.

  • Dialog.Root — owns the open state; renders no element of its own. Surfaces modal and disablePointerDismissal.
  • Dialog.Trigger — opens the dialog. Style it directly or compose a Button through render.
  • Dialog.Content — the common composition: portal, backdrop, optional viewport, and centered panel in one. Props: size (default md), scroll (body default, or viewport), dismissable (default true — set false to suppress the corner CloseButton), backdropClassName, and the forwarded initialFocus / finalFocus. Reach for Dialog.Portal + Dialog.Backdrop + Dialog.Popup directly only when you need to customize the portal or backdrop wiring.
  • Dialog.Viewport — the overlay-level scroll container for scroll="viewport". Dialog.Content renders it for you; reach for it directly only in a hand-built composition.
  • Dialog.Popup — the centered panel surface. Set initialFocus / finalFocus here to control where focus lands and returns, and elevation (default elevated) to retheme the scope.
  • Dialog.Header — the panel's top region. A gutter row: its first column stacks the Title over the Description, and a reserved inline-end gutter holds the corner action, so a long title never runs under the close. Separated from the body by a divider.
  • Dialog.Body — the scrollable middle region; it grows to fill and scrolls its overflow so the header and footer stay put.
  • Dialog.Footer — the panel's bottom region, pinned to the base, typically holding the primary and secondary actions. It stacks on small screens and lays out in a row at the inline-end from 40rem up; it comfortably holds two or three actions (place the primary at the inline-end, cancel before it).
  • Dialog.Title + Dialog.Description — the panel's accessible name and supporting copy, linked to the popup via aria-labelledby and aria-describedby.
  • Dialog.CloseButton — the built-in corner close: a ghost icon Button with a localized aria-label, dropped into the header gutter (no absolute positioning). Defaults its glyph to an X and its name to the translated "Close"; pass children or aria-label to override, or dismissable={false} on Dialog.Content to suppress it.
  • Dialog.Close — closes the nearest dialog. Renders a bare button with no styling of its own, so it composes with any Button through render — a secondary or primary button for a footer action. Give it an aria-label (or visible text) for the accessible name.

Every rendered part carries a data-slot (noctis-dialog-trigger, noctis-dialog-backdrop, noctis-dialog-viewport, noctis-dialog-popup, noctis-dialog-close, noctis-dialog-header, noctis-dialog-body, noctis-dialog-footer, noctis-dialog-title, noctis-dialog-description) for host-side styling — the popup also carries data-size. Pair it with the Base UI state attributes (data-open, data-closed, data-starting-style, data-ending-style, and data-nested-dialog-open on a backgrounded layer). The popup renders through Surface at elevated elevation, bordered, with the modal shadow, so controls inside re-derive off that base and separate cleanly. The backdrop blurs the page behind it, and the panel enters with a short scale-and-rise that respects prefers-reduced-motion.

Accessibility

  • Roles & naming. The popup is role="dialog" with aria-modal; Dialog.Title names it (aria-labelledby) and Dialog.Description describes it (aria-describedby). For a description holding complex semantics (a list or table), drop the Description so a screen reader doesn't flatten it.
  • Initial focus. Set initialFocus to the considered first stop — the first field of a form, or a primary action for an informational dialog. Without it, focus lands on the first tabbable element. Keep a visible close in the tab sequence.
  • Focus return. Focus returns to the trigger on close; override with finalFocus when the trigger has unmounted (e.g. a deleted row).
  • Dismissal. Escape always closes the dialog and the backdrop click closes it by default; disablePointerDismissal turns off the backdrop click for flows that need an explicit choice. Keep Dialog.Close / Dialog.CloseButton inside the popup so touch screen-reader users can escape.
  • RTL. The header gutter, footer alignment, and corner close use logical properties, so the layout mirrors under RTL by construction.

Keyboard

KeyAction
Enter / SpaceOn the trigger: open the dialog and move focus into the panel per initialFocus.
Tab / Shift + TabMove focus to the next / previous element, trapped within the open panel (it wraps).
EscClose the panel and return focus to the trigger (or finalFocus).

A click on the dimmed backdrop also closes the dialog (unless disablePointerDismissal). Content behind the panel stays inert and unreachable while the dialog is open. Focus order and directional glyphs mirror under RTL by construction.

Design tokens

Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every dialog in that region retunes — e.g. .editor { --noctis-dialog-popup-max-inline-size: 40rem; } widens dialogs beneath it. Knobs that aren't minted are reached through the part's data-slot. See Customization for the full override ladder and Tokens for the whole graph.

Token

API reference

Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table; parts that only forward to Base UI's Dialog list just the props they pass through. Expand a row for the full type and description.

Dialog.Root

No props of its own — forwards to the underlying Base UI part.

Dialog.Trigger

Prop

Dialog.Portal

Prop

Dialog.Backdrop

Prop

Dialog.Popup

Prop

Dialog.Content

Prop

Dialog.Close

Prop

Dialog.Header

Prop

Dialog.Body

Prop

Dialog.Footer

Prop

Dialog.Title

Prop

Dialog.Description

Prop

AttributeDescription
data-slotThe stable, prefixed styling/testing hook on every rendered part.
data-sizePresent on the popup: the cross-axis cap (`sm` / `md` / `lg` / `full`) the width rules key off.
data-openPresent on the popup, backdrop, and trigger while the dialog is open.
data-closedPresent on the popup, backdrop, and trigger while the dialog is closed.
data-starting-stylePresent on the popup and backdrop during the enter transition's first frame.
data-ending-stylePresent on the popup and backdrop during the exit transition.
data-nestedPresent on the popup when it is nested inside another dialog.
data-nested-dialog-openPresent on the popup when it has a nested dialog open on top of it.