XDSDialog@xds/core · Dialog
Usage
Dialog displays a modal overlay that blocks interaction with the page until the user responds. Use it for delete confirmations, edit forms, terms acceptance, or any decision that should not be skipped. For cases where you want to show a dialog without managing open state, use the `useXDSImperativeDialog` hook — call `dialog.show(content)` and render `dialog.element` in your tree.Best practices
| Guidance | Practices |
|---|---|
| Do | Choose the right purpose: info for dismissable content, form to prevent accidental backdrop dismissal, required when the user must respond. |
| Do | Include a clear title in the header so users immediately understand what the dialog is asking. |
| Do | Use purpose="form" for dialogs with inputs so the user can't accidentally lose data by clicking the backdrop. |
| Do | Keep dialogs focused on a single task — if the content grows beyond what fits, consider a full page instead. |
| Don't | Use a dialog for simple messages that could be shown inline or as a toast notification. |
| Don't | Nest dialogs inside other dialogs — restructure the flow into steps within a single dialog instead. |
| Don't | Use the fullscreen variant for simple confirmations — it is meant for complex content like editors or long forms. |
Anatomy
| Element | Description | |
|---|---|---|
| Header | required | Title, optional subtitle, and close button. The title receives focus on open for accessibility. |
| Body | required | The main content area — text, forms, lists, or any layout. |
| Footer | Action buttons like Save/Cancel or Accept/Decline, aligned to the end. | |
| Backdrop | required | Semi-transparent overlay behind the dialog that blocks page interaction. |
Import
tsimport {XDSDialog} from '@xds/core/Dialog'
Props
| Prop | Type | Description |
|---|---|---|
isOpenrequired | boolean | Whether the dialog is open (required). |
onOpenChangerequired | (isOpen: boolean) => unknown | Callback when dialog visibility changes (required). |
childrenrequired | ReactNode | Dialog content. |
width | number | string (default: 400) | Width of the dialog in pixels or any CSS value. |
maxHeight | number | string (default: '75vh') | Maximum height of the dialog. |
position | XDSDialogPosition | Static position for the dialog; centered by default when omitted. |
variant | 'standard' | 'fullscreen' (default: 'standard') | Dialog variant — fullscreen expands to fill the entire viewport. |
purpose | 'required' | 'form' | 'info' (default: 'info') | Controls dismissal behavior: required disables Escape and backdrop click; form disables backdrop click after interaction; info allows both. |
isInline | boolean (default: false) | Renders dialog content inline without the <dialog> element, backdrop, or modal behavior. For documentation previews and showcases only. |
Sub-components
Dialog is a compound component with 3 sub-components.XDSDialog
Modal dialog using the native <dialog> element.| Prop | Type | Description |
|---|---|---|
isOpenrequired | boolean | Whether the dialog is open (required). |
onOpenChangerequired | (isOpen: boolean) => unknown | Callback when dialog visibility changes (required). |
childrenrequired | ReactNode | Dialog content. |
width | number | string (default: 400) | Width of the dialog in pixels or any CSS value. |
maxHeight | number | string (default: '75vh') | Maximum height of the dialog. |
position | XDSDialogPosition | Static position for the dialog; centered by default when omitted. |
variant | 'standard' | 'fullscreen' (default: 'standard') | Dialog variant — fullscreen expands to fill the entire viewport. |
purpose | 'required' | 'form' | 'info' (default: 'info') | Controls dismissal behavior: required disables Escape and backdrop click; form disables backdrop click after interaction; info allows both. |
isInline | boolean (default: false) | Renders dialog content inline without the <dialog> element, backdrop, or modal behavior. For documentation previews and showcases only. |
XDSDialogHeader
Header for dialogs with a title, optional subtitle, close button, and start/end content slots.| Prop | Type | Description |
|---|---|---|
title | string | Dialog title (receives focus on open). |
subtitle | string | Subtitle below the title. |
onOpenChange | (isOpen: boolean) => unknown | Close button callback (no button if omitted). |
startContent | ReactNode | Content before the title (e.g., a back button). |
endContent | ReactNode | Content after the title, before close button. |
hasDivider | boolean (default: true) | Adds border at the bottom edge. |
useXDSImperativeDialog
Hook for showing a dialog without managing open state. Call dialog.show(content, options) to open and dialog.hide() to close. Render dialog.element in your JSX tree.| Prop | Type | Description |
|---|---|---|
show | (content: ReactNode, options?: DialogOptions) => void | Show the dialog with the given content. Options are the same as XDSDialog props minus isOpen/onOpenChange/children. |
hide | () => void | Hide the dialog. |
isOpen | boolean | Whether the dialog is currently open. |
element | ReactNode | The dialog element — render this in your JSX tree. |
Examples
Common configurations, variations, and states.Dialog — ConfirmationAsks the user to confirm a destructive action before it happens. Use before deleting projects, removing team members, revoking API keys, or any irreversible operation.
tsx'use client';import {XDSDialog,XDSDialogHeader,useXDSImperativeDialog,} from '@xds/core/Dialog';import {XDSLayout,XDSLayoutContent,XDSLayoutFooter,XDSHStack,} from '@xds/core/Layout';import {XDSButton} from '@xds/core/Button';import {XDSText} from '@xds/core/Text';function Content({onClose}: {onClose: () => void}) {return (<XDSLayoutheader={<XDSDialogHeadertitle="Delete project?"onOpenChange={() => onClose()}/>}content={<XDSLayoutContent><XDSText type="body">This will permanently delete "Marketing Dashboard" and allof its data. This action cannot be undone.</XDSText></XDSLayoutContent>}footer={<XDSLayoutFooter><XDSHStack gap={2} hAlign="end"><XDSButton label="Cancel" variant="secondary" onClick={onClose} /><XDSButton label="Delete" variant="destructive" onClick={onClose} /></XDSHStack></XDSLayoutFooter>}/>);}// Remove isInline for production — dialogs should be modal.export default function DialogConfirmationDialog() {const dialog = useXDSImperativeDialog({width: 400, purpose: 'form'});return (<><XDSDialogisOpenisInlineonOpenChange={() => {}}width={400}purpose="form"><ContentonClose={() => dialog.show(<Content onClose={() => dialog.hide()} />)}/></XDSDialog>{dialog.element}</>);}
Dialog — FormCollects user input without navigating away from the page. Uses purpose=
tsx'use client';import {useState} from 'react';import {XDSDialog,XDSDialogHeader,useXDSImperativeDialog,} from '@xds/core/Dialog';import {XDSLayout,XDSLayoutContent,XDSLayoutFooter,XDSHStack,XDSVStack,} from '@xds/core/Layout';import {XDSButton} from '@xds/core/Button';import {XDSTextInput} from '@xds/core/TextInput';import {XDSTextArea} from '@xds/core/TextArea';function Content({onClose}: {onClose: () => void}) {const [name, setName] = useState('Ruby Cheung');const [bio, setBio] = useState('Design systems engineer');return (<XDSLayoutheader={<XDSDialogHeadertitle="Edit profile"subtitle="Update your display name and bio"onOpenChange={() => onClose()}/>}content={<XDSLayoutContent><XDSVStack gap={4}><XDSTextInputlabel="Display name"value={name}onChange={setName}placeholder="Enter your name"/><XDSTextArealabel="Bio"value={bio}onChange={setBio}placeholder="Tell us about yourself"/></XDSVStack></XDSLayoutContent>}footer={<XDSLayoutFooter><XDSHStack gap={2} hAlign="end"><XDSButton label="Cancel" variant="secondary" onClick={onClose} /><XDSButton label="Save" variant="primary" onClick={onClose} /></XDSHStack></XDSLayoutFooter>}/>);}// Remove isInline for production — dialogs should be modal.export default function DialogFormDialog() {const dialog = useXDSImperativeDialog({purpose: 'form', width: 480});return (<><XDSDialogisOpenisInlineonOpenChange={() => {}}purpose="form"width={480}><ContentonClose={() => dialog.show(<Content onClose={() => dialog.hide()} />)}/></XDSDialog>{dialog.element}</>);}
Dialog — Fullscreen
tsx'use client';import {useState} from 'react';import {XDSDialog, XDSDialogHeader} from '@xds/core/Dialog';import {XDSLayout,XDSLayoutContent,XDSLayoutFooter,XDSHStack,XDSVStack,} from '@xds/core/Layout';import {XDSButton} from '@xds/core/Button';import {XDSText} from '@xds/core/Text';import {XDSCard} from '@xds/core/Card';const SECTIONS = [{title: 'Getting started',body: 'Create your first project by clicking New Project in the sidebar. Choose a template or start from scratch.',},{title: 'Team members',body: 'Invite collaborators from Settings > Team. Each member can have Admin, Editor, or Viewer permissions.',},{title: 'Billing',body: 'Free plans include up to 3 projects. Upgrade to Pro for unlimited projects and priority support.',},{title: 'API access',body: 'Generate API keys from Settings > Developer. Rate limits are 1,000 requests per minute on free plans.',},{title: 'Data export',body: 'Export your data anytime from Settings > Data. Exports are available as CSV or JSON within 24 hours.',},];export default function DialogFullscreenDialog() {const [isOpen, setIsOpen] = useState(false);return (<XDSCard><XDSVStack gap={3}><XDSVStack gap={1}><XDSText type="body" weight="bold">Help & Documentation</XDSText><XDSText type="supporting" color="secondary">5 articles · Last updated Apr 2026</XDSText></XDSVStack><XDSButtonlabel="Open documentation"variant="secondary"onClick={() => setIsOpen(true)}/></XDSVStack><XDSDialog isOpen={isOpen} onOpenChange={setIsOpen} variant="fullscreen"><XDSLayoutheader={<XDSDialogHeadertitle="Documentation"subtitle="Everything you need to get started"onOpenChange={setIsOpen}/>}content={<XDSLayoutContent><XDSVStack gap={4}>{SECTIONS.map(({title, body}) => (<XDSVStack key={title} gap={1}><XDSText type="body" weight="bold">{title}</XDSText><XDSText type="body">{body}</XDSText></XDSVStack>))}</XDSVStack></XDSLayoutContent>}footer={<XDSLayoutFooter><XDSHStack hAlign="end"><XDSButtonlabel="Close"variant="primary"onClick={() => setIsOpen(false)}/></XDSHStack></XDSLayoutFooter>}/></XDSDialog></XDSCard>);}
Dialog — RequiredCannot be dismissed by Escape or backdrop click — the user must explicitly choose an action. Uses purpose=
tsx'use client';import {XDSDialog,XDSDialogHeader,useXDSImperativeDialog,} from '@xds/core/Dialog';import {XDSLayout,XDSLayoutContent,XDSLayoutFooter,XDSHStack,} from '@xds/core/Layout';import {XDSButton} from '@xds/core/Button';import {XDSText} from '@xds/core/Text';function Content({onClose}: {onClose: () => void}) {return (<XDSLayoutheader={<XDSDialogHeadertitle="Transfer project ownership"subtitle="This action requires confirmation from the new owner"/>}content={<XDSLayoutContent><XDSText type="body">You are about to transfer "Marketing Dashboard" to SarahChen. Once accepted, you will lose admin access.</XDSText></XDSLayoutContent>}footer={<XDSLayoutFooter><XDSHStack gap={2} hAlign="end"><XDSButton label="Cancel" variant="secondary" onClick={onClose} /><XDSButton label="Transfer" variant="primary" onClick={onClose} /></XDSHStack></XDSLayoutFooter>}/>);}// Remove isInline for production — dialogs should be modal.export default function DialogWithSubtitle() {const dialog = useXDSImperativeDialog({purpose: 'required'});return (<><XDSDialog isOpen isInline onOpenChange={() => {}} purpose="required"><ContentonClose={() => dialog.show(<Content onClose={() => dialog.hide()} />)}/></XDSDialog>{dialog.element}</>);}
Dialog — Scrollable
tsx'use client';import {XDSDialog,XDSDialogHeader,useXDSImperativeDialog,} from '@xds/core/Dialog';import {XDSLayout,XDSLayoutContent,XDSLayoutFooter,XDSHStack,XDSVStack,} from '@xds/core/Layout';import {XDSButton} from '@xds/core/Button';import {XDSText} from '@xds/core/Text';const TERMS = ['You agree to use the service only for lawful purposes and in compliance with all applicable laws and regulations in your jurisdiction.','Your account credentials are your responsibility. Notify us immediately if you suspect unauthorized access to your account.','We reserve the right to suspend accounts that violate these terms or engage in abusive behavior toward other users.','Content you upload remains your property. You grant us a license to host and display it within the service.','We may update these terms at any time. Continued use after changes constitutes acceptance of the updated terms.','The service is provided as-is without warranties of any kind. We are not liable for data loss or service interruptions.','You may cancel your account at any time. Your data will be deleted within 30 days of cancellation.','Disputes will be resolved through binding arbitration in accordance with applicable regulations.','You agree not to reverse-engineer, decompile, or disassemble any part of the service or its underlying technology.','We may collect anonymized usage data to improve the service. Personal data is handled per our Privacy Policy.','Third-party integrations are governed by their own terms. We are not responsible for third-party service outages.','You are responsible for maintaining backups of your data. We provide export tools but do not guarantee data recovery.','Commercial use requires a Business plan. Free accounts are limited to personal and non-commercial projects.','We may introduce new features or discontinue existing ones with 30 days notice via email or in-app notification.','Violation of these terms may result in immediate termination of your account without prior notice or refund.',];function Content({onClose}: {onClose: () => void}) {return (<XDSLayoutheader={<XDSDialogHeadertitle="Terms and Conditions"onOpenChange={() => onClose()}/>}content={<XDSLayoutContent><XDSVStack gap={3}>{TERMS.map((term, i) => (<XDSText type="body" key={i}>{i + 1}. {term}</XDSText>))}</XDSVStack></XDSLayoutContent>}footer={<XDSLayoutFooter><XDSHStack gap={2} hAlign="end"><XDSButton label="Decline" variant="secondary" onClick={onClose} /><XDSButton label="Accept" variant="primary" onClick={onClose} /></XDSHStack></XDSLayoutFooter>}/>);}// Remove isInline for production — dialogs should be modal.export default function DialogScrollingContent() {const dialog = useXDSImperativeDialog({maxHeight: '50vh'});return (<><XDSDialog isOpen isInline onOpenChange={() => {}} maxHeight={360}><ContentonClose={() => dialog.show(<Content onClose={() => dialog.hide()} />)}/></XDSDialog>{dialog.element}</>);}
Showcase source
tsx'use client';import {XDSDialog, XDSDialogHeader} from '@xds/core/Dialog';import {XDSLayout, XDSLayoutContent} from '@xds/core/Layout';import {XDSText} from '@xds/core/Text';// Remove isInline for production — dialogs should be modal.export default function DialogShowcase() {return (<XDSDialog isOpen isInline onOpenChange={() => {}}><XDSLayoutheader={<XDSDialogHeader title="Modal Title" onOpenChange={() => {}} />}content={<XDSLayoutContent><XDSText type="body">Dialog content goes here.</XDSText></XDSLayoutContent>}/></XDSDialog>);}