Dialog

Show a modal with configurable layout and accessible actions

Dialog presents focused content in an accessible modal with composable parts and optional Sheet adaptation.

Features

  • Customizable, themeable default styling

  • Transitions, themes, and focus props

  • Accessibility checks for ARIA props during development

  • Per-part presence lifecycle for reliable exit animations

Dialog shows content in a floating window above the page. Dialogs automatically stack above other overlays. Open the code example above for a copy-paste implementation.

Installation

Dialog is already installed in tamagui, or you can install it independently:

yarn add @tamagui/dialog

For native apps, we recommend setting up native portals to preserve React context inside Dialog content.

Anatomy

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Dialog.tsx (registry item "dialog")
// npm dependencies: @tamagui/core @tamagui/dialog
import { Dialog } from "../components/tamagui/Dialog"; // or '@tamagui/dialog'
export default () => <Dialog>
<Dialog.Trigger />
{/* Optional: control focus from a parent */}
<Dialog.FocusScope loop trapped focusOnIdle={true}>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
{/* ... */}
</Dialog.Content>
</Dialog.Portal>
</Dialog.FocusScope>
</Dialog>;

Scoping

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 this lets you render only Dialog.Trigger in that subtree, since Dialog is not cheap to render and carries a lot of functionality.

Here’s the basic anatomy of using scope and placing your Dialog higher up for performance:

_layout.tsx

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Dialog.tsx (registry item "dialog")
// npm dependencies: @tamagui/core @tamagui/dialog
import { Dialog } from "../components/tamagui/Dialog"; // 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>
{/* the rest of your app, note that it's inside of Dialog */}
{children}
</Dialog>;

UserProfile.tsx

export default () => (
<Dialog.Trigger scope="user-profile">
<Button>Open Profile</Button>
</Dialog.Trigger>
)

Note that the Trigger scope ties to the Dialog scope.

Dismissal behavior

By default, dialogs can be dismissed by:

  • Clicking outside the dialog content (on the overlay)
  • Pressing the Escape key
  • Clicking a Dialog.Close element
  • Modal dialogs (modal={true}, which is the default):
    • In v1, have disableOutsidePointerEvents set to true by default
    • Still dismiss on outside click, but prevent interaction with elements behind the dialog
    • Prevent right-click dismissal (right-clicks on the overlay are ignored)
  • Non-modal dialogs (modal={false}):
    • Allow interaction with elements behind the dialog
    • Dismiss on any outside click
    • Do not trap focus
    • Do not enable RemoveScroll while open in v3

In v3, Dialog parts own their presence lifecycle. Overlay and content can finish exit animations before unmounting, and onTransition reports completion from the animated part rather than from the root alone.

Preventing outside dismissal

To prevent a dialog from closing when clicking outside:

<Dialog.Content onPointerDownOutside={(event) => { event.preventDefault() }} >
{/* Dialog contents */}
</Dialog.Content>

API reference

Dialog

Contains every component for the dialog. Beyond Tamagui Props, adds:

