0
# Content and Accessibility
1
2
Main content container with focus management and accessibility components for proper dialog implementation.
3
4
## Capabilities
5
6
### DialogContent
7
8
Primary content container that handles focus management, keyboard navigation, and dismissal behavior.
9
10
```typescript { .api }
11
/**
12
* Main content container for dialog with comprehensive accessibility features
13
* Handles focus trapping, keyboard navigation, and dismissal behavior
14
*/
15
type DialogContentElement = React.ComponentRef<typeof Primitive.div>;
16
interface DialogContentProps extends DialogContentTypeProps {
17
/** Force mounting when more control is needed for animations */
18
forceMount?: true;
19
}
20
21
interface DialogContentTypeProps extends Omit<DismissableLayerProps, 'onDismiss'> {
22
/**
23
* When `true`, focus cannot escape the `Content` via keyboard,
24
* pointer, or a programmatic focus.
25
* @defaultValue false (for non-modal), true (for modal)
26
*/
27
trapFocus?: boolean;
28
29
/**
30
* When `true`, hover/focus/click interactions will be disabled on elements outside
31
* the `DismissableLayer`. Users will need to click twice on outside elements to
32
* interact with them: once to close the `DismissableLayer`, and again to trigger the element.
33
*/
34
disableOutsidePointerEvents?: boolean;
35
36
/**
37
* Event handler called when auto-focusing on open.
38
* Can be prevented.
39
*/
40
onOpenAutoFocus?: (event: Event) => void;
41
42
/**
43
* Event handler called when auto-focusing on close.
44
* Can be prevented.
45
*/
46
onCloseAutoFocus?: (event: Event) => void;
47
48
/**
49
* Event handler called when the escape key is down.
50
* Can be prevented.
51
*/
52
onEscapeKeyDown?: (event: KeyboardEvent) => void;
53
54
/**
55
* Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`.
56
* Can be prevented.
57
*/
58
onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;
59
60
/**
61
* Event handler called when the focus moves outside of the `DismissableLayer`.
62
* Can be prevented.
63
*/
64
onFocusOutside?: (event: FocusOutsideEvent) => void;
65
66
/**
67
* Event handler called when an interaction happens outside the `DismissableLayer`.
68
* Specifically, when a `pointerdown` event happens outside or focus moves outside of it.
69
* Can be prevented.
70
*/
71
onInteractOutside?: (event: PointerDownOutsideEvent | FocusOutsideEvent) => void;
72
}
73
74
// Event types from DismissableLayer
75
type PointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }>;
76
type FocusOutsideEvent = CustomEvent<{ originalEvent: FocusEvent }>;
77
78
type DismissableLayerProps = React.ComponentPropsWithoutRef<typeof Primitive.div> & {
79
disableOutsidePointerEvents?: boolean;
80
onEscapeKeyDown?: (event: KeyboardEvent) => void;
81
onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;
82
onFocusOutside?: (event: FocusOutsideEvent) => void;
83
onInteractOutside?: (event: PointerDownOutsideEvent | FocusOutsideEvent) => void;
84
onDismiss?: () => void;
85
};
86
87
const DialogContent: React.ForwardRefExoticComponent<
88
DialogContentProps & React.RefAttributes<DialogContentElement>
89
>;
90
```
91
92
**Usage Examples:**
93
94
```typescript
95
import {
96
Dialog,
97
DialogTrigger,
98
DialogPortal,
99
DialogOverlay,
100
DialogContent,
101
DialogTitle,
102
DialogDescription,
103
DialogClose
104
} from "@radix-ui/react-dialog";
105
106
// Basic content
107
function BasicContent() {
108
return (
109
<Dialog>
110
<DialogTrigger>Open</DialogTrigger>
111
<DialogPortal>
112
<DialogOverlay />
113
<DialogContent>
114
<DialogTitle>Dialog Title</DialogTitle>
115
<DialogDescription>Dialog description here</DialogDescription>
116
<DialogClose>Close</DialogClose>
117
</DialogContent>
118
</DialogPortal>
119
</Dialog>
120
);
121
}
122
123
// Content with custom focus handling
124
function CustomFocusContent() {
125
const handleOpenAutoFocus = (event: Event) => {
126
// Prevent default auto-focus and focus specific element
127
event.preventDefault();
128
const firstInput = document.querySelector('input');
129
firstInput?.focus();
130
};
131
132
const handleCloseAutoFocus = (event: Event) => {
133
// Custom focus restoration
134
event.preventDefault();
135
const customButton = document.getElementById('custom-trigger');
136
customButton?.focus();
137
};
138
139
return (
140
<Dialog>
141
<DialogTrigger id="custom-trigger">Open</DialogTrigger>
142
<DialogPortal>
143
<DialogOverlay />
144
<DialogContent
145
onOpenAutoFocus={handleOpenAutoFocus}
146
onCloseAutoFocus={handleCloseAutoFocus}
147
>
148
<DialogTitle>Custom Focus Dialog</DialogTitle>
149
<input placeholder="This gets focused on open" />
150
<DialogClose>Close</DialogClose>
151
</DialogContent>
152
</DialogPortal>
153
</Dialog>
154
);
155
}
156
157
// Content with outside interaction handling
158
function OutsideInteractionContent() {
159
const handlePointerDownOutside = (event: PointerDownOutsideEvent) => {
160
console.log('Pointer down outside dialog');
161
// Prevent default to stop dialog from closing
162
// event.preventDefault();
163
};
164
165
const handleInteractOutside = (event: PointerDownOutsideEvent | FocusOutsideEvent) => {
166
// Handle any interaction outside the dialog
167
if (event.target?.closest('.keep-open-trigger')) {
168
event.preventDefault(); // Don't close when clicking this element
169
}
170
};
171
172
return (
173
<Dialog>
174
<DialogTrigger>Open</DialogTrigger>
175
<DialogPortal>
176
<DialogOverlay />
177
<DialogContent
178
onPointerDownOutside={handlePointerDownOutside}
179
onInteractOutside={handleInteractOutside}
180
>
181
<DialogTitle>Outside Interaction Dialog</DialogTitle>
182
<p>Try clicking outside or pressing ESC</p>
183
<DialogClose>Close</DialogClose>
184
</DialogContent>
185
</DialogPortal>
186
</Dialog>
187
);
188
}
189
190
// Non-modal content
191
function NonModalContent() {
192
return (
193
<Dialog modal={false}>
194
<DialogTrigger>Open Non-Modal</DialogTrigger>
195
<DialogPortal>
196
<DialogContent
197
trapFocus={false}
198
disableOutsidePointerEvents={false}
199
>
200
<DialogTitle>Non-Modal Dialog</DialogTitle>
201
<p>Focus is not trapped, background remains interactive</p>
202
<DialogClose>Close</DialogClose>
203
</DialogContent>
204
</DialogPortal>
205
</Dialog>
206
);
207
}
208
209
// Modal content with disabled outside pointer events
210
function DisabledOutsidePointerEventsContent() {
211
return (
212
<Dialog>
213
<DialogTrigger>Open Modal</DialogTrigger>
214
<DialogPortal>
215
<DialogOverlay />
216
<DialogContent
217
trapFocus={true}
218
disableOutsidePointerEvents={true}
219
>
220
<DialogTitle>Modal Dialog</DialogTitle>
221
<p>Outside interactions require two clicks: one to close dialog, one to activate element</p>
222
<DialogClose>Close</DialogClose>
223
</DialogContent>
224
</DialogPortal>
225
</Dialog>
226
);
227
}
228
```
229
230
### DialogTitle
231
232
Title component for dialog accessibility, automatically linked via ARIA attributes.
233
234
```typescript { .api }
235
/**
236
* Dialog title component for proper accessibility labeling
237
* Automatically referenced by DialogContent via aria-labelledby
238
*/
239
type DialogTitleElement = React.ComponentRef<typeof Primitive.h2>;
240
interface DialogTitleProps extends React.ComponentPropsWithoutRef<typeof Primitive.h2> {}
241
242
const DialogTitle: React.ForwardRefExoticComponent<
243
DialogTitleProps & React.RefAttributes<DialogTitleElement>
244
>;
245
```
246
247
**Usage Examples:**
248
249
```typescript
250
// Basic title
251
function BasicTitle() {
252
return (
253
<Dialog>
254
<DialogTrigger>Open</DialogTrigger>
255
<DialogPortal>
256
<DialogOverlay />
257
<DialogContent>
258
<DialogTitle>Confirm Delete</DialogTitle>
259
<p>Are you sure you want to delete this item?</p>
260
</DialogContent>
261
</DialogPortal>
262
</Dialog>
263
);
264
}
265
266
// Styled title
267
function StyledTitle() {
268
return (
269
<Dialog>
270
<DialogTrigger>Open</DialogTrigger>
271
<DialogPortal>
272
<DialogOverlay />
273
<DialogContent>
274
<DialogTitle className="dialog-title" style={{ fontSize: '24px' }}>
275
Settings
276
</DialogTitle>
277
<p>Configure your application settings</p>
278
</DialogContent>
279
</DialogPortal>
280
</Dialog>
281
);
282
}
283
284
// Visually hidden title (for accessibility only)
285
function VisuallyHiddenTitle() {
286
return (
287
<Dialog>
288
<DialogTrigger>Open</DialogTrigger>
289
<DialogPortal>
290
<DialogOverlay />
291
<DialogContent>
292
<DialogTitle className="sr-only">
293
Image Gallery
294
</DialogTitle>
295
{/* Visual content without visible title */}
296
<div className="image-gallery">
297
{/* Images here */}
298
</div>
299
</DialogContent>
300
</DialogPortal>
301
</Dialog>
302
);
303
}
304
```
305
306
### DialogDescription
307
308
Description component for dialog accessibility, automatically linked via ARIA attributes.
309
310
```typescript { .api }
311
/**
312
* Dialog description component for additional accessibility context
313
* Automatically referenced by DialogContent via aria-describedby
314
*/
315
type DialogDescriptionElement = React.ComponentRef<typeof Primitive.p>;
316
interface DialogDescriptionProps extends React.ComponentPropsWithoutRef<typeof Primitive.p> {}
317
318
const DialogDescription: React.ForwardRefExoticComponent<
319
DialogDescriptionProps & React.RefAttributes<DialogDescriptionElement>
320
>;
321
```
322
323
**Usage Examples:**
324
325
```typescript
326
// Basic description
327
function BasicDescription() {
328
return (
329
<Dialog>
330
<DialogTrigger>Delete Item</DialogTrigger>
331
<DialogPortal>
332
<DialogOverlay />
333
<DialogContent>
334
<DialogTitle>Confirm Deletion</DialogTitle>
335
<DialogDescription>
336
This action cannot be undone. The item will be permanently removed.
337
</DialogDescription>
338
<div className="dialog-actions">
339
<DialogClose>Cancel</DialogClose>
340
<DialogClose>Delete</DialogClose>
341
</div>
342
</DialogContent>
343
</DialogPortal>
344
</Dialog>
345
);
346
}
347
348
// Multiple descriptions
349
function MultipleDescriptions() {
350
return (
351
<Dialog>
352
<DialogTrigger>Show Details</DialogTrigger>
353
<DialogPortal>
354
<DialogOverlay />
355
<DialogContent>
356
<DialogTitle>User Profile</DialogTitle>
357
<DialogDescription>
358
View and edit your profile information below.
359
</DialogDescription>
360
<DialogDescription className="warning">
361
Changes will be saved automatically.
362
</DialogDescription>
363
<form>
364
{/* Form fields */}
365
</form>
366
</DialogContent>
367
</DialogPortal>
368
</Dialog>
369
);
370
}
371
372
// Optional description
373
function OptionalDescription() {
374
const [showDetails, setShowDetails] = React.useState(false);
375
376
return (
377
<Dialog>
378
<DialogTrigger>Open</DialogTrigger>
379
<DialogPortal>
380
<DialogOverlay />
381
<DialogContent aria-describedby={showDetails ? undefined : undefined}>
382
<DialogTitle>Simple Dialog</DialogTitle>
383
{showDetails && (
384
<DialogDescription>
385
Additional details when needed
386
</DialogDescription>
387
)}
388
<button onClick={() => setShowDetails(!showDetails)}>
389
{showDetails ? 'Hide' : 'Show'} Details
390
</button>
391
</DialogContent>
392
</DialogPortal>
393
</Dialog>
394
);
395
}
396
```
397
398
## Accessibility Features
399
400
### DialogContent Accessibility
401
402
DialogContent automatically provides:
403
404
- `role="dialog"` for proper semantic role
405
- `aria-labelledby` referencing DialogTitle ID
406
- `aria-describedby` referencing DialogDescription ID
407
- `data-state` attribute with "open" or "closed" values
408
- Focus management and trapping
409
- Keyboard navigation (ESC to close)
410
411
### Focus Management
412
413
#### Auto Focus Behavior
414
415
**On Open:**
416
1. Focus moves to first focusable element in DialogContent
417
2. If no focusable elements, focus goes to DialogContent itself
418
3. Can be customized with `onOpenAutoFocus`
419
420
**On Close:**
421
1. Focus returns to DialogTrigger that opened the dialog
422
2. Can be customized with `onCloseAutoFocus`
423
424
#### Focus Trapping
425
426
For modal dialogs:
427
- Tab cycles through focusable elements within DialogContent
428
- Shift+Tab cycles backwards
429
- Focus cannot escape the dialog via keyboard
430
431
### Keyboard Navigation
432
433
- **ESC**: Closes the dialog
434
- **Tab/Shift+Tab**: Navigate focusable elements
435
- **Enter/Space**: Activate buttons and controls
436
437
### Screen Reader Support
438
439
- Dialog is announced when opened
440
- Title and description are read to users
441
- Focus changes are announced
442
- State changes (open/closed) are communicated
443
444
## Development Warnings
445
446
In development mode, Radix UI provides helpful warnings:
447
448
- Missing DialogTitle warns in console
449
- Missing DialogDescription warns if aria-describedby is not undefined
450
- These warnings help ensure accessibility compliance
451
452
```typescript
453
// This will warn in development
454
<DialogContent>
455
{/* Missing DialogTitle */}
456
<p>Content without title</p>
457
</DialogContent>
458
459
// This will not warn
460
<DialogContent aria-describedby={undefined}>
461
<DialogTitle>Title Present</DialogTitle>
462
<p>Content without description (intentional)</p>
463
</DialogContent>
464
```