Components Overlays
Dialog
StableA modal window for focused tasks that must be finished or dismissed before returning to the page.
Use Dialog for short, self-contained tasks such as renaming a template or inviting a colleague. It traps focus, locks page scroll and returns focus to the trigger when closed. For confirming destructive actions use AlertDialog; for long or secondary content use Drawer; for non-blocking content use Popover. On phones the dialog becomes a bottom sheet automatically.
Overview
import { Button, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@bindshq/stance-react';
export function Basic() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">View deployment details</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Deployment details</DialogTitle>
<DialogDescription>
Signatures were pushed to 128 mailboxes through Exchange Online.
</DialogDescription>
</DialogHeader>
<DialogBody>
<p style={{ margin: 0 }}>
The next automatic sync runs tonight at 02:00. Mailboxes added after that will receive
the template on the following run.
</p>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button>Done</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}Light, dark and high contrast
Every Stance component is designed and tested in all three appearance modes.
Import
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose } from '@bindshq/stance-react';Platforms
Dialog has native counterparts. They share Stance tokens and intent, and use each platform’s own controls and conventions. The live previews on this page are the web implementation.
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose } from '@bindshq/stance-react';Examples
With a form
Focus moves to the first field. Submitting closes the dialog.
import * as React from 'react';
import { Button, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, FormField, Input } from '@bindshq/stance-react';
export function WithForm() {
const [open, setOpen] = React.useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Rename template</Button>
</DialogTrigger>
<DialogContent size="sm">
<form
onSubmit={(e) => {
e.preventDefault();
setOpen(false);
}}
>
<DialogHeader>
<DialogTitle>Rename template</DialogTitle>
<DialogDescription>
Names are visible to everyone in your organisation.
</DialogDescription>
</DialogHeader>
<DialogBody>
<FormField label="Template name" required>
<Input defaultValue="Sales, EMEA" />
</FormField>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button type="submit">Save</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}Sizes
sm 400px, md 520px, lg 720px, xl 960px, or full screen.
import { Button, Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@bindshq/stance-react';
export function Sizes() {
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
{(['sm', 'md', 'lg', 'xl'] as const).map((size) => (
<Dialog key={size}>
<DialogTrigger asChild>
<Button variant="outline">Open {size}</Button>
</DialogTrigger>
<DialogContent size={size}>
<DialogHeader>
<DialogTitle>Size “{size}”</DialogTitle>
</DialogHeader>
<DialogBody>Content wraps to the dialog width and scrolls when tall.</DialogBody>
</DialogContent>
</Dialog>
))}
</div>
);
}Controlled
Drive open state yourself, e.g. after an async check.
import * as React from 'react';
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@bindshq/stance-react';
export function Controlled() {
const [open, setOpen] = React.useState(false);
return (
<>
<Button variant="secondary" onClick={() => setOpen(true)}>
Open programmatically
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Session expiring</DialogTitle>
<DialogDescription>
You will be signed out in two minutes for security.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
Sign out
</Button>
<Button onClick={() => setOpen(false)}>Stay signed in</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}API reference
Dialog
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | None | Controlled open state. |
defaultOpen | boolean | false | Initial state when uncontrolled. |
onOpenChange | (open: boolean) => void | None | Called when the dialog opens or closes. |
DialogTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render your own element (e.g. a Button) as the trigger. |
DialogContent
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'md' | Maximum width. |
initialFocus | RefObject<HTMLElement> | None | Element focused on open. Defaults to the first focusable element. |
showClose | boolean | true | Show the top-right close button. |
closeLabel | string | 'Close' | Accessible label for the close button. |
dismissOnOutsideClick | boolean | true | Close when the backdrop is clicked. |
restoreFocus | boolean | true | Return focus to the trigger on close. |
onEscapeKeyDown | (event: KeyboardEvent) => void | None | Call event.preventDefault() to keep the dialog open. |
onPointerDownOutside | (event: PointerEvent) => void | None | Call event.preventDefault() to keep the dialog open. |
disablePortal | boolean | false | Render inline instead of into document.body. |
DialogTitle
| Prop | Type | Default | Description |
|---|---|---|---|
level | 1 | 2 | 3 | 4 | 2 | Heading level. The title is the dialog’s accessible name and is required. |
DialogDescription
| Prop | Type | Default | Description |
|---|---|---|---|
...props | HTMLAttributes<HTMLParagraphElement> | None | Linked to the dialog via aria-describedby. |
DialogHeader / DialogBody / DialogFooter
| Prop | Type | Default | Description |
|---|---|---|---|
...props | HTMLAttributes<HTMLDivElement> | None | Layout regions. The body scrolls when content is tall. |
DialogClose
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Wrap your own Button to close the dialog. |
Accessibility
- The surface has role="dialog" and aria-modal="true", named by DialogTitle and described by DialogDescription.
- Focus moves into the dialog on open, is trapped while open, and returns to the trigger on close.
- Page scroll is locked while open; nested overlays (menus, popovers) keep working inside the dialog.
- The trigger exposes aria-haspopup="dialog", aria-expanded and aria-controls.
- Escape always closes the top-most layer only.
Keyboard
| Keys | Action |
|---|---|
| Tab | Moves to the next focusable element inside the dialog, wrapping at the end. |
| Shift Tab | Moves to the previous focusable element, wrapping at the start. |
| Esc | Closes the dialog and returns focus to the trigger. |
Usage guidelines
Do
- Give every dialog a clear title that states the task.
- Keep dialogs short; move long flows to a page or Drawer.
- Put the primary action last in the footer.
Don’t
- Open a dialog from another dialog.
- Use a dialog for messages that need no decision; use Toast or Alert.