Skip to content

Dialog

A sheet that takes the page away until it is answered.

tsx
import { Button, Dialog, DialogClose } from 'neba';

<Dialog
  trigger={<Button color="danger">Delete workspace</Button>}
  title="Delete this workspace?"
  description="Every project, deploy and log inside it goes with it."
  actions={<DialogClose render={<Button color="danger">Delete</Button>} />}
>
  This cannot be undone.
</Dialog>;

Props

PropTypeDefaultDescription
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'The type scale, the padding, and how wide the sheet may get. One axis rather than a second scale spelled maxWidth
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. The sheet is never dyed, so it reaches the edge and the focus ring
densityshared'default' | 'compact''default'Padding only
openbooleanWhether it is shown. With onOpenChange, a controlled dialog
defaultOpenbooleanfalseThe initial state of an uncontrolled dialog
onOpenChange(open: boolean) => voidCalled when it opens or closes
triggerReactElementThe element that opens it, wired up by Base UI. Optional — a controlled dialog opened elsewhere needs none
titleReactNodeThe heading. Rendered as the h2 that names the dialog
descriptionReactNodeA line under the title, and the dialog's accessible description
actionsReactNodeThe bottom row, end-aligned. DialogClose is what makes one of them dismiss
dividersbooleanfalseSeparates the sections with hairlines instead of space. Worth turning on the moment the body scrolls
showClosebooleantrueThe × in the corner. On by default, unlike most booleans here: a modal takes the page away, and the way out should be visible
closeLabelstring'Close'Accessible name of the × button
widthnumber | stringA hard cap overriding the one size implies. Numbers are pixels
fullWidthbooleantrueTakes the full width its size allows. On by default, the other way round from everywhere else: a dialog’s container is the viewport
fullScreenbooleanfalseFills the viewport edge to edge
modalboolean | 'trap-focus'trueWhether the page behind is taken away. 'trap-focus' keeps it scrollable and clickable while holding focus inside
dismissiblebooleantrueWhether Escape and an outside click close it. Turn it off only with actions that answer it
childrenReactNodeThe body — the only part that scrolls

Examples

Size is the width

A body that scrolls

Only the body scrolls; the heading and the actions stay put. That is what dividers is really for here — the hairlines are what say the header did not move.

A dialog that has to be answered

dismissible={false} turns off both Escape and the click outside. Turn it off only when the dialog has actions that answer it, because there will be no other way out.

Closing without owning state

An uncontrolled dialog has no setOpen for its Cancel button to call, and making every dialog controlled is a piece of state per dialog that exists only to answer a button. DialogClose is the way out: it is Base UI's own close part, so render puts a real Neba button inside it.

tsx
actions={
  <>
    <DialogClose render={<Button variant="text" color="secondary">Cancel</Button>} />
    <DialogClose render={<Button color="danger">Delete</Button>} />
  </>
}

One size axis, not two

MUI splits this into size and maxWidth. Here they are one: size sets the type scale, the padding and how wide the sheet may get. A second five-value scale would be a second spelling of an idea the library already has a word for — see prop conventions.

The case the split exists for is real, though: small type on a wide sheet, for a table or a diff. That is what width is, and it is a hard number rather than a scale, so it never has to agree with anything.

What it does not have

No variant: the three weights answer "how much does this surface assert itself against the page around it", and a modal has already taken the page.

No elevation: the popup is one of the two surfaces in the library that is supposed to float, so it carries a level-3 shadow always. A dialog that could be told to sit flat would be a dialog that could be told to stop being a dialog.

Accessibility

Base UI owns everything hard here — the focus trap, the scroll lock, the inert page behind, restoring focus to the trigger on close, and wiring title and description into aria-labelledby and aria-describedby. The title is a real <h2>, so a dialog appears in the document outline where it should.

showClose is on by default, unlike most booleans in this library. A modal takes the page away until it is answered, and the visible way out should not have to be remembered. It is also what a touch screen reader escapes the popup with.

Released under the MIT License