Props

  • children (required)

    React.ReactNode

    Must contain Dialog.Content

  • scope

    string

    Isolates this Dialog and its parts from other Dialog instances when needed.

  • open

    boolean

    Controlled open state of the dialog.

  • defaultOpen

    boolean

    Initial open state when uncontrolled.

  • onOpenChange

    (open: boolean) => void

    Called when the dialog opens or closes.

  • modal

    boolean

    Default: 

    true

    When true, traps focus, blocks outside scroll and interaction, and portals into the root of the app. When false, the dialog renders inline and is non-blocking.

  • keepChildrenMounted

    boolean

    Default: 

    false

    When true, dialog content stays mounted in the DOM even when closed. Useful for preserving state across open/close cycles or when you need faster re-opening.

  • disableRemoveScroll

    boolean

    Used to disable the automatic removal of scrolling from the page when open.

  • onAnimationComplete

    (info: { open: boolean }) => void

    Called when the dialog open or close animation completes.

  • Dialog.Trigger

    Just Tamagui Props.

    Dialog.Portal

    Renders Dialog into appropriate container. Beyond Tamagui Props, adds:

    Props

  • forceMount

    boolean

    Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.

  • unstyled

    boolean

    Removes all default Tamagui styles.

  • Dialog.Content

    Main container for Dialog content, this is where you should apply animations. Dialog.Content no longer accepts the old no-op size variant in v3.

    To sit the content a level above the page, wrap it in theme="level2" (see Surfaces and levels):

    <Dialog.Content theme="level2">{/* ... */}</Dialog.Content>

    Beyond Tamagui Props, adds:

    Props

  • forceMount

    boolean

    Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.

  • unstyled

    boolean

    Removes all default Tamagui styles.

  • disableOutsidePointerEvents

    boolean

    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.

  • onEscapeKeyDown

    (event: KeyboardEvent) => void

    Called when Escape is pressed while the content is active.

  • onInteractOutside

    (event: Event) => void

    Called when pointer or focus interaction happens outside the content. Prevent default to stop dismissal.

  • Dialog.Overlay

    Displays behind Content. Beyond Tamagui Props, adds:

    Props

  • forceMount

    boolean

    Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.

  • Dialog.Title

    Required. Can wrap in VisuallyHidden to hide.

    Defaults to H2, see Headings.

    Dialog.Description

    Required. Can wrap in VisuallyHidden to hide.

    Defaults to Paragraph, see Paragraph.

    Dialog.Close

    Closes the Dialog, accepts the same props as View. Recommended to use with your own component and asChild.

    Props

  • displayWhenAdapted

    boolean

    By default Close elements hide when Adapt is active. If set to true, they will show when adapted.

  • Dialog.FocusScope

    Provides access to the underlying FocusScope component used by Dialog for focus management. Can be used to control focus behavior from a parent component.

    Props

  • enabled

    boolean

    Default: 

    true

    Whether focus management is enabled

  • loop

    boolean

    Default: 

    false

    When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable

  • trapped

    boolean

    Default: 

    false

    When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus

  • noFocus

    boolean

    Default: 

    false

    Zero focus mode. While active, focus is allowed neither inside nor outside the scope. Web only.

  • focusOnIdle

    boolean | number

    Default: 

    true

    When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations

  • onMountAutoFocus

    (event: Event) => void

    Event handler called when auto-focusing on mount. Can be prevented

  • onUnmountAutoFocus

    (event: Event) => void

    Event handler called when auto-focusing on unmount. Can be prevented

  • Adapted Sheet

    When used with Adapt, Dialog can hand its content to a Sheet at the matching breakpoint.

    See Sheet for more props.

    Use Adapt.Contents inside Sheet.Container to insert the contents given to Dialog.Content.

    // copy-paste: these skins import from files you own. copy each registry item
    // below into your app, then adjust the relative import paths to fit.
    // components/tamagui/Dialog.tsx (registry item "dialog")
    // components/tamagui/Sheet.tsx (registry item "sheet")
    // npm dependencies: @tamagui/core @tamagui/dialog @tamagui/sheet
    import { Adapt } from 'tamagui';
    import { Dialog } from "../components/tamagui/Dialog";
    import { Sheet } from "../components/tamagui/Sheet";
    export default () => <Dialog>
    <Dialog.Trigger />
    <Dialog.Portal>
    <Dialog.Overlay />
    <Dialog.Content>
    <Dialog.Title />
    <Dialog.Description />
    <Dialog.Close />
    {/* ... */}
    </Dialog.Content>
    </Dialog.Portal>
    {/* optionally change to sheet when small screen */}
    <Adapt when="max-md" platform="touch">
    <Sheet modal dismissOnSnapToBottom>
    <Sheet.Container>
    <Sheet.Background />
    <Adapt.Contents />
    </Sheet.Container>
    <Sheet.Overlay />
    </Sheet>
    </Adapt>
    </Dialog>;

    Source

    v2-look Dialog: scrim background on the Overlay and background/border/padding/radius/shadow on the Content, over the unstyled @tamagui/ui Dialog behavior (which keeps only positioning + pointer-event bookkeeping). This is the exact source the registry ships — copy it into your app and the examples above import from your copy.

    components/tamagui/Dialog.tsx
    // styled Dialog = the unstyled @tamagui/ui Dialog behavior + the default v2-look // skin on its Overlay (scrim background) and Content (background, border, // padding, radius, shadow). The behavior frames keep only positioning + // pointer-event bookkeeping. Single skin definition; the shadcn registry item is // generated from this file. // // the opt-in `elevate` + `bordered` v2-compat variants (formerly from // ThemeableStack) live on the unstyled DialogContent frame, so // `<Dialog.Content elevate bordered>` keeps working; this skin only adds the // static background/border/padding/radius. import { createRefComponent, styled, type TamaguiElement, withStaticProperties, } from '@tamagui/core' import { Dialog as UiDialog } from '@tamagui/dialog' import type * as React from 'react' export const dialogOverlayStyles = { backgroundColor: 'background', } as const export const dialogContentStyles = { backgroundColor: 'background', borderWidth: 1, borderColor: 'border-color', padding: '4', borderRadius: '4', elevate: true, } as const export const DialogOverlay = styled(UiDialog.Overlay, { displayName: 'DialogOverlay', ...dialogOverlayStyles, }) export const DialogContent = styled(UiDialog.Content, { displayName: 'DialogContent', ...dialogContentStyles, }) // `withStaticProperties` assigns onto the component it is given, so composing the // styled parts straight onto UiDialog would rewrite @tamagui/ui's own // Dialog.Overlay/.Content for every consumer of the unstyled package — the styled // layer would leak into the unstyled one. Compose onto a fresh root and carry the // behavior parts this skin does not restyle. const DialogRoot = createRefComponent< TamaguiElement, React.ComponentProps<typeof UiDialog> >(function Dialog(props, ref) { return <UiDialog {...props} ref={ref} /> }) export const Dialog = withStaticProperties(DialogRoot, { Trigger: UiDialog.Trigger, Portal: UiDialog.Portal, Title: UiDialog.Title, Description: UiDialog.Description, Close: UiDialog.Close, FocusScope: UiDialog.FocusScope, Adapt: UiDialog.Adapt, Overlay: DialogOverlay, Content: DialogContent, })

    Dependencies

    yarn add @tamagui/core @tamagui/dialog

    Expects theme tokens: background, border-color. Native: requires a Portal provider at the app root for the dialog to mount above content

    Need raw behavior without any skin? tamagui/unstyled re-exports the @tamagui/ui primitives (advanced).