Dialog

import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from '@hanzo/ui'

Default

A button opens a modal panel with a title, a description, a body and a footer; Escape, the corner X, a click outside or the Done button closes it.

export function Default() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Keyboard shortcuts</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Keyboard shortcuts</DialogTitle>
<DialogDescription>
Press ? anywhere to open this list.
</DialogDescription>
</DialogHeader>
<YStack gap="$2">
<XStack justify="space-between">
<Text>Open the command palette</Text>
<Text fontFamily="$mono">⌘ K</Text>
</XStack>
<XStack justify="space-between">
<Text>Search files</Text>
<Text fontFamily="$mono">⌘ P</Text>
</XStack>
<XStack justify="space-between">
<Text>Toggle the sidebar</Text>
<Text fontFamily="$mono">⌘ B</Text>
</XStack>
</YStack>
<DialogFooter>
<DialogClose asChild>
<Button>Done</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

Without close button

`showCloseButton={false}` removes the corner X, so the footer's Got it is the panel's only button; Escape and a click outside still close it.

export function WithoutCloseButton() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Release notes</Button>
</DialogTrigger>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Version 2.4</DialogTitle>
<DialogDescription>
Three changes, released this week.
</DialogDescription>
</DialogHeader>
<YStack gap="$2">
<Text>Threads can be pinned to the top of a channel.</Text>
<Text>Search now covers attachments.</Text>
<Text>Exports run in the background and notify you when done.</Text>
</YStack>
<DialogFooter>
<DialogClose asChild>
<Button variant="primary">Got it</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

Overlay

`overlay` takes the scrim's own props and `maxW` sets the panel's width: a faint dim and a narrow box for a one-field rename.

export function Overlay() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Rename branch</Button>
</DialogTrigger>
<DialogContent maxW={360} overlay={{ opacity: 0.2 }}>
<DialogHeader>
<DialogTitle>Rename branch</DialogTitle>
<DialogDescription>
Open pull requests follow the new name.
</DialogDescription>
</DialogHeader>
<YStack gap="$2">
<Label htmlFor="branch-name">Name</Label>
<Input id="branch-name" defaultValue="feature/search" />
</YStack>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button variant="primary">Rename</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

Controlled

`open` and `onOpenChange` hand the state to you, so Send invite can do its work and then close the panel itself.

export function Controlled() {
const [open, setOpen] = useState(false)
const [email, setEmail] = useState("")
const [sent, setSent] = useState("")
return (
<YStack gap="$3" items="flex-start">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="primary">Invite a teammate</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Invite a teammate</DialogTitle>
<DialogDescription>
They get editor access to this workspace.
</DialogDescription>
</DialogHeader>
<YStack gap="$2">
<Label htmlFor="invite-email">Email</Label>
<Input id="invite-email" placeholder="[email protected]" value={email} onChangeText={setEmail} />
</YStack>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button variant="primary" disabled={email === ""} onPress={() => { setSent(email) setEmail("") setOpen(false) }} >
Send invite
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{sent ? (
<Paragraph color="$color11">Invite sent to {sent}</Paragraph>
) : null}
</YStack>
)
}

Own overlay

`DialogPortal` and `DialogOverlay` are what Content mounts for you; mount them yourself when the scrim carries content of its own, with Content's overlay hidden through `overlay={{ display: 'none' }}`.

export function OwnOverlay() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open hero.png</Button>
</DialogTrigger>
<DialogPortal>
<DialogOverlay bg="black" opacity={0.92} justify="flex-end" items="center" pb="$6" >
<Text color="white" fontSize="$2">
3 of 12 · Esc to close
</Text>
</DialogOverlay>
<DialogContent maxW={720} showCloseButton={false} overlay={{ display: "none" }} >
<DialogHeader>
<DialogTitle>hero.png</DialogTitle>
<DialogDescription>1920 × 1080 · 412 KB</DialogDescription>
</DialogHeader>
<YStack width="100%" aspectRatio={16 / 9} rounded="$3" bg="$color3" />
<DialogFooter>
<DialogClose asChild>
<Button>Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogPortal>
</Dialog>
)
}

Types

export type DialogOverlayProps = ComponentProps<typeof GuiDialog.Overlay> & {
zIndex?: number
} & { [attr: `data-${string}`]: string | undefined }
export type DialogContentProps = ComponentProps<typeof GuiDialog.Content> & {
showCloseButton?: boolean
/**
* The overlay's own props. Content owns the overlay — that is what lets a
* caller mount Content alone — but owning it is not the same as hiding it,
* and with no way through, the dim, the stacking and the click target were
* unreachable from outside. A consumer stacking dialogs (a preview opened
* from a dialog) has no way to say which one dims which.
*
* Absent, nothing changes: the default overlay renders exactly as before.
*/
overlay?: DialogOverlayProps
}
export type DialogSectionProps = ComponentProps<typeof YStack> & { children?: ReactNode }

Exports

DialogDialogCloseDialogContentDialogDescriptionDialogFooterDialogHeaderDialogOverlayDialogPortalDialogTitleDialogTrigger