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

GuidancePractices
DoChoose the right purpose: info for dismissable content, form to prevent accidental backdrop dismissal, required when the user must respond.
DoInclude a clear title in the header so users immediately understand what the dialog is asking.
DoUse purpose="form" for dialogs with inputs so the user can't accidentally lose data by clicking the backdrop.
DoKeep dialogs focused on a single task — if the content grows beyond what fits, consider a full page instead.
Don'tUse a dialog for simple messages that could be shown inline or as a toast notification.
Don'tNest dialogs inside other dialogs — restructure the flow into steps within a single dialog instead.
Don'tUse the fullscreen variant for simple confirmations — it is meant for complex content like editors or long forms.

Anatomy

ElementDescription
HeaderrequiredTitle, optional subtitle, and close button. The title receives focus on open for accessibility.
BodyrequiredThe main content area — text, forms, lists, or any layout.
FooterAction buttons like Save/Cancel or Accept/Decline, aligned to the end.
BackdroprequiredSemi-transparent overlay behind the dialog that blocks page interaction.

Import

ts
import {XDSDialog} from '@xds/core/Dialog'

Props

PropTypeDescription
isOpenrequired
booleanWhether the dialog is open (required).
onOpenChangerequired
(isOpen: boolean) => unknownCallback when dialog visibility changes (required).
childrenrequired
ReactNodeDialog 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
XDSDialogPositionStatic 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.
PropTypeDescription
isOpenrequired
booleanWhether the dialog is open (required).
onOpenChangerequired
(isOpen: boolean) => unknownCallback when dialog visibility changes (required).
childrenrequired
ReactNodeDialog 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
XDSDialogPositionStatic 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.
PropTypeDescription
title
stringDialog title (receives focus on open).
subtitle
stringSubtitle below the title.
onOpenChange
(isOpen: boolean) => unknownClose button callback (no button if omitted).
startContent
ReactNodeContent before the title (e.g., a back button).
endContent
ReactNodeContent 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.
PropTypeDescription
show
(content: ReactNode, options?: DialogOptions) => voidShow the dialog with the given content. Options are the same as XDSDialog props minus isOpen/onOpenChange/children.
hide
() => voidHide the dialog.
isOpen
booleanWhether the dialog is currently open.
element
ReactNodeThe 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 (
<XDSLayout
header={
<XDSDialogHeader
title="Delete project?"
onOpenChange={() => onClose()}
/>
}
content={
<XDSLayoutContent>
<XDSText type="body">
This will permanently delete &quot;Marketing Dashboard&quot; and all
of 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 (
<>
<XDSDialog
isOpen
isInline
onOpenChange={() => {}}
width={400}
purpose="form">
<Content
onClose={() => 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 (
<XDSLayout
header={
<XDSDialogHeader
title="Edit profile"
subtitle="Update your display name and bio"
onOpenChange={() => onClose()}
/>
}
content={
<XDSLayoutContent>
<XDSVStack gap={4}>
<XDSTextInput
label="Display name"
value={name}
onChange={setName}
placeholder="Enter your name"
/>
<XDSTextArea
label="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 (
<>
<XDSDialog
isOpen
isInline
onOpenChange={() => {}}
purpose="form"
width={480}>
<Content
onClose={() => 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 &amp; Documentation
</XDSText>
<XDSText type="supporting" color="secondary">
5 articles · Last updated Apr 2026
</XDSText>
</XDSVStack>
<XDSButton
label="Open documentation"
variant="secondary"
onClick={() => setIsOpen(true)}
/>
</XDSVStack>
<XDSDialog isOpen={isOpen} onOpenChange={setIsOpen} variant="fullscreen">
<XDSLayout
header={
<XDSDialogHeader
title="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">
<XDSButton
label="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 (
<XDSLayout
header={
<XDSDialogHeader
title="Transfer project ownership"
subtitle="This action requires confirmation from the new owner"
/>
}
content={
<XDSLayoutContent>
<XDSText type="body">
You are about to transfer &quot;Marketing Dashboard&quot; to Sarah
Chen. 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">
<Content
onClose={() => 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 (
<XDSLayout
header={
<XDSDialogHeader
title="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}>
<Content
onClose={() => 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={() => {}}>
<XDSLayout
header={<XDSDialogHeader title="Modal Title" onOpenChange={() => {}} />}
content={
<XDSLayoutContent>
<XDSText type="body">Dialog content goes here.</XDSText>
</XDSLayoutContent>
}
/>
</XDSDialog>
);
}