Skip to main content

Dialog

Dialogs interrupt the user with an important prompt or a focused task. Follows the Material Design 3 Dialog specification.

Dialog renders through Portal, so your app must be wrapped in a PortalHost once at the root. It claims the PORTAL_LAYERS.dialog layer, which puts it above bottom sheets and below snackbars, menus, and tooltips.

Usage

import { Button, Dialog, PortalHost } from '@rootnative/components'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useState } from 'react'
import { View } from 'react-native'

function Screen() {
const [open, setOpen] = useState(false)
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button onPress={() => setOpen(true)}>Delete file</Button>
<Dialog visible={open} onDismiss={() => setOpen(false)}>
<Dialog.Title>Delete file?</Dialog.Title>
<Dialog.Content>
This permanently removes report.pdf. You cannot undo this action.
</Dialog.Content>
<Dialog.Actions>
<Button variant="text" onPress={() => setOpen(false)}>Cancel</Button>
<Button variant="text" onPress={() => setOpen(false)}>Delete</Button>
</Dialog.Actions>
</Dialog>
</View>
)
}

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider>
<PortalHost>
<Screen />
</PortalHost>
</ThemeProvider>
</SafeAreaProvider>
)
}

Slots

Dialog composes from four slots. Write them in any order — Dialog places them in MD3 order (icon → headline → content → actions) itself, which is what lets the fullscreen variant move the title and actions into its header.

SlotRendersMD3 role
Dialog.IconAn IconSource at 24dp, centeredOptional hero icon. Its presence centers the headline.
Dialog.TitleheadlineSmall / onSurfaceHeadline
Dialog.ContentStrings get bodyMedium / onSurfaceVariant; anything else renders as-isSupporting text or arbitrary content
Dialog.ActionsAn end-aligned row with an 8dp gapAction buttons — text Buttons per MD3

Any child that isn't one of these is treated as content, so <Dialog><MyForm /></Dialog> works without ceremony.

With an icon

An icon makes the dialog a hero dialog: the icon sits centered above the headline in secondary, and the headline centers with it.

import { Button, Dialog, PortalHost } from '@rootnative/components'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useState } from 'react'
import { View } from 'react-native'

function Screen() {
const [open, setOpen] = useState(false)
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button variant="tonal" onPress={() => setOpen(true)}>Reset settings</Button>
<Dialog visible={open} onDismiss={() => setOpen(false)}>
<Dialog.Icon icon="alert-circle-outline" />
<Dialog.Title>Reset settings?</Dialog.Title>
<Dialog.Content>
Every preference returns to its default. Your data is untouched.
</Dialog.Content>
<Dialog.Actions>
<Button variant="text" onPress={() => setOpen(false)}>Cancel</Button>
<Button variant="text" onPress={() => setOpen(false)}>Reset</Button>
</Dialog.Actions>
</Dialog>
</View>
)
}

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider>
<PortalHost>
<Screen />
</PortalHost>
</ThemeProvider>
</SafeAreaProvider>
)
}

Full-screen

variant="fullscreen" fills the screen instead of centering a card. Dialog builds the MD3 header for you: a leading close button wired to onDismiss, the Dialog.Title as a titleLarge headline, and Dialog.Actions as the trailing confirming action. Dialog.Content scrolls below it. There is no scrim.

import { Button, Column, Dialog, PortalHost, TextField } from '@rootnative/components'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useState } from 'react'
import { View } from 'react-native'

function Screen() {
const [open, setOpen] = useState(false)
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button variant="tonal" onPress={() => setOpen(true)}>Edit profile</Button>
<Dialog visible={open} variant="fullscreen" onDismiss={() => setOpen(false)}>
<Dialog.Title>Edit profile</Dialog.Title>
<Dialog.Actions>
<Button variant="text" onPress={() => setOpen(false)}>Save</Button>
</Dialog.Actions>
<Dialog.Content>
<Column gap="lg">
<TextField label="Display name" value="Ada Lovelace" />
<TextField label="Email" value="ada@example.com" />
</Column>
</Dialog.Content>
</Dialog>
</View>
)
}

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider>
<PortalHost>
<Screen />
</PortalHost>
</ThemeProvider>
</SafeAreaProvider>
)
}

Dismissal

onDismiss fires for the user-initiated ways out — a scrim tap, the Android hardware back button, and the fullscreen close button. Buttons inside Dialog.Actions are yours to wire; Dialog never closes itself on an action press.

Set dismissable={false} when the user has to resolve the dialog with one of its actions. The scrim stops responding and stops being announced as an accessibility affordance, and the back button falls through to navigation.

Motion

Basic dialogs scale up from 80% and fade in on springDefaultSpatial; the scrim fades on springFastEffects. The fullscreen variant rises 48dp instead of scaling.

