code/tamagui.dev/data/docs/components/dialog/1.131.0.mdx
<Highlights
features={[
Comes with styling, yet completely customizable and themeable.,
Accepts animations, themes, size props and more.,
Accessible with dev-time checks to ensure ARIA props.,
]}
/>
Dialog is a great way to show content inside a new floating window above content. Be sure to open the code example above for a copy-paste implementation.
Dialog is already installed in tamagui, or you can install it independently:
npm install @tamagui/dialog
If you aren't using tamagui and instead using the @tamagui/dialog package
separately, you'll first need to install the @tamagui/portal package:
npm install @tamagui/portal
Then add PortalProvider to the root of your app:
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
import { Dialog } from 'tamagui' // or '@tamagui/dialog'
export default () => (
<Dialog>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
</Dialog.Content>
</Dialog.Portal>
<Dialog.FocusScope loop trapped focusOnIdle={true}>
<Dialog.FocusScope.Scope>
</Dialog.FocusScope.Scope>
</Dialog.FocusScope>
</Dialog>
)
Dialog supports scoping which lets you mount one or more Dialog instances at the root of your app, while having a deeply nested child Trigger or Content attach to the proper parent Dialog instance.
In performance sensitive areas you may want to take advantage of this, it allows you to only need to render the Dialog.Trigger inside the sensitive area as Dialogs aren't the cheapest component - they have a lot of functionality.
Here's the basic anatomy of using scope and placing your Dialog higher up for
performance:
import { Dialog } from 'tamagui'
// in your root layout:
export default ({ children }) => (
<Dialog scope="user-profile">
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
</Dialog.Content>
</Dialog.Portal>
{children}
</Dialog>
)
export default () => (
<Dialog.Trigger scope="user-profile">
<Button>Open Profile</Button>
</Dialog.Trigger>
)
Note that the Trigger scope ties to the Dialog scope.
By default, dialogs can be dismissed by:
modal={true}, which is the default):
disableOutsidePointerEvents set to true by defaultmodal={false}):
To prevent a dialog from closing when clicking outside:
<Dialog.Content
onPointerDownOutside={(event) => {
event.preventDefault()
}}
>
</Dialog.Content>
Contains every component for the dialog. Beyond Tamagui Props, adds:
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: Must contain Dialog.Content,
},
{
name: 'size',
type: 'SizeTokens',
description: Passes size down to all sub-components when set for padding, arrow, borderRadius.,
},
{
name: 'open',
type: 'boolean',
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
},
{
name: 'modal',
type: 'boolean',
default: 'true',
description: Renders into root of app instead of inline,
},
{
name: 'disableRemoveScroll',
type: 'boolean',
required: false,
description: Used to disable the automatic removal of scrolling from the page when open.,
},
]}
/>
Just Tamagui Props.
Renders Dialog into appropriate container. Beyond Tamagui Props, adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.,
},
{
name: 'unstyled',
required: false,
type: boolean,
description: Removes all default Tamagui styles.,
},
]}
/>
Main container for Dialog content, this is where you should apply animations.
Beyond Tamagui Props, adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.,
},
{
name: 'unstyled',
required: false,
type: boolean,
description: Removes all default Tamagui styles.,
},
{
name: 'disableOutsidePointerEvents',
type: 'boolean',
required: false,
description: When true, hover/focus/click interactions will be disabled on elements outside the Dialog. Users will need to click twice on outside elements to interact with them: once to close the Dialog, and again to trigger the element. Note: In v1, modal dialogs have this set to true by default.,
},
]}
/>
Displays behind Content. Beyond Tamagui Props, adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.,
},
]}
/>
Required. Can wrap in VisuallyHidden to hide.
Defaults to H2, see Headings.
Required. Can wrap in VisuallyHidden to hide.
Defaults to Paragraph, see Paragraph.
Closes the Dialog, accepts the same props as YStack. Recommended to use with
your own component and asChild.
<PropsTable
data={[
{
name: 'displayWhenAdapted',
type: 'boolean',
description: By default Close elements hide when Adapt is active. If set to true, they will show when adapted.,
},
]}
/>
Just Tamagui Props.
Provides access to the underlying FocusScope component used by Dialog for focus management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: Whether focus management is enabled,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: Event handler called when auto-focusing on mount. Can be prevented,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: Event handler called when auto-focusing on unmount. Can be prevented,
},
]}
/>
When used with Adapt, Dialog will render as a sheet when that breakpoint is
active.
See Sheet for more props.
Must use Adapt.Contents inside the Dialog.Sheet.Frame to insert the contents
given to Dialog.Content
import { Dialog } from 'tamagui' // or '@tamagui/dialog'
export default () => (
<Dialog>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
</Dialog.Content>
</Dialog.Portal>
<Dialog.Adapt when="maxMd">
<Dialog.Sheet>
<Dialog.Sheet.Frame>
<Dialog.Adapt.Contents />
</Dialog.Sheet.Frame>
<Dialog.Sheet.Overlay />
</Dialog.Sheet>
</Dialog.Adapt>
</Dialog>
)
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: Defines whether to add a default root host or not.,
},
]}
/>
If you're using native modals (maybe from react-navigation), you'll notice the
Dialogs won't show up inside the modal. To get around this, you should wrap your
screen inside PortalProvider, like so:
import { PortalProvider } from 'tamagui'
// this component used in react-navigation/expo-router with `presentation: "modal"`
export function Page() {
return (
<PortalProvider></PortalProvider>
)
}