Latticecomponents

Dialog

Modal surface primitive that owns open state, focus trapping, layered dismissal, and presence motion while you own the visuals.

@lattice-ui/react-dialogStable directionimport Dialogdepends on runtime, focus, layer, motion

Dialog is the primitive for any surface that should take over the screen: confirmations, forms, settings panels, and store windows. It coordinates open state, focus, dismissal, and exit motion so your component only has to render the frame and its contents.

Reach for Dialog when a surface needs to be modal (block interaction behind it), restore focus to whatever opened it, and dismiss predictably — by an explicit close, an outside interaction, or a controlled state change.

Preview

The component running live in the browser — the same @rbxts/react tree Roblox renders, fully interactive.

Edit

Import

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

Anatomy

Compose the full set of parts. Trigger, Overlay, and Close are optional depending on how you drive the dialog, but Root, Portal, and Content form the minimum useful surface.

Dialog anatomy

Dialog anatomy
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content />
</Dialog.Portal>
</Dialog.Root>
PartRequiredResponsibility
Dialog.RootyesOwns open state and shares it with every part through context.
Dialog.TriggernoA button that opens the dialog and is the default focus-restore target.
Dialog.PortalyesRenders the surface into a ScreenGui outside the local tree.
Dialog.OverlaynoA full-screen backdrop behind the content that closes the dialog on press.
Dialog.ContentyesThe focus-trapped, dismissable surface.
Dialog.ClosenoA button that closes the dialog from inside the content.

Examples

Basic usage

Uncontrolled state seeded with defaultOpen, a trigger that opens it, a dimmed overlay, and a close button inside the content.

Dialog.Overlay covers the screen and dismisses the dialog on press whether or not you style it — but it draws nothing until you give it a color, so pass BackgroundColor3 and BackgroundTransparency for a visible dim.

QuestDialog.tsx
import { Dialog } from "@lattice-ui/react-dialog";
export function QuestDialog() {
return (
<Dialog.Root defaultOpen={false}>
<Dialog.Trigger asChild>
<textbutton Size={UDim2.fromOffset(140, 38)} Text="View quest" />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay
BackgroundColor3={Color3.fromRGB(0, 0, 0)}
BackgroundTransparency={0.5}
/>
<Dialog.Content>
<frame
AnchorPoint={new Vector2(0.5, 0.5)}
BackgroundColor3={Color3.fromRGB(24, 26, 32)}
Position={UDim2.fromScale(0.5, 0.5)}
Size={UDim2.fromOffset(320, 180)}
>
<uilistlayout Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder} />
<uipadding PaddingLeft={new UDim(0, 16)} PaddingTop={new UDim(0, 16)} />
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(288, 40)}
Text="Retrieve the lost relic from the sunken vault"
TextColor3={Color3.fromRGB(240, 244, 250)}
TextWrapped={true}
TextXAlignment={Enum.TextXAlignment.Left}
/>
<Dialog.Close asChild>
<textbutton
BackgroundColor3={Color3.fromRGB(88, 142, 255)}
Size={UDim2.fromOffset(100, 34)}
Text="Done"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
</Dialog.Close>
</frame>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

Controlled confirmation from a game event

Pass open and onOpenChange when something other than a trigger drives the dialog — a server event, a touched part, a timer. There is no Dialog.Trigger here, which changes focus restoration: a trigger focuses itself the moment it opens the dialog, so the focus scope has a reliable snapshot to restore. Opened from a game event, there is usually nothing meaningful focused at open time, so opt out with restoreFocus={false} (or focus a specific element yourself when the dialog closes).

Note that an outside press still closes the dialog through onOpenChange, so treat open becoming false — not just the buttons — as the “dismissed” signal.

TradeRequestDialog.tsx
import { useEffect, useState } from "@rbxts/react";
import { Dialog } from "@lattice-ui/react-dialog";
export function TradeRequestDialog(props: {
requestFrom?: string;
onRespond: (accepted: boolean) => void;
}) {
const [open, setOpen] = useState(false);
useEffect(() => {
if (props.requestFrom !== undefined) {
setOpen(true);
}
}, [props.requestFrom]);
const respond = (accepted: boolean) => {
props.onRespond(accepted);
setOpen(false);
};
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Portal>
<Dialog.Overlay
BackgroundColor3={Color3.fromRGB(0, 0, 0)}
BackgroundTransparency={0.5}
/>
<Dialog.Content restoreFocus={false}>
<frame
AnchorPoint={new Vector2(0.5, 0.5)}
BackgroundColor3={Color3.fromRGB(24, 26, 32)}
Position={UDim2.fromScale(0.5, 0.5)}
Size={UDim2.fromOffset(300, 150)}
>
<uilistlayout Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder} />
<uipadding PaddingLeft={new UDim(0, 16)} PaddingTop={new UDim(0, 16)} />
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(268, 24)}
Text={`${props.requestFrom} wants to trade`}
TextColor3={Color3.fromRGB(240, 244, 250)}
TextXAlignment={Enum.TextXAlignment.Left}
/>
<textbutton
BackgroundColor3={Color3.fromRGB(88, 142, 255)}
Event={{ Activated: () => respond(true) }}
Size={UDim2.fromOffset(120, 32)}
Text="Accept"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
<textbutton
BackgroundColor3={Color3.fromRGB(59, 66, 84)}
Event={{ Activated: () => respond(false) }}
Size={UDim2.fromOffset(120, 32)}
Text="Decline"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
</frame>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

Custom overlay and content transition

Use asChild on Dialog.Overlay when you need a different element class for the backdrop, and pass transition on Dialog.Content for the entrance. Two things to design around: the overlay runs no motion of its own and owns no color, so both the dim and any fade are yours; and it closes the dialog on press, so keep the child an activatable button class. The content’s transition is used as-is (see Motion and presence) — here a taller 24px rise.

