Dialog

A centred card over a dimmed app that asks for a decision or a short input — confirm a delete, rename a file, accept terms.

Open Source

Installation

Dialog ships with the library — install it once, then import:

import { Dialog } from "@delacour/react-native-ui/dialog";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add dialog

A dialog draws through the overlay foundation, so mount OverlayProvider once at your app's root, inside DelacourProvider and around BottomSheetProvider. Without it the dialog draws inline and warns once in development.

Usage

Confirm a destructive action
app/index.tsx
import { Button } from "@/components/ui/button";
import { Dialog } from "@/components/ui/dialog";
import { View } from "react-native";

export default function Example() {
  return (
    <View className="flex-1 items-center justify-center p-4">
      <Dialog>
        <Dialog.Trigger asChild>
          <Button variant="destructive">Delete project</Button>
        </Dialog.Trigger>
        <Dialog.Content>
          <Dialog.Close />
          <Dialog.Header>
            <Dialog.Title>Delete project?</Dialog.Title>
            <Dialog.Description>This cannot be undone.</Dialog.Description>
          </Dialog.Header>
          <Dialog.Footer>
            <Dialog.Close asChild>
              <Button variant="secondary">Cancel</Button>
            </Dialog.Close>
            <Dialog.Close asChild>
              <Button variant="destructive">Delete</Button>
            </Dialog.Close>
          </Dialog.Footer>
        </Dialog.Content>
      </Dialog>
    </View>
  );
}

Anatomy

<Dialog>
  <Dialog.Trigger />
  <Dialog.Content>
    <Dialog.Close />
    <Dialog.Header>
      <Dialog.Title />
      <Dialog.Description />
    </Dialog.Header>
    <Dialog.Body />
    <Dialog.Footer />
  </Dialog.Content>
</Dialog>

Dialog.Content renders nothing while the dialog is closed, and stays mounted through its exit animation. Dialog.Close without children is the ✕ in the card's corner; with asChild it turns any control into one that closes. Dialog.Body is optional — a confirmation is a header and a footer.

Use a dialog when the user has to answer before going on. Use a Popover when the surrounding context must stay visible, and a BottomSheet when the content is long or scrolls.

Alert dialog

isDismissible={false} turns off the ways out the user did not choose: the scrim still takes the touch but does nothing, Android back is ignored, the iOS escape gesture does nothing, and the card announces itself as an alert dialog. Its own actions still close it.

An alert dialog
<Dialog isDismissible={false}>
  <Dialog.Trigger asChild>
    <Button variant="outline">Show terms</Button>
  </Dialog.Trigger>
  <Dialog.Content size="sm">
    <Dialog.Header>
      <Dialog.Title>Updated terms</Dialog.Title>
      <Dialog.Description>Review and accept the terms to keep working.</Dialog.Description>
    </Dialog.Header>
    <Dialog.Footer>
      <Dialog.Close asChild>
        <Button>Accept</Button>
      </Dialog.Close>
    </Dialog.Footer>
  </Dialog.Content>
</Dialog>

A short form

Put a field in Dialog.Body. When it takes focus the card lifts just far enough to keep its bottom edge 16pt above the keyboard — never so far that its top slides under the status bar — and settles back when the keyboard closes. variant="panel" sets the footer on a muted band that runs to the card's edges.

A short form
const [isOpen, setIsOpen] = useState(false);
const [name, setName] = useState("Harbour Bridge");

<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
  <Dialog.Trigger asChild>
    <Button variant="secondary">Rename</Button>
  </Dialog.Trigger>
  <Dialog.Content>
    <Dialog.Close />
    <Dialog.Header>
      <Dialog.Title>Rename project</Dialog.Title>
    </Dialog.Header>
    <Dialog.Body>
      <Field>
        <Field.Label>Name</Field.Label>
        <Input onChangeText={setName} value={name} />
      </Field>
    </Dialog.Body>
    <Dialog.Footer variant="panel">
      <Dialog.Close asChild>
        <Button variant="secondary">Cancel</Button>
      </Dialog.Close>
      <Button onPress={() => setIsOpen(false)}>Save</Button>
    </Dialog.Footer>
  </Dialog.Content>
</Dialog>;

Sizes

size caps the card's width: sm 320, md 400 (the default), lg 520, and full, which fills the screen gutter. The card is always full width up to its cap, so on a narrow phone the sizes look alike and only a wider screen tells them apart. An sm footer stacks its actions full width, in written order — write the primary action last and it sits last either way.

Sizes
<Dialog.Content size="sm">…</Dialog.Content>
<Dialog.Content size="md">…</Dialog.Content>
<Dialog.Content size="lg">…</Dialog.Content>
<Dialog.Content size="full">…</Dialog.Content>

Controlled

Pass isOpen and onOpenChange to own the state. A controlled dialog needs no Dialog.Trigger — any control can set it. onOpenChange fires once per change, whichever way it happened: the trigger, a close, the scrim, Android back or the escape gesture.

const [isOpen, setIsOpen] = useState(false);

<Button onPress={() => setIsOpen(true)}>Open</Button>
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
  <Dialog.Content>…</Dialog.Content>
</Dialog>;

useDialog() reads the enclosing dialog, so a custom child can close it without a setter threaded down:

function SubmitButton() {
  const { close } = useDialog();
  return <Button onPress={async () => { await save(); close(); }}>Save</Button>;
}

Over a bottom sheet

A dialog draws in the overlay foundation's modal band, above every bottom sheet and the navigator's header, so a dialog opened from inside a sheet appears over it, and Android back closes the dialog first. The reverse does not hold: a sheet opened from a dialog draws under it, so do not open a sheet from a dialog.

Over a bottom sheet

Accessibility

The card is modal to assistive technology — VoiceOver and TalkBack cannot move focus out of it — and announces itself as a dialog, or an alertdialog when it is not dismissible. On Android it is labelled by its title. Once the card has finished entering, focus moves to the title; once it has gone, focus returns to the trigger if it is still on screen. The iOS escape gesture (a two-finger Z) closes a dismissible dialog. The scrim is hidden from assistive technology. Dialog.Close without children is labelled Close and has 8pt of extra touch area around its glyph.

Under Reduce Motion the card fades without growing or sliding.

API Reference

Dialog

Prop

Type

Dialog.Trigger

Takes Pressable's props. With asChild it hands its press to the single child — a Button, say — which then opens the dialog before running its own onPress.

Dialog.Content

Extends React Native's ViewProps.

Prop

Type

Dialog.Close

Takes Pressable's props. Without children it is the corner ✕; with asChild it hands its press to the single child. Either way it closes the dialog, then calls your onPress.

Prop

Type

Dialog.Header, Dialog.Body

Take React Native's ViewProps and className.

Dialog.Title, Dialog.Description

Take Text's props. The title is a Text.Header, the description a muted Text.Paragraph.

Dialog.Footer

Extends React Native's ViewProps.

Prop

Type

useDialog

Reads the enclosing dialog. Throws outside a Dialog.

Prop

Type

On this page