Lattice getting started

Your first dialog

Build a controlled, modal dialog step by step with the Dialog primitive.

Dialog is a good first primitive because it shows the whole Lattice UI model in one surface: a Root that owns state, parts that read from it, and an app-owned frame for the visuals. By the end of this walkthrough you will have a working confirmation dialog that opens from a button, traps focus, and closes predictably.

This page is a guided build, not a reference. For the full prop list and behavior details, see the Dialog component page.

Step 1 — Import and sketch the anatomy

Dialog is a compound component: a Root and a set of parts hanging off it. Start from the import and the shape you are going to fill in.

ConfirmDialog.tsx
import React from "@rbxts/react";
import { Dialog } from "@lattice-ui/dialog";
export function ConfirmDialog() {
return (
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content />
</Dialog.Portal>
</Dialog.Root>
);
}

Root, Portal, and Content are the minimum useful surface; Trigger, Overlay, and Close are there to drive and dress it.

Step 2 — Add a trigger

Dialog.Trigger is the button that opens the surface. Use asChild so your own textbutton becomes the trigger instead of rendering a default one — the trigger behavior merges onto the element you provide.

The trigger
<Dialog.Trigger asChild>
<textbutton
Text="Delete save"
Size={UDim2.fromOffset(140, 38)}
/>
</Dialog.Trigger>

The trigger also registers itself as the focus-restore target, so when the dialog closes, selection returns here automatically.

Step 3 — Build the surface

Dialog.Portal renders the surface into a ScreenGui above your game UI, Dialog.Overlay is the backdrop, and Dialog.Content is the focus-trapped, dismissable panel. The visuals are entirely yours — Lattice only owns the behavior.

Overlay and content
<Dialog.Portal>
<Dialog.Overlay>
<frame
BackgroundColor3={Color3.fromRGB(0, 0, 0)}
BackgroundTransparency={0.5}
Size={UDim2.fromScale(1, 1)}
/>
</Dialog.Overlay>
<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)}
>
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(280, 40)}
Text="Delete this save?"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
</frame>
</Dialog.Content>
</Dialog.Portal>

By default Dialog.Content traps focus while open, restores focus on close, and runs a canvas-group reveal/exit animation — you get all of that without configuring anything.

Step 4 — Add a close action

Put a Dialog.Close inside the content for an explicit close. Like the trigger, it accepts asChild so your own button drives it.

Close from inside
<Dialog.Close asChild>
<textbutton
Text="Cancel"
Size={UDim2.fromOffset(100, 34)}
/>
</Dialog.Close>

Because the dialog is modal by default, an outside press also dismisses it — so the close button is a convenience, not the only way out.

Step 5 — Control the open state

So far the dialog runs uncontrolled: the trigger and close button drive it through shared context. That is enough for most cases. When something outside the dialog needs to open or close it, lift the state with open and onOpenChange.

ConfirmDialog.tsx
import React, { useState } from "@rbxts/react";
import { Dialog } from "@lattice-ui/dialog";
export function ConfirmDialog() {
const [open, setOpen] = useState(false);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<textbutton Text="Delete save" Size={UDim2.fromOffset(140, 38)} />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay>
<frame
BackgroundColor3={Color3.fromRGB(0, 0, 0)}
BackgroundTransparency={0.5}
Size={UDim2.fromScale(1, 1)}
/>
</Dialog.Overlay>
<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)}
>
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(280, 40)}
Text="Delete this save?"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
<Dialog.Close asChild>
<textbutton Text="Cancel" Size={UDim2.fromOffset(100, 34)} />
</Dialog.Close>
</frame>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

Controlled and uncontrolled usage behave identically — the trigger, the close button, and an outside press all flow through the same state, so onOpenChange fires no matter how the dialog opens or closes.

Recap

  • Dialog.Root owns the open state; every part reads it through context.
  • asChild lets your own textbutton become the trigger and close button.
  • Dialog.Content traps and restores focus and animates in and out for free.
  • A modal dialog dismisses on an outside press, not just the close button.

Next step

See how this same shape repeats across every primitive in the Composition model, or read the full Dialog reference for every prop.