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.
| Slot | Renders | MD3 role |
|---|---|---|
Dialog.Icon | An IconSource at 24dp, centered | Optional hero icon. Its presence centers the headline. |
Dialog.Title | headlineSmall / onSurface | Headline |
Dialog.Content | Strings get bodyMedium / onSurfaceVariant; anything else renders as-is | Supporting text or arbitrary content |
Dialog.Actions | An end-aligned row with an 8dp gap | Action 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
| Token | Basic | Full-screen |
|---|---|---|
| Container | surfaceContainerHigh | surface |
| Corner | 28dp (cornerExtraLarge) | — |
| Elevation | Level 3 | — |
| Width | min 280dp, max 560dp | Fills the screen |
| Padding | 24dp | 24dp body, 56dp header |
| Icon | 24dp, secondary | 24dp, secondary |
| Headline | headlineSmall / onSurface | titleLarge / onSurface |
| Supporting text | bodyMedium / onSurfaceVariant | bodyMedium / onSurfaceVariant |
| Actions | Text buttons, end-aligned, 8dp gap | Trailing text button in the header |
| Scrim | scrim @ 32% | None |
Accessibility
- Both surfaces report
role="dialog"plusaria-modalandaccessibilityViewIsModal, so assistive tech treats the content beneath as inert. Passrole="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.Titlewhen the headline is a plain string. Build the headline out of nodes and there is nothing to lift, so passaccessibilityLabelyourself. Dialog.Titlereportsrole="heading".- On web, focus moves into the surface when it opens, Tab and Shift-Tab cycle inside it, Escape dismisses a
dismissabledialog, 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 whendismissable={false}. - The fullscreen close button is labelled "Close" — override with
closeAccessibilityLabel.
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
visible | boolean | - | Yes | Whether the dialog is shown. Exit animations run before it unmounts. |
onDismiss | () => void | - | Yes | Called 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. |
children | ReactNode | - | No | `Dialog.Icon`, `Dialog.Title`, `Dialog.Content`, `Dialog.Actions`. |
variant | enum | basic | No | Dialog anatomy. |
dismissable | boolean | | No | Whether 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. |
containerColor | string | surfaceContainerHigh (basic) / surface (fullscreen) | No | Override the container (surface) color. |
closeIcon | IconSource | 'close' | No | Icon for the fullscreen variant's leading close button. |
closeAccessibilityLabel | string | Close | No | Screen-reader label for the fullscreen close button. |
scrimAccessibilityLabel | string | Close dialog | No | Screen-reader label for the scrim's dismiss action. |
role | enum | 'dialog' | No | Announced 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. |
accessibilityLabel | string | - | No | Accessible 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. |
style | StyleProp<ViewStyle> | - | No | Style applied to the dialog surface. |
scrimStyle | StyleProp<ViewStyle> | - | No | Style applied to the scrim. Ignored by the fullscreen variant. |
testID | string | - | No | Test id applied to the dialog surface. |
onKeyDown | (event: { nativeEvent: { key?: string; }; }) => void | - | No | - |
Dialog.Icon
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
icon | IconSource | - | Yes | Icon to display. Accepts a string name (resolved via the theme's `iconResolver`), a pre-rendered element, or a render function. |
color | string | theme.colors.secondary | No | Override the icon color. |
size | number | 24 | No | Override the icon size in dp. |
style | StyleProp<ViewStyle> | - | No | - |
Dialog.Title
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
children | ReactNode | - | Yes | Headline text, or arbitrary nodes when a plain string isn't enough. |
color | string | theme.colors.onSurface | No | Override the headline color. |
style | StyleProp<TextStyle> | - | No | - |
Dialog.Content
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
children | ReactNode | - | Yes | Supporting text or arbitrary content. Strings and numbers are wrapped in MD3 supporting-text styling; anything else renders as given. |
color | string | theme.colors.onSurfaceVariant | No | Override the supporting-text color. |
style | StyleProp<ViewStyle> | - | No | - |
Dialog.Actions
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
children | ReactNode | - | Yes | Action buttons — text `Button`s per MD3. |
style | StyleProp<ViewStyle> | - | No | - |