A high-quality, accessible dialog component for React applications, providing overlay modals, focus management, keyboard navigation, and screen reader support as part of the Radix UI primitives collection
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Main content container with focus management and accessibility components for proper dialog implementation.
Primary content container that handles focus management, keyboard navigation, and dismissal behavior.
/**
* Main content container for dialog with comprehensive accessibility features
* Handles focus trapping, keyboard navigation, and dismissal behavior
*/
type DialogContentElement = React.ComponentRef<typeof Primitive.div>;
interface DialogContentProps extends DialogContentTypeProps {
/** Force mounting when more control is needed for animations */
forceMount?: true;
}
interface DialogContentTypeProps extends Omit<DismissableLayerProps, 'onDismiss'> {
/**
* When `true`, focus cannot escape the `Content` via keyboard,
* pointer, or a programmatic focus.
* @defaultValue false (for non-modal), true (for modal)
*/
trapFocus?: boolean;
/**
* When `true`, hover/focus/click interactions will be disabled on elements outside
* the `DismissableLayer`. Users will need to click twice on outside elements to
* interact with them: once to close the `DismissableLayer`, and again to trigger the element.
*/
disableOutsidePointerEvents?: boolean;
/**
* Event handler called when auto-focusing on open.
* Can be prevented.
*/
onOpenAutoFocus?: (event: Event) => void;
/**
* Event handler called when auto-focusing on close.
* Can be prevented.
*/
onCloseAutoFocus?: (event: Event) => void;
/**
* Event handler called when the escape key is down.
* Can be prevented.
*/
onEscapeKeyDown?: (event: KeyboardEvent) => void;
/**
* Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`.
* Can be prevented.
*/
onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;
/**
* Event handler called when the focus moves outside of the `DismissableLayer`.
* Can be prevented.
*/
onFocusOutside?: (event: FocusOutsideEvent) => void;
/**
* Event handler called when an interaction happens outside the `DismissableLayer`.
* Specifically, when a `pointerdown` event happens outside or focus moves outside of it.
* Can be prevented.
*/
onInteractOutside?: (event: PointerDownOutsideEvent | FocusOutsideEvent) => void;
}
// Event types from DismissableLayer
type PointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }>;
type FocusOutsideEvent = CustomEvent<{ originalEvent: FocusEvent }>;
type DismissableLayerProps = React.ComponentPropsWithoutRef<typeof Primitive.div> & {
disableOutsidePointerEvents?: boolean;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;
onFocusOutside?: (event: FocusOutsideEvent) => void;
onInteractOutside?: (event: PointerDownOutsideEvent | FocusOutsideEvent) => void;
onDismiss?: () => void;
};
const DialogContent: React.ForwardRefExoticComponent<
DialogContentProps & React.RefAttributes<DialogContentElement>
>;Usage Examples:
import {
Dialog,
DialogTrigger,
DialogPortal,
DialogOverlay,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose
} from "@radix-ui/react-dialog";
// Basic content
function BasicContent() {
return (
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Dialog description here</DialogDescription>
<DialogClose>Close</DialogClose>
</DialogContent>
</DialogPortal>
</Dialog>
);
}
// Content with custom focus handling
function CustomFocusContent() {
const handleOpenAutoFocus = (event: Event) => {
// Prevent default auto-focus and focus specific element
event.preventDefault();
const firstInput = document.querySelector('input');
firstInput?.focus();
};
const handleCloseAutoFocus = (event: Event) => {
// Custom focus restoration
event.preventDefault();
const customButton = document.getElementById('custom-trigger');
customButton?.focus();
};
return (
<Dialog>
<DialogTrigger id="custom-trigger">Open</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent
onOpenAutoFocus={handleOpenAutoFocus}
onCloseAutoFocus={handleCloseAutoFocus}
>
<DialogTitle>Custom Focus Dialog</DialogTitle>
<input placeholder="This gets focused on open" />
<DialogClose>Close</DialogClose>
</DialogContent>
</DialogPortal>
</Dialog>
);
}
// Content with outside interaction handling
function OutsideInteractionContent() {
const handlePointerDownOutside = (event: PointerDownOutsideEvent) => {
console.log('Pointer down outside dialog');
// Prevent default to stop dialog from closing
// event.preventDefault();
};
const handleInteractOutside = (event: PointerDownOutsideEvent | FocusOutsideEvent) => {
// Handle any interaction outside the dialog
if (event.target?.closest('.keep-open-trigger')) {
event.preventDefault(); // Don't close when clicking this element
}
};
return (
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent
onPointerDownOutside={handlePointerDownOutside}
onInteractOutside={handleInteractOutside}
>
<DialogTitle>Outside Interaction Dialog</DialogTitle>
<p>Try clicking outside or pressing ESC</p>
<DialogClose>Close</DialogClose>
</DialogContent>
</DialogPortal>
</Dialog>
);
}
// Non-modal content
function NonModalContent() {
return (
<Dialog modal={false}>
<DialogTrigger>Open Non-Modal</DialogTrigger>
<DialogPortal>
<DialogContent
trapFocus={false}
disableOutsidePointerEvents={false}
>
<DialogTitle>Non-Modal Dialog</DialogTitle>
<p>Focus is not trapped, background remains interactive</p>
<DialogClose>Close</DialogClose>
</DialogContent>
</DialogPortal>
</Dialog>
);
}
// Modal content with disabled outside pointer events
function DisabledOutsidePointerEventsContent() {
return (
<Dialog>
<DialogTrigger>Open Modal</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent
trapFocus={true}
disableOutsidePointerEvents={true}
>
<DialogTitle>Modal Dialog</DialogTitle>
<p>Outside interactions require two clicks: one to close dialog, one to activate element</p>
<DialogClose>Close</DialogClose>
</DialogContent>
</DialogPortal>
</Dialog>
);
}Title component for dialog accessibility, automatically linked via ARIA attributes.
/**
* Dialog title component for proper accessibility labeling
* Automatically referenced by DialogContent via aria-labelledby
*/
type DialogTitleElement = React.ComponentRef<typeof Primitive.h2>;
interface DialogTitleProps extends React.ComponentPropsWithoutRef<typeof Primitive.h2> {}
const DialogTitle: React.ForwardRefExoticComponent<
DialogTitleProps & React.RefAttributes<DialogTitleElement>
>;Usage Examples:
// Basic title
function BasicTitle() {
return (
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent>
<DialogTitle>Confirm Delete</DialogTitle>
<p>Are you sure you want to delete this item?</p>
</DialogContent>
</DialogPortal>
</Dialog>
);
}
// Styled title
function StyledTitle() {
return (
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent>
<DialogTitle className="dialog-title" style={{ fontSize: '24px' }}>
Settings
</DialogTitle>
<p>Configure your application settings</p>
</DialogContent>
</DialogPortal>
</Dialog>
);
}
// Visually hidden title (for accessibility only)
function VisuallyHiddenTitle() {
return (
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent>
<DialogTitle className="sr-only">
Image Gallery
</DialogTitle>
{/* Visual content without visible title */}
<div className="image-gallery">
{/* Images here */}
</div>
</DialogContent>
</DialogPortal>
</Dialog>
);
}Description component for dialog accessibility, automatically linked via ARIA attributes.
/**
* Dialog description component for additional accessibility context
* Automatically referenced by DialogContent via aria-describedby
*/
type DialogDescriptionElement = React.ComponentRef<typeof Primitive.p>;
interface DialogDescriptionProps extends React.ComponentPropsWithoutRef<typeof Primitive.p> {}
const DialogDescription: React.ForwardRefExoticComponent<
DialogDescriptionProps & React.RefAttributes<DialogDescriptionElement>
>;Usage Examples:
// Basic description
function BasicDescription() {
return (
<Dialog>
<DialogTrigger>Delete Item</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent>
<DialogTitle>Confirm Deletion</DialogTitle>
<DialogDescription>
This action cannot be undone. The item will be permanently removed.
</DialogDescription>
<div className="dialog-actions">
<DialogClose>Cancel</DialogClose>
<DialogClose>Delete</DialogClose>
</div>
</DialogContent>
</DialogPortal>
</Dialog>
);
}
// Multiple descriptions
function MultipleDescriptions() {
return (
<Dialog>
<DialogTrigger>Show Details</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent>
<DialogTitle>User Profile</DialogTitle>
<DialogDescription>
View and edit your profile information below.
</DialogDescription>
<DialogDescription className="warning">
Changes will be saved automatically.
</DialogDescription>
<form>
{/* Form fields */}
</form>
</DialogContent>
</DialogPortal>
</Dialog>
);
}
// Optional description
function OptionalDescription() {
const [showDetails, setShowDetails] = React.useState(false);
return (
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent aria-describedby={showDetails ? undefined : undefined}>
<DialogTitle>Simple Dialog</DialogTitle>
{showDetails && (
<DialogDescription>
Additional details when needed
</DialogDescription>
)}
<button onClick={() => setShowDetails(!showDetails)}>
{showDetails ? 'Hide' : 'Show'} Details
</button>
</DialogContent>
</DialogPortal>
</Dialog>
);
}DialogContent automatically provides:
role="dialog" for proper semantic rolearia-labelledby referencing DialogTitle IDaria-describedby referencing DialogDescription IDdata-state attribute with "open" or "closed" valuesOn Open:
onOpenAutoFocusOn Close:
onCloseAutoFocusFor modal dialogs:
In development mode, Radix UI provides helpful warnings:
// This will warn in development
<DialogContent>
{/* Missing DialogTitle */}
<p>Content without title</p>
</DialogContent>
// This will not warn
<DialogContent aria-describedby={undefined}>
<DialogTitle>Title Present</DialogTitle>
<p>Content without description (intentional)</p>
</DialogContent>Install with Tessl CLI
npx tessl i tessl/npm-radix-ui--react-dialog