StoreDialog.tsx
import { Dialog } from "@lattice-ui/react-dialog";
import { motionTargets, type PresenceMotionConfig } from "@lattice-ui/react-motion";
const STORE_REVEAL: PresenceMotionConfig = {
target: motionTargets.offsetWrapper("store reveal"),
initial: { Position: UDim2.fromOffset(0, 24) },
reveal: {
values: { Position: UDim2.fromOffset(0, 0) },
intent: { duration: 0.2, tempo: "swift", tone: "calm" },
},
exit: {
values: { Position: UDim2.fromOffset(0, 24) },
intent: { duration: 0.16, tempo: "swift", tone: "calm" },
},
};
export function StoreDialog() {
return (
<Dialog.Root defaultOpen={false}>
<Dialog.Trigger asChild>
<textbutton Size={UDim2.fromOffset(140, 38)} Text="Open store" />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay asChild>
<textbutton
AutoButtonColor={false}
BackgroundColor3={Color3.fromRGB(16, 10, 32)}
BorderSizePixel={0}
Size={UDim2.fromScale(1, 1)}
Text=""
/>
</Dialog.Overlay>
<Dialog.Content transition={STORE_REVEAL}>
<frame
AnchorPoint={new Vector2(0.5, 0.5)}
BackgroundColor3={Color3.fromRGB(24, 26, 32)}
Position={UDim2.fromScale(0.5, 0.5)}
Size={UDim2.fromOffset(420, 280)}
>
<uicorner CornerRadius={new UDim(0, 10)} />
<textlabel
BackgroundTransparency={1}
Position={UDim2.fromOffset(16, 12)}
Size={UDim2.fromOffset(388, 28)}
Text="Item shop"
TextColor3={Color3.fromRGB(240, 244, 250)}
TextXAlignment={Enum.TextXAlignment.Left}
/>
<Dialog.Close asChild>
<textbutton
AnchorPoint={new Vector2(1, 0)}
Position={new UDim2(1, -12, 0, 12)}
Size={UDim2.fromOffset(28, 28)}
Text="X"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
</Dialog.Close>
</frame>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

Non-modal panel that vetoes dismissal

modal={false} removes the full-screen input sink, so the game world and other UI behind the panel stay interactive. Outside presses still route to the dialog and would dismiss it — modality controls blocking, not dismissal — so a persistent panel also has to veto the close by calling event.preventDefault() in onInteractOutside. Dropping trapFocus lets gamepad selection leave the panel too. The result closes only through its own button.

CraftingPanel.tsx
import { useState } from "@rbxts/react";
import { Dialog } from "@lattice-ui/react-dialog";
export function CraftingPanel() {
const [open, setOpen] = useState(false);
return (
<Dialog.Root open={open} onOpenChange={setOpen} modal={false}>
<Dialog.Trigger asChild>
<textbutton Size={UDim2.fromOffset(140, 38)} Text="Crafting" />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content
trapFocus={false}
onInteractOutside={(event) => event.preventDefault()}
>
<frame
AnchorPoint={new Vector2(1, 0.5)}
BackgroundColor3={Color3.fromRGB(24, 26, 32)}
Position={new UDim2(1, -16, 0.5, 0)}
Size={UDim2.fromOffset(260, 340)}
>
<uilistlayout Padding={new UDim(0, 10)} SortOrder={Enum.SortOrder.LayoutOrder} />
<uipadding PaddingLeft={new UDim(0, 12)} PaddingTop={new UDim(0, 12)} />
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(236, 24)}
Text="Crafting"
TextColor3={Color3.fromRGB(240, 244, 250)}
TextXAlignment={Enum.TextXAlignment.Left}
/>
<Dialog.Close asChild>
<textbutton
BackgroundColor3={Color3.fromRGB(59, 66, 84)}
Size={UDim2.fromOffset(100, 32)}
Text="Close"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
</Dialog.Close>
</frame>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

There is no overlay here on purpose: a backdrop would read as modal, and the default overlay would dismiss on press anyway.

Stacked dialogs with display order

Each Dialog.Content mounts its own layered ScreenGui whose DisplayOrder is displayOrderBase plus a global mount counter, so a later-opened dialog already lands above an earlier one at the same base. Explicit displayOrderBase bands make the ordering deterministic against your other layered UI — here the settings dialog sits in the 1000 band and its destructive confirmation in the 2000 band. Outside presses always go to the top-most open layer, so pressing the settings surface while the confirmation is up dismisses only the confirmation.

ResetSettingsDialog.tsx
import { useState } from "@rbxts/react";
import { Dialog } from "@lattice-ui/react-dialog";
export function ResetSettingsDialog() {
const [settingsOpen, setSettingsOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
return (
<Dialog.Root open={settingsOpen} onOpenChange={setSettingsOpen}>
<Dialog.Trigger asChild>
<textbutton Size={UDim2.fromOffset(140, 38)} Text="Settings" />
</Dialog.Trigger>
<Dialog.Portal displayOrderBase={1000}>
<Dialog.Overlay
BackgroundColor3={Color3.fromRGB(0, 0, 0)}
BackgroundTransparency={0.5}
/>
<Dialog.Content>
<frame
AnchorPoint={new Vector2(0.5, 0.5)}
BackgroundColor3={Color3.fromRGB(24, 26, 32)}
Position={UDim2.fromScale(0.5, 0.5)}
Size={UDim2.fromOffset(360, 220)}
>
<uilistlayout Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder} />
<uipadding PaddingLeft={new UDim(0, 16)} PaddingTop={new UDim(0, 16)} />
<textbutton
BackgroundColor3={Color3.fromRGB(120, 52, 52)}
Event={{ Activated: () => setConfirmOpen(true) }}
Size={UDim2.fromOffset(160, 34)}
Text="Reset progress"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
<Dialog.Close asChild>
<textbutton
BackgroundColor3={Color3.fromRGB(59, 66, 84)}
Size={UDim2.fromOffset(100, 34)}
Text="Close"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
</Dialog.Close>
</frame>
</Dialog.Content>
</Dialog.Portal>
<Dialog.Root open={confirmOpen} onOpenChange={setConfirmOpen}>
<Dialog.Portal displayOrderBase={2000}>
<Dialog.Overlay
BackgroundColor3={Color3.fromRGB(0, 0, 0)}
BackgroundTransparency={0.5}
/>
<Dialog.Content>
<frame
AnchorPoint={new Vector2(0.5, 0.5)}
BackgroundColor3={Color3.fromRGB(24, 26, 32)}
Position={UDim2.fromScale(0.5, 0.5)}
Size={UDim2.fromOffset(280, 140)}
>
<uilistlayout Padding={new UDim(0, 10)} SortOrder={Enum.SortOrder.LayoutOrder} />
<uipadding PaddingLeft={new UDim(0, 14)} PaddingTop={new UDim(0, 14)} />
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(252, 24)}
Text="Reset all progress? This cannot be undone."
TextColor3={Color3.fromRGB(240, 244, 250)}
TextWrapped={true}
TextXAlignment={Enum.TextXAlignment.Left}
/>
<textbutton
BackgroundColor3={Color3.fromRGB(120, 52, 52)}
Event={{
Activated: () => {
setConfirmOpen(false);
setSettingsOpen(false);
},
}}
Size={UDim2.fromOffset(100, 32)}
Text="Reset"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
</frame>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</Dialog.Root>
);
}

The confirmation is a controlled sibling dialog rather than a nested Dialog.Trigger, so it survives its own open/close cycle without being tied to the settings content tree.

Exit animation with forceMount

Without forceMount, Dialog.Content sits inside a presence wrapper that unmounts it after the exit motion reports completion, with a short fallback window as a safety net — long, expressive exits risk being cut off by that fallback. forceMount bypasses the presence wrapper entirely: the content stays mounted while closed (hidden once the exit finishes), the full exit intent always plays, and child state such as scroll position or TextBox contents survives between opens.

MatchResultsDialog.tsx
import { Dialog } from "@lattice-ui/react-dialog";
import { motionTargets, type PresenceMotionConfig } from "@lattice-ui/react-motion";
const RESULTS_TRANSITION: PresenceMotionConfig = {
target: motionTargets.offsetWrapper("results reveal"),
initial: { Position: UDim2.fromOffset(0, 16) },
reveal: {
values: { Position: UDim2.fromOffset(0, 0) },
intent: { duration: 0.35, tempo: "gentle", tone: "expressive" },
},
exit: {
values: { Position: UDim2.fromOffset(0, 16) },
intent: { duration: 0.28, tempo: "gentle", tone: "expressive" },
},
};
export function MatchResultsDialog(props: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
return (
<Dialog.Root open={props.open} onOpenChange={props.onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay
BackgroundColor3={Color3.fromRGB(0, 0, 0)}
BackgroundTransparency={0.5}
/>
<Dialog.Content forceMount transition={RESULTS_TRANSITION} restoreFocus={false}>
<frame
AnchorPoint={new Vector2(0.5, 0.5)}
BackgroundColor3={Color3.fromRGB(24, 26, 32)}
Position={UDim2.fromScale(0.5, 0.5)}
Size={UDim2.fromOffset(340, 200)}
>
<uilistlayout Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder} />
<uipadding PaddingLeft={new UDim(0, 16)} PaddingTop={new UDim(0, 16)} />
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(308, 32)}
Text="Victory!"
TextColor3={Color3.fromRGB(240, 244, 250)}
TextSize={24}
TextXAlignment={Enum.TextXAlignment.Left}
/>
<Dialog.Close asChild>
<textbutton
BackgroundColor3={Color3.fromRGB(88, 142, 255)}
Size={UDim2.fromOffset(120, 34)}
Text="Continue"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
</Dialog.Close>
</frame>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

A force-mounted dialog that has never opened renders with its exit values pre-applied, so it stays invisible until the first reveal.

How it behaves

Open state

Dialog.Root is controllable. Pass open and onOpenChange to control it, or defaultOpen to run uncontrolled (defaulting to closed). Every path to a state change — trigger activation, close button, overlay press, outside-press dismissal — goes through the same setOpen, so onOpenChange sees all of them and controlled and uncontrolled usage behave identically.

Focus and selection

Dialog.Content wraps its children in a focus scope. By default it traps focus (trapFocus defaults to true) so gamepad and selection movement stay inside the surface while it is open, and restores focus (restoreFocus defaults to true) to whatever was focused just before it opened.

Dialog.Trigger is what makes restoration reliable: it registers itself as a focus node and focuses itself in the same activation that opens the dialog, so the scope’s restore snapshot points at the trigger. When you open a dialog without a trigger — controlled state driven by a game event — there may be nothing focused at open time; either pass restoreFocus={false} or move focus yourself after closing.

Dismissal and layering

Dialog.Content registers on a global dismissable-layer stack. An outside pointer press (mouse button or touch, ignoring input the engine already processed) is routed to the top-most open layer only: onPointerDownOutside fires first, then onInteractOutside for the same press, and then the dialog closes unless either handler called event.preventDefault(). This is how stacked dialogs behave sanely — a press on a lower dialog dismisses only the top one.

“Outside” is measured against the first direct host element you render inside Dialog.Content (your panel frame); additional top-level host children also count as inside. Presses within those bounds never trigger dismissal. Under asChild the same rule applies one level down, against the first host child of the element you supplied.

modal (default true) controls blocking, not dismissal: when modal, a full-screen input sink behind the content swallows interaction with everything underneath. With modal={false} the world stays interactive, but outside presses still dismiss unless you veto them.

Motion and presence

Dialog.Content renders your children inside a full-screen Frame, but runs no motion of its own. Pass a transition to animate it; the config you pass is used as-is, with nothing underneath to merge with. What the transition can move is the host — animate Position and the whole surface slides:

Opting into the reveal
import { motionTargets, type PresenceMotionConfig } from "@lattice-ui/react-motion";
const RISE: PresenceMotionConfig = {
target: motionTargets.offsetWrapper("dialog rise"),
initial: { Position: UDim2.fromOffset(0, 8) },
reveal: {
values: { Position: UDim2.fromOffset(0, 0) },
intent: { duration: 0.12, tempo: "swift", tone: "calm" },
},
exit: {
values: { Position: UDim2.fromOffset(0, 8) },
intent: { duration: 0.096, tempo: "swift", tone: "calm" },
},
};
<Dialog.Content transition={RISE}>
{/* your panel */}
</Dialog.Content>

Fading a dialog

The default host is a plain Frame spanning the whole layer, and a Frame has no property that fades its descendants — BackgroundTransparency on the host fades the host’s own background, which covers the screen. Animating it through transition fills the screen with a rectangle instead of fading your panel. So either keep the content transition to Position and put the fade where the pixels are:

  • Fade the dim. createOverlayFadeRecipe() on an element inside Dialog.Overlay covers most of what a dialog reveal reads as.
  • Fade your own elements. Animate BackgroundTransparency on your panel frame and TextTransparency / ImageTransparency on the children that need it, driven by the same open state you pass to Dialog.Root. Keep those durations at or under the content transition’s exit duration, or presence unmounts the tree before your fade finishes.

— or hand the dialog a canvasgroup with asChild. Your element becomes the motion host, so createCanvasGroupRevealRecipe() fades the whole subtree as one composited layer, the way the primitive used to before it stopped rendering a CanvasGroup for every dialog:

Opting into a whole-surface fade
import { createCanvasGroupRevealRecipe } from "@lattice-ui/react-motion";
<Dialog.Content asChild transition={createCanvasGroupRevealRecipe()}>
<canvasgroup>
<frame
AnchorPoint={new Vector2(0.5, 0.5)}
BackgroundColor3={Color3.fromRGB(24, 26, 32)}
Position={UDim2.fromScale(0.5, 0.5)}
Size={UDim2.fromOffset(320, 180)}
/>
</canvasgroup>
</Dialog.Content>

That buys an offscreen render target the size of the screen, which is exactly why it is opt-in rather than the default.

Presence timing is independent of that: with or without a transition, the content stays mounted until its exit resolves, so an exit animation is never cut off. forceMount on either part keeps it mounted while closed and lets long exits run outside the presence wrapper’s bounded unmount window.

Dialog.Overlay exposes no transition prop — an unstyled overlay has nothing to fade. It still owns presence timing. To animate a dim, render an element inside the overlay (or pass asChild) and animate that element yourself; createOverlayFadeRecipe() describes the fade the primitive used to run.

API reference

Dialog.Root

PropTypeDescription
openbooleanControlled open state. Pair with onOpenChange.
defaultOpenbooleanInitial open state for uncontrolled usage. Defaults to false.
onOpenChange(open: boolean) => voidCalled whenever the open state changes, including outside-press and overlay dismissal.
modalbooleanWhen true, mounts a full-screen input sink that blocks interaction behind the dialog. Outside presses dismiss in both modes; veto them with the outside-interaction callbacks. Defaults to true.
childrenReact.ReactNodeThe dialog parts.

Dialog.Trigger

PropTypeDescription
asChildbooleanMerge trigger behavior onto the single child element instead of the textbutton the part renders. The child must be an activatable button.
disabledbooleanPrevents the trigger from opening the dialog and removes it from focus registration.
childrenReact.ReactElementThe element to render. Required when asChild is set.

Dialog.Portal

PropTypeDescription
containerBasePlayerGuiTarget PlayerGui to render the surface into. Defaults to the app-level portal provider's container.
displayOrderBasenumberBase DisplayOrder band for the generated ScreenGui; the layer renders at this base plus a global mount counter. Defaults to 1000.
childrenReact.ReactNodeOverlay and content parts.

Dialog.Overlay

Renders a TextButton. Unknown props forward onto it and are type-checked against it, so a prop TextButton does not accept is a compile error. The primitive owns Visible, Active, Selectable and Size from presence and full-screen hit-testing, so values you pass for those are ignored.

PropTypeDescription
asChildbooleanMerge overlay behavior onto the single child element instead of the textbutton the part renders. Use an activatable button class so press-to-close keeps working.
forceMountbooleanKeeps the overlay mounted while closed and through its exit, bypassing the presence wrapper.
childrenReact.ReactNodeRendered inside the overlay. With asChild, the single element the behavior merges onto instead.
…TextButton propsPartial<WritableInstanceProperties<TextButton>>Forwarded onto the rendered textbutton and type-checked against it. Pass BackgroundColor3 and BackgroundTransparency here for a visible dim. Visible and Active are owned by the primitive.

Dialog.Content

Renders a Frame. Unknown props forward onto it and are type-checked against it, so a prop Frame does not accept is a compile error. The primitive owns Visible and Size from presence and full-screen layer geometry, so values you pass for those are ignored.

PropTypeDescription
asChildbooleanMake the single child element the motion host instead of the frame the part renders. Pass a canvasgroup here to fade the whole surface as one layer. The outside-press boundary becomes that element's first host child.
trapFocusbooleanTraps focus and selection inside the content while open. Defaults to true.
restoreFocusbooleanRestores focus to the previously focused element on close — the trigger, when one opened the dialog. Defaults to true.
forceMountbooleanBypasses the presence wrapper: the content stays mounted while closed, long exits run to completion, and child state persists between opens.
transitionPresenceMotionConfigReveal/exit motion. Used as-is — there is no default underneath, so a partial config animates only the steps it defines. Omit it for no animation.
onPointerDownOutside(event: LayerInteractEvent) => voidCalled when a pointer press lands outside the content, before dismissal. Call event.preventDefault() to keep the dialog open.
onInteractOutside(event: LayerInteractEvent) => voidCalled after onPointerDownOutside for the same outside interaction, before dismissal. Call event.preventDefault() to keep the dialog open.
childrenReact.ReactNodeThe surface contents. The first direct host element is the outside-press hit-test boundary.
…Frame propsPartial<WritableInstanceProperties<Frame>>Forwarded onto the rendered frame — or onto your asChild element — and type-checked against Frame. The host spans the layer, so styling it paints the full screen; style your panel child instead. A transition owns Position while it runs.

Dialog.Close

PropTypeDescription
asChildbooleanMerge close behavior onto the single child element instead of the textbutton the part renders. The child must be an activatable button.
childrenReact.ReactElementThe element to render. Required when asChild is set.