Every one of these collapses to a hard cut under reduced motion — see Motion.

Intentional deviation. MD3 slides a full-screen dialog up from the bottom edge of the screen. That needs the measured surface height, which fights the safe-area layout, so RootNative uses a 48dp rise. At spring speed the two read the same.

Tokens

TokenBasicFull-screen
ContainersurfaceContainerHighsurface
Corner28dp (cornerExtraLarge)
ElevationLevel 3
Widthmin 280dp, max 560dpFills the screen
Padding24dp24dp body, 56dp header
Icon24dp, secondary24dp, secondary
HeadlineheadlineSmall / onSurfacetitleLarge / onSurface
Supporting textbodyMedium / onSurfaceVariantbodyMedium / onSurfaceVariant
ActionsText buttons, end-aligned, 8dp gapTrailing text button in the header
Scrimscrim @ 32%None

Accessibility

  • Both surfaces report role="dialog" plus aria-modal and accessibilityViewIsModal, so assistive tech treats the content beneath as inert. Pass role="alertdialog" for a dialog that interrupts the user with something they must resolve — assistive technology treats that role as urgent, so it is opt-in rather than the default.
  • The dialog takes its accessible name from Dialog.Title when the headline is a plain string. Build the headline out of nodes and there is nothing to lift, so pass accessibilityLabel yourself.
  • Dialog.Title reports role="heading".
  • On web, focus moves into the surface when it opens, Tab and Shift-Tab cycle inside it, Escape dismisses a dismissable dialog, and focus returns to the control that opened it on close. See Accessibility.
  • The scrim is announced as a button labelled "Close dialog" — override with scrimAccessibilityLabel. It disappears from the accessibility tree when dismissable={false}.
  • The fullscreen close button is labelled "Close" — override with closeAccessibilityLabel.

Props

PropTypeDefaultRequiredDescription
visibleboolean-YesWhether the dialog is shown. Exit animations run before it unmounts.
onDismiss() => void-YesCalled when the user dismisses the dialog — scrim tap, Android back button, or the fullscreen close button. Actions inside `Dialog.Actions` are wired up by the consumer, not by this callback.
childrenReactNode-No`Dialog.Icon`, `Dialog.Title`, `Dialog.Content`, `Dialog.Actions`.
variantenumbasicNoDialog anatomy.
dismissablebooleanNoWhether a scrim tap and the Android back button dismiss the dialog. Set `false` for a dialog the user must resolve with one of its actions.
containerColorstringsurfaceContainerHigh (basic) / surface (fullscreen)NoOverride the container (surface) color.
closeIconIconSource'close'NoIcon for the fullscreen variant's leading close button.
closeAccessibilityLabelstringCloseNoScreen-reader label for the fullscreen close button.
scrimAccessibilityLabelstringClose dialogNoScreen-reader label for the scrim's dismiss action.
roleenum'dialog'NoAnnounced role. An MD3 dialog is a plain `'dialog'`; pass `'alertdialog'` only for one that interrupts the user with something they must resolve (destructive confirmation, error), since assistive technology treats that role as urgent.
accessibilityLabelstring-NoAccessible name for the dialog. Derived from `Dialog.Title` when its headline is a plain string, so this is only needed when the headline is built from nodes or the dialog has no title.
styleStyleProp<ViewStyle>-NoStyle applied to the dialog surface.
scrimStyleStyleProp<ViewStyle>-NoStyle applied to the scrim. Ignored by the fullscreen variant.
testIDstring-NoTest id applied to the dialog surface.
onKeyDown(event: { nativeEvent: { key?: string; }; }) => void-No-

Dialog.Icon

PropTypeDefaultRequiredDescription
iconIconSource-YesIcon to display. Accepts a string name (resolved via the theme's `iconResolver`), a pre-rendered element, or a render function.
colorstringtheme.colors.secondaryNoOverride the icon color.
sizenumber24NoOverride the icon size in dp.
styleStyleProp<ViewStyle>-No-

Dialog.Title

PropTypeDefaultRequiredDescription
childrenReactNode-YesHeadline text, or arbitrary nodes when a plain string isn't enough.
colorstringtheme.colors.onSurfaceNoOverride the headline color.
styleStyleProp<TextStyle>-No-

Dialog.Content

PropTypeDefaultRequiredDescription
childrenReactNode-YesSupporting text or arbitrary content. Strings and numbers are wrapped in MD3 supporting-text styling; anything else renders as given.
colorstringtheme.colors.onSurfaceVariantNoOverride the supporting-text color.
styleStyleProp<ViewStyle>-No-

Dialog.Actions

PropTypeDefaultRequiredDescription
childrenReactNode-YesAction buttons — text `Button`s per MD3.
styleStyleProp<ViewStyle>-No-