@jobber/components 8.24.1 → 8.24.2-visual-reg-5479aeb.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/ProgressIndicator.d.ts +1 -0
  2. package/{DescriptionList.js → ProgressIndicator.js} +3 -3
  3. package/dist/AtlantisThemeContext/AtlantisThemeContext.d.ts +1 -1
  4. package/dist/AtlantisThemeContext/index.cjs +1 -2
  5. package/dist/AtlantisThemeContext/index.mjs +1 -2
  6. package/dist/AtlantisThemeContext/themeStore.d.ts +11 -0
  7. package/dist/AtlantisThemeContext/types.d.ts +14 -0
  8. package/dist/AtlantisThemeContext/updateTheme.d.ts +1 -1
  9. package/dist/AtlantisThemeContext-cjs.js +58 -17
  10. package/dist/AtlantisThemeContext-es.js +60 -20
  11. package/dist/BottomSheet-cjs.js +8 -7
  12. package/dist/BottomSheet-es.js +9 -8
  13. package/dist/ComboboxChipRemove-es.js +2 -2
  14. package/dist/DatePicker-es.js +1 -1
  15. package/dist/Dialog/Dialog.d.ts +47 -3
  16. package/dist/Dialog/Dialog.types.d.ts +1 -15
  17. package/dist/Dialog/DialogBottomSheet.d.ts +12 -13
  18. package/dist/Dialog/DialogModal.d.ts +11 -11
  19. package/dist/Dialog/dialogComposableShared.d.ts +13 -3
  20. package/dist/DrawerDescription-cjs.js +4 -97
  21. package/dist/DrawerDescription-es.js +6 -98
  22. package/dist/InputNumberExperimental-cjs.js +21 -25
  23. package/dist/InputNumberExperimental-es.js +22 -26
  24. package/dist/MenuSubmenuTrigger-es.js +3 -3
  25. package/dist/Modal/index.cjs +12 -2
  26. package/dist/Modal/index.mjs +13 -3
  27. package/dist/ProgressBar/ProgressBar.d.ts +7 -0
  28. package/dist/ProgressBar-cjs.js +4 -0
  29. package/dist/ProgressBar-es.js +4 -0
  30. package/dist/ProgressIndicator/ProgressIndicator.d.ts +60 -0
  31. package/dist/ProgressIndicator/index.cjs +9 -0
  32. package/dist/ProgressIndicator/index.d.ts +1 -0
  33. package/dist/ProgressIndicator/index.mjs +3 -0
  34. package/dist/ProgressIndicator-cjs.js +43 -0
  35. package/dist/ProgressIndicator-es.js +41 -0
  36. package/dist/ScrollAreaViewport-cjs.js +2122 -12
  37. package/dist/ScrollAreaViewport-es.js +2118 -13
  38. package/dist/dialogReturnFocus-cjs.js +61 -34
  39. package/dist/dialogReturnFocus-es.js +63 -36
  40. package/dist/docs/AtlantisThemeContext/AtlantisThemeContext.md +41 -0
  41. package/dist/docs/DataTable/DataTable.md +0 -4
  42. package/dist/docs/Dialog/Dialog.md +1006 -0
  43. package/dist/docs/Icon/Icon.md +60 -53
  44. package/dist/docs/InputNumberExperimental/InputNumberExperimental.md +578 -0
  45. package/dist/docs/ProgressBar/ProgressBar.md +9 -0
  46. package/dist/docs/ProgressIndicator/ProgressIndicator.md +105 -0
  47. package/dist/docs/Table/Table.md +1 -3
  48. package/dist/docs/index.md +3 -1
  49. package/dist/index.cjs +3 -4
  50. package/dist/index.d.mts +1 -1
  51. package/dist/index.d.ts +1 -1
  52. package/dist/index.mjs +2 -3
  53. package/dist/isTypedArray-es.js +1 -1
  54. package/dist/primitives/InputNumberExperimental/InputNumberExperimental.d.ts +31 -15
  55. package/dist/primitives/InputNumberExperimental/types.d.ts +0 -1
  56. package/dist/styles.css +172 -51
  57. package/dist/unstyledPrimitives/index.cjs +218 -2230
  58. package/dist/unstyledPrimitives/index.mjs +224 -2236
  59. package/dist/useButton-es.js +1 -1
  60. package/dist/useScrollLock-es.js +2 -2
  61. package/dist/useScrollToActive-es.js +1 -1
  62. package/dist/utils/meta/meta.json +1 -1
  63. package/package.json +7 -7
  64. package/DescriptionList.d.ts +0 -1
  65. package/dist/DescriptionList/DescriptionList.d.ts +0 -11
  66. package/dist/DescriptionList/index.cjs +0 -10
  67. package/dist/DescriptionList/index.d.ts +0 -1
  68. package/dist/DescriptionList/index.mjs +0 -4
  69. package/dist/DescriptionList-cjs.js +0 -14
  70. package/dist/DescriptionList-es.js +0 -12
  71. package/dist/docs/DescriptionList/DescriptionList.md +0 -25
  72. package/dist/updateTheme-cjs.js +0 -20
  73. package/dist/updateTheme-es.js +0 -18
@@ -0,0 +1,1006 @@
1
+ # Dialog
2
+
3
+ ## Summary
4
+
5
+ Dialog is a modal surface that presents a single, focused task or decision above
6
+ the page. On large screens it appears as a centered modal; on small screens it
7
+ appears as a sheet anchored to the bottom of the screen.
8
+
9
+ ## Anatomy
10
+
11
+ | Part | Description |
12
+ | ---------- | ------------------------------------------------------------------- |
13
+ | Trigger | An interactive element that opens the dialog |
14
+ | Content | The floating panel (modal) or sheet that contains the dialog |
15
+ | Header | Layout slot at the top, commonly holding the title and close button |
16
+ | Title | Primary heading of the dialog, wired to the accessible name |
17
+ | Close | The dismiss control that closes the dialog |
18
+ | Body | The main content region, and the default scroll area |
19
+ | Footer | Layout slot at the bottom, commonly holding the actions |
20
+ | Actions | Optional: the primary, secondary, and tertiary action button layout |
21
+ | ScrollArea | Optional: advanced override for which region scrolls versus pins |
22
+
23
+ ## Behavior
24
+
25
+ #### Opening and closing
26
+
27
+ The dialog opens when:
28
+
29
+ * The trigger is clicked or tapped
30
+ * The trigger is activated by keyboard (e.g. via the Enter or Space keys)
31
+
32
+ The dialog closes when:
33
+
34
+ * The close button is selected
35
+ * An action that resolves the task is selected
36
+ * A click or tap occurs outside the dialog (on the backdrop)
37
+ * The ESC key is pressed
38
+
39
+ An `alertdialog` is the exception: it ignores outside and backdrop dismissal, so
40
+ the user must choose an action to resolve it. ESC still closes it.
41
+
42
+ #### Scrolling
43
+
44
+ The `Dialog.Body` is the default scroll region, while the header and footer stay
45
+ pinned as the content scrolls. Which region scrolls is decided by composition,
46
+ not a prop — wrap parts in a `Dialog.ScrollArea` when a header or footer should
47
+ scroll with the content instead of pinning.
48
+
49
+ #### Nested dialog
50
+
51
+ A dialog can open a second dialog, which layers on top of the first. The
52
+ underlying dialog stays in place behind the new overlay and returns when the top
53
+ one closes.
54
+
55
+ #### Small screens
56
+
57
+ When a user is on a small screen such as a mobile device, the dialog opens as a
58
+ sheet anchored to the bottom of the screen. The parts and accessibility wiring
59
+ are identical to the desktop modal.
60
+
61
+ #### Native mobile
62
+
63
+ Atlantis does not provide a native `Dialog` equivalent in
64
+ `@jobber/components-native`. For a comparable mobile-native experience, use
65
+ [`ContentOverlay`](/components/ContentOverlay) from `@jobber/components-native`
66
+ to present content or actions in a sheet.
67
+
68
+ ## Variants
69
+
70
+ #### Type
71
+
72
+ **1. Dialog:** The default. Used for most tasks and content. Can be dismissed by
73
+ clicking outside, pressing ESC, or the close button.
74
+
75
+ **2. Alert dialog:** Used to interrupt the user and require a decision, such as
76
+ confirming a destructive action. It is always modal, cannot be dismissed by
77
+ clicking outside, and has no close button — the user must choose an action.
78
+
79
+ #### Size
80
+
81
+ | Size | Max width | Use for |
82
+ | ----------- | --------- | ------------------------------------------------------------------------- |
83
+ | Small | 440px | Short confirmations or simple choices |
84
+ | Base | 720px | The default, suitable for most content |
85
+ | Large | 1032px | Denser content or forms |
86
+ | Full screen | Viewport | Fills the viewport on large screens; a full-height sheet on small screens |
87
+
88
+ Widths are max widths — the dialog narrows to fit smaller viewports.
89
+
90
+ #### Actions
91
+
92
+ `Dialog.Actions` arranges the action buttons: a tertiary/destructive action on
93
+ the left, and primary and secondary actions grouped on the right. The footer is
94
+ a generic region, so it can also hold a note, a checkbox, or a custom layout
95
+ instead.
96
+
97
+ #### Custom content
98
+
99
+ * The body supports flexible content layouts
100
+ * `fullBleed` removes body padding when content should reach the edges
101
+ * Header and footer are layout-only slots; they own arrangement, not typography
102
+ or actions
103
+
104
+ ## Content Guidelines
105
+
106
+ These build on the general [Voice & tone](../voice-and-tone/voice-and-tone.md) and
107
+ [Formatting](../formatting/formatting.md) guidelines.
108
+
109
+ A dialog interrupts the user, so every word should earn its place:
110
+ name the task, state the outcome, and give a clear way out.
111
+
112
+ #### Title
113
+
114
+ The title names the task as opposed to the component. Use verbs when the
115
+ dialog is asking the user to actually do something. Be specific. Use a noun
116
+ when you're presenting information. Don't phrase confirmation requests as questions.
117
+
118
+ | Do | Don't |
119
+ | ------------------ | -------------------------------------------- |
120
+ | Delete client | Are you sure you want to delete this client? |
121
+ | Edit job details | Job Editor |
122
+ | Assign team member | Assign |
123
+ | Send invoice | Send |
124
+ | Client information | Info |
125
+
126
+ #### Body
127
+
128
+ Body copy needs to expand on the title by providing what the title can't. It shouldn't
129
+ repeat the title and should provide useful information such as explaining the impact of
130
+ any action the user might be taking. For a routine task, one short sentence is often enough.
131
+
132
+ | Do | Don't |
133
+ | ---------------------------------------------- | -------------------------------------------------------------- |
134
+ | This will remove the discount from the invoice | This will let you remove the discount that's currently applied |
135
+
136
+ #### Actions
137
+
138
+ Primary action lables will need to lead with a verb and match the action taking place.
139
+ They shouldn't be generic such as "Confirm" or "Yes". The action needs to be clearly
140
+ intentional. Also, remember that buttons are title case.
141
+
142
+ | Do | Don't |
143
+ | ------------------- | ------- |
144
+ | Cancel Subscription | Confirm |
145
+ | Sync QuickBooks | Next |
146
+ | Delete 3 Files | Delete |
147
+
148
+ #### Alert dialogs
149
+
150
+ As `alertdialog` can't be dismissed by clicking outside, the way out needs to be clear. We should not be reliant on
151
+ users pressing ESC. When relevant, cancel actions need to be considered alonsgside confirmation actions and
152
+ should be just as easy to find. Ideally via secondary CTAs.
153
+
154
+ ## Do's and Don'ts
155
+
156
+ Here are some general rules to follow when working with the dialog component:
157
+
158
+ #### Do:
159
+
160
+ * ✅ Give every dialog a clear title that names the task
161
+ * ✅ Keep a dialog focused on a single task or decision
162
+ * ✅ Use an alert dialog for destructive or irreversible actions, and always
163
+ offer a way to cancel
164
+ * ✅ Lead with a short, action-oriented primary button label
165
+ * ✅ Choose the size that comfortably fits the content
166
+
167
+ #### Don't:
168
+
169
+ * ❌ Combine multiple unrelated tasks in one dialog
170
+ * ❌ Use a dialog for long or multi-step flows that belong on their own page
171
+ * ❌ Use a nested dialog unless the flow genuinely requires it
172
+ * ❌ Remove the ability to dismiss without offering an explicit action to leave
173
+
174
+ ## Accessibility
175
+
176
+ Users need to be able to open the dialog, operate its content, and close it with
177
+ assistive technology.
178
+
179
+ #### Semantics
180
+
181
+ * The surface `role` is `dialog`, or `alertdialog` for the alert type
182
+ * `Dialog.Title` is auto-wired to `aria-labelledby`
183
+ * `Dialog.Body` is auto-wired to `aria-describedby`
184
+
185
+ #### Focus
186
+
187
+ * Focus is trapped within the dialog while it is open
188
+ * On close, focus returns to the trigger
189
+ * When the trigger is removed while the dialog is open, focus can be directed to
190
+ the closest sensible element instead
191
+
192
+ #### Keyboard navigation
193
+
194
+ | Key | Behavior |
195
+ | -------------- | ---------------------------------------------------- |
196
+ | Tab | Moves focus between focusable elements in the dialog |
197
+ | Enter or Space | Activates the focused control |
198
+ | Esc | Closes the dialog |
199
+
200
+ ## Related components
201
+
202
+ * To trigger a single action rather than presenting a task or decision, use a
203
+ [Button](../Button/Button.md) or [IconButton](/components/IconButton)
204
+ * For a comparable overlay experience in native mobile, use
205
+ [BottomSheet](/components/BottomSheet) from `@jobber/components-native`
206
+ * To present a list of options or actions, use a [Menu](../Menu/Menu.md)
207
+
208
+
209
+ ## Component customization
210
+
211
+ ### Composable API
212
+
213
+ `Dialog` is composed from its building-block subcomponents rather than driven by
214
+ a single set of props: `Dialog.Trigger`, `Dialog.Content`, `Dialog.Header`,
215
+ `Dialog.Title`, `Dialog.Close`, `Dialog.Body`, `Dialog.Footer`,
216
+ `Dialog.Actions`, and `Dialog.ScrollArea`. This gives you control over the
217
+ dialog's layout while keeping the accessibility wiring automatic.
218
+
219
+ Here is a basic example:
220
+
221
+ ```tsx
222
+ <Dialog>
223
+ <Dialog.Trigger>
224
+ <Button label="Open" />
225
+ </Dialog.Trigger>
226
+ <Dialog.Content>
227
+ <Dialog.Header>
228
+ <Dialog.Title>Dialog title</Dialog.Title>
229
+ <Dialog.Close />
230
+ </Dialog.Header>
231
+ <Dialog.Body>
232
+ <Text>Body content goes here.</Text>
233
+ </Dialog.Body>
234
+ <Dialog.Footer>
235
+ <Dialog.Actions
236
+ primary={{ label: "Save", onClick: handleSave }}
237
+ secondary={{ label: "Cancel", onClick: handleCancel }}
238
+ />
239
+ </Dialog.Footer>
240
+ </Dialog.Content>
241
+ </Dialog>
242
+ ```
243
+
244
+ `Dialog.Header` and `Dialog.Footer` are layout-only slots — they own
245
+ arrangement, not typography or actions. `Dialog.Actions` is the action-button
246
+ layout (tertiary on the left; primary/secondary grouped on the right) and is
247
+ dropped inside a `Dialog.Footer`.
248
+
249
+ ### Controlled open state
250
+
251
+ By default the dialog manages its own open state — the trigger opens it and the
252
+ close/actions dismiss it, with no state to wire up. To drive it yourself (for
253
+ example, to open it from elsewhere or to gate closing on a save), pass `open`
254
+ and handle `onRequestClose`. `onRequestClose` fires whenever the user asks to
255
+ close — Escape, a backdrop click, or the dismiss button — so a controlled dialog
256
+ should update its state there. The trigger's own control still opens the dialog
257
+ via its `onClick`.
258
+
259
+ ```tsx
260
+ function ControlledDialog() {
261
+ const [open, setOpen] = useState(false);
262
+
263
+ return (
264
+ <Dialog open={open} onRequestClose={() => setOpen(false)}>
265
+ <Dialog.Trigger>
266
+ <Button label="Open" onClick={() => setOpen(true)} />
267
+ </Dialog.Trigger>
268
+ <Dialog.Content>
269
+ <Dialog.Header>
270
+ <Dialog.Title>Edit details</Dialog.Title>
271
+ <Dialog.Close />
272
+ </Dialog.Header>
273
+ <Dialog.Body>
274
+ <Text>Body content goes here.</Text>
275
+ </Dialog.Body>
276
+ <Dialog.Footer>
277
+ <Dialog.Actions
278
+ primary={{ label: "Save", onClick: () => setOpen(false) }}
279
+ secondary={{ label: "Cancel", onClick: () => setOpen(false) }}
280
+ />
281
+ </Dialog.Footer>
282
+ </Dialog.Content>
283
+ </Dialog>
284
+ );
285
+ }
286
+ ```
287
+
288
+ Because the state is yours, you can keep the dialog open when a save fails —
289
+ only call `setOpen(false)` once the action succeeds. When you don't need that
290
+ control, omit `open`/`onRequestClose` and let the dialog manage itself.
291
+
292
+ ### Trigger and close customization
293
+
294
+ `Dialog.Trigger` and `Dialog.Close` accept Atlantis-style content and can render
295
+ a default control, composed Atlantis `Button` content, or a custom element via
296
+ `render`. An Atlantis `<Button>`/`<ButtonDismiss>` is adapted automatically; any
297
+ other valid element is used as-is. When `render` is a function, it receives the
298
+ Base UI props so the element stays wired to open/close the dialog.
299
+
300
+ ```tsx
301
+ {/* default primary Button trigger / default ButtonDismiss close */}
302
+ <Dialog.Trigger>Open</Dialog.Trigger>
303
+ <Dialog.Close />
304
+
305
+ {/* custom element */}
306
+ <Dialog.Trigger render={<Button type="tertiary" label="Open" />} />
307
+ <Dialog.Close render={<Button type="tertiary" label="Done" />} />
308
+
309
+ {/* render function */}
310
+ <Dialog.Close render={closeProps => <MyClose {...closeProps} />} />
311
+ ```
312
+
313
+ ### Detached triggers
314
+
315
+ When the trigger can't be nested inside the `Dialog` — for example an action
316
+ inside a `Combobox`, `Autocomplete`, or `Menu` overlay — connect it through a
317
+ `handle` from `Dialog.createHandle()` instead of nesting. The trigger can pass
318
+ `payload` data that is available to `Dialog`'s render-function `children`.
319
+
320
+ ```tsx
321
+ const handle = Dialog.createHandle();
322
+
323
+ <Dialog handle={handle}>
324
+ {({ payload }) => <Dialog.Content>{/* read payload here */}</Dialog.Content>}
325
+ </Dialog>;
326
+
327
+ <Dialog.Trigger handle={handle} payload={selectedClient}>
328
+ Create client
329
+ </Dialog.Trigger>;
330
+ ```
331
+
332
+ ### Scroll and sticky model
333
+
334
+ Composition — not a `sticky` prop — decides what scrolls. The scroll region is a
335
+ real element and owns the scrollbar:
336
+
337
+ * Content **inside** the scroll region scrolls (the scrollbar lives here).
338
+ * Content **outside** the scroll region (a direct child of `Dialog.Content`)
339
+ stays pinned.
340
+
341
+ `Dialog.Body` is the default scroll region, so a flat `Header`/`Body`/`Footer`
342
+ pins the header and footer and scrolls the body — the common case needs nothing
343
+ extra.
344
+
345
+ ```tsx
346
+ <Dialog.Content>
347
+ <Dialog.Header /> {/* pinned */}
348
+ <Dialog.Body>…</Dialog.Body> {/* scrolls */}
349
+ <Dialog.Footer /> {/* pinned */}
350
+ </Dialog.Content>
351
+ ```
352
+
353
+ To make a header or footer scroll with the content instead of pinning, wrap the
354
+ parts that should scroll together in `Dialog.ScrollArea`. Anything inside it
355
+ scrolls; anything left as a sibling in `Dialog.Content` stays pinned.
356
+
357
+ ```tsx
358
+ {
359
+ /* sticky Header, scrolling Footer */
360
+ }
361
+ <Dialog.Content>
362
+ <Dialog.Header /> {/* pinned */}
363
+ <Dialog.ScrollArea>
364
+ <Dialog.Body>…</Dialog.Body>
365
+ <Dialog.Footer /> {/* inside → scrolls */}
366
+ </Dialog.ScrollArea>
367
+ </Dialog.Content>;
368
+ ```
369
+
370
+ The four sticky combinations map to composition:
371
+
372
+ | Sticky combination | Composition |
373
+ | ------------------------------- | ---------------------------------------------------- |
374
+ | Both sticky | `Header`, `Body`, `Footer` flat in `Content` (basic) |
375
+ | Sticky header, scrolling footer | `Header`, then `ScrollArea{ Body, Footer }` |
376
+ | Scrolling header, sticky footer | `ScrollArea{ Header, Body }`, then `Footer` |
377
+ | Nothing sticky | `ScrollArea{ Header, Body, Footer }` |
378
+
379
+ ### Rendering the surface as a form
380
+
381
+ `Dialog.Content` accepts Base UI's `render` escape hatch. Pass
382
+ `render={<form onSubmit={…} />}` to make the surface itself a `<form>`, so a
383
+ submit button in `Dialog.Footer` submits it while the header and footer stay
384
+ pinned. For two or more independent forms in one dialog, give each
385
+ `<form id="…">` its own id in the body and point each submit control at it with
386
+ the native `form` attribute instead.
387
+
388
+ ```tsx
389
+ <Dialog.Content render={<form onSubmit={handleSubmit} />}>
390
+ <Dialog.Header>…</Dialog.Header>
391
+ <Dialog.Body>{/* form fields */}</Dialog.Body>
392
+ <Dialog.Footer>
393
+ <Dialog.Actions primary={{ label: "Save", type: "submit" }} />
394
+ </Dialog.Footer>
395
+ </Dialog.Content>
396
+ ```
397
+
398
+ ### Focus return when the trigger unmounts
399
+
400
+ Base UI returns focus on close to the trigger, then the element focused before
401
+ open — each only if it is still connected to the DOM. It never walks to a
402
+ neighbor. A common Jobber flow breaks both at once: a "Create…" action opens the
403
+ dialog, and on save the trigger is replaced by a card. By the time the dialog
404
+ closes, the trigger is gone and focus falls to `document.body`.
405
+
406
+ For that case, `finalFocus` accepts a function. Build it with
407
+ `getDialogReturnFocus({ trigger, boundary })`:
408
+
409
+ ```tsx
410
+ const triggerRef = useRef<HTMLButtonElement>(null);
411
+ const boundaryRef = useRef<HTMLDivElement>(null);
412
+
413
+ <div ref={boundaryRef}>
414
+ {created ? <Card>…</Card> : <Trigger ref={triggerRef} />}
415
+ </div>;
416
+
417
+ <Dialog
418
+ handle={handle}
419
+ finalFocus={getDialogReturnFocus({
420
+ trigger: triggerRef,
421
+ boundary: boundaryRef,
422
+ })}
423
+ >
424
+
425
+ </Dialog>;
426
+ ```
427
+
428
+ Resolution order on close:
429
+
430
+ 1. `trigger`, if it is still connected (the normal case);
431
+ 2. otherwise the first focusable element inside `boundary` (typically the card
432
+ that replaced the trigger);
433
+ 3. otherwise `null`, letting Base UI fall back to its default behavior.
434
+
435
+ Keep `boundary` scoped tightly around the region that swaps. When the trigger
436
+ never unmounts, a plain `finalFocus={triggerRef}` is enough and this helper is
437
+ unnecessary.
438
+
439
+ ### Alert dialog
440
+
441
+ `type="alertdialog"` is always modal, ignores outside/backdrop dismissal, and
442
+ omits the default `Dialog.Close` button — the user must choose an action. Escape
443
+ still closes it. Always include a cancel action so there is a genuine way out.
444
+
445
+ ```tsx
446
+ <Dialog type="alertdialog">
447
+ <Dialog.Trigger>Delete client</Dialog.Trigger>
448
+ <Dialog.Content>
449
+ <Dialog.Header>
450
+ <Dialog.Title>Delete client</Dialog.Title>
451
+ </Dialog.Header>
452
+ <Dialog.Body>
453
+ <Text>
454
+ Sarah Johnson and all her history will be removed. This can't be undone.
455
+ </Text>
456
+ </Dialog.Body>
457
+ <Dialog.Footer>
458
+ <Dialog.Actions
459
+ primary={{ label: "Delete Client", onClick: handleDelete }}
460
+ secondary={{ label: "Cancel", onClick: handleCancel }}
461
+ />
462
+ </Dialog.Footer>
463
+ </Dialog.Content>
464
+ </Dialog>
465
+ ```
466
+
467
+ ## Sizing
468
+
469
+ `size` accepts `"small"` (440px), `"base"` (720px, the default), `"large"`
470
+ (1032px), and `"fullScreen"`. The pixel values are max widths; the dialog
471
+ narrows to fit smaller viewports.
472
+
473
+ Use `size="fullScreen"` for complex workflows or dense layouts that need more
474
+ space without turning the task into a new route. On desktop it fills the
475
+ viewport with squared corners; on mobile the bottom sheet becomes a full-width,
476
+ full-height surface instead of the default peek.
477
+
478
+ Use `fullBleed` on `Dialog.Body` to remove its padding when content should reach
479
+ the edges — for example a full-width image, table, or map.
480
+
481
+ ## Accessibility
482
+
483
+ **Role and modality**: `Dialog.Content` renders with `role="dialog"`, or
484
+ `role="alertdialog"` when `type="alertdialog"`. Focus is trapped within the
485
+ dialog while it is open.
486
+
487
+ **Naming**: the accessible name and description are wired automatically from the
488
+ composed parts:
489
+
490
+ * `Dialog.Title` is wired to `aria-labelledby`.
491
+ * `Dialog.Body` is wired to `aria-describedby`.
492
+
493
+ **Responsive parity**: on small screens the dialog is presented as a bottom
494
+ sheet built on the `BottomSheet`/`Drawer` primitives, but the public parts and
495
+ accessibility wiring match the desktop modal.
496
+
497
+ ### Testing tip
498
+
499
+ Query by the accessible name derived from `Dialog.Title`:
500
+
501
+ ```tsx
502
+ screen.getByRole("dialog", { name: /Dialog title/i });
503
+ ```
504
+
505
+ ## Migrating to Dialog
506
+
507
+ `Dialog` is the successor to both `Modal` (in its prop-driven and composable
508
+ `Modal.Provider` forms) and `ConfirmationModal`. This guide maps each old API
509
+ onto the composable Dialog parts.
510
+
511
+ The same target shape covers every source: a `Dialog` root that owns state and
512
+ behavior, a `Dialog.Content` surface, and the layout parts (`Header`, `Title`,
513
+ `Close`, `Body`, `Footer`, `Actions`) inside it.
514
+
515
+ ```tsx
516
+ <Dialog open={open} onRequestClose={() => setOpen(false)}>
517
+ <Dialog.Content>
518
+ <Dialog.Header>
519
+ <Dialog.Title>Title</Dialog.Title>
520
+ <Dialog.Close />
521
+ </Dialog.Header>
522
+ <Dialog.Body>…</Dialog.Body>
523
+ <Dialog.Footer>
524
+ <Dialog.Actions primary={…} secondary={…} />
525
+ </Dialog.Footer>
526
+ </Dialog.Content>
527
+ </Dialog>
528
+ ```
529
+
530
+ ### Behavior differences to know first
531
+
532
+ A few defaults changed, so a mechanical find-and-replace won't be correct on its
533
+ own:
534
+
535
+ * **Sticky is inverted.** `Modal.Header`/`Modal.Actions` were `inline` by
536
+ default and opted into `variant="sticky"`. In Dialog the header and footer are
537
+ **pinned by default**, and you opt *out* by wrapping the parts that should
538
+ scroll in `Dialog.ScrollArea`. Most `variant="sticky"` usages become the plain
539
+ default; the rare non-sticky header/footer is what now needs extra markup.
540
+ * **`dismissible={false}` becomes `type="alertdialog"`.** Blocking
541
+ outside/backdrop dismissal is no longer a boolean — it's the alert dialog
542
+ type, which also drops the default close button. Escape still closes.
543
+ * **Naming is automatic.** `ariaLabel` / `modalLabelledBy` are gone. A
544
+ `Dialog.Title` is auto-wired to `aria-labelledby` and `Dialog.Body` to
545
+ `aria-describedby`. If you have no visible title, provide one (visually hidden
546
+ if needed) rather than an `ariaLabel` prop.
547
+ * **Sizes shifted.** See the size table at the end.
548
+
549
+ ***
550
+
551
+ ## Prop-driven Modal → Dialog
552
+
553
+ The legacy `<Modal>` is controlled by `open` and renders its title and actions
554
+ from props. Move the title into `Dialog.Header`/`Dialog.Title`, the children
555
+ into `Dialog.Body`, and the `*Action` props into `Dialog.Actions`. Keep
556
+ controlling `open`/`onRequestClose` exactly as before — a trigger is optional.
557
+
558
+ ```tsx
559
+ // Before
560
+ <Modal
561
+ open={open}
562
+ onRequestClose={() => setOpen(false)}
563
+ title="Edit client"
564
+ size="large"
565
+ primaryAction={{ label: "Save", onClick: handleSave }}
566
+ secondaryAction={{ label: "Cancel", onClick: () => setOpen(false) }}
567
+ >
568
+ <Content>{/* form */}</Content>
569
+ </Modal>
570
+ ```
571
+
572
+ ```tsx
573
+ // After
574
+ <Dialog open={open} onRequestClose={() => setOpen(false)} size="large">
575
+ <Dialog.Content>
576
+ <Dialog.Header>
577
+ <Dialog.Title>Edit client</Dialog.Title>
578
+ <Dialog.Close />
579
+ </Dialog.Header>
580
+ <Dialog.Body>{/* form */}</Dialog.Body>
581
+ <Dialog.Footer>
582
+ <Dialog.Actions
583
+ primary={{ label: "Save", onClick: handleSave }}
584
+ secondary={{ label: "Cancel", onClick: () => setOpen(false) }}
585
+ />
586
+ </Dialog.Footer>
587
+ </Dialog.Content>
588
+ </Dialog>
589
+ ```
590
+
591
+ | Modal prop | Dialog equivalent |
592
+ | --------------------- | ------------------------------------------------------------ |
593
+ | `open` | `open` (unchanged) |
594
+ | `onRequestClose` | `onRequestClose` (now also receives Base UI event details) |
595
+ | `title` | `<Dialog.Title>` inside `<Dialog.Header>` |
596
+ | `children` | `<Dialog.Body>` |
597
+ | `primaryAction` | `<Dialog.Actions primary={…} />` |
598
+ | `secondaryAction` | `<Dialog.Actions secondary={…} />` |
599
+ | `tertiaryAction` | `<Dialog.Actions tertiary={…} />` (rendered left) |
600
+ | `size` | `size` (remap values — see table) |
601
+ | `dismissible={false}` | `type="alertdialog"` (also removes the default close button) |
602
+ | `ariaLabel` | Use a `Dialog.Title`; naming is auto-wired |
603
+ | `version` | Removed |
604
+
605
+ The default close button lives in the header as `<Dialog.Close />`, so add one
606
+ there instead of relying on the modal's built-in dismiss. If you were opening
607
+ the modal from a button, you can keep that button external (controlled `open`)
608
+ or wrap it in `Dialog.Trigger` and drop the manual `setOpen(true)`.
609
+
610
+ ***
611
+
612
+ ## Composable Modal (`Modal.Provider`) → Dialog
613
+
614
+ The Provider API already composes parts, so the migration is close to a rename —
615
+ with the sticky inversion being the one real change.
616
+
617
+ ```tsx
618
+ // Before
619
+ <Modal.Provider open={open} onRequestClose={() => setOpen(false)}>
620
+ <Modal.Activator>
621
+ <Button label="Open" onClick={() => setOpen(true)} />
622
+ </Modal.Activator>
623
+ <Modal.Content>
624
+ <Modal.Header title="Billing settings" variant="sticky" />
625
+ <Content>{/* body */}</Content>
626
+ <Modal.Actions
627
+ variant="sticky"
628
+ primary={{ label: "Save", onClick: handleSave }}
629
+ secondary={{ label: "Cancel", onClick: () => setOpen(false) }}
630
+ />
631
+ </Modal.Content>
632
+ </Modal.Provider>
633
+ ```
634
+
635
+ ```tsx
636
+ // After
637
+ <Dialog open={open} onRequestClose={() => setOpen(false)}>
638
+ <Dialog.Trigger>
639
+ <Button label="Open" onClick={() => setOpen(true)} />
640
+ </Dialog.Trigger>
641
+ <Dialog.Content>
642
+ <Dialog.Header>
643
+ <Dialog.Title>Billing settings</Dialog.Title>
644
+ <Dialog.Close />
645
+ </Dialog.Header>
646
+ <Dialog.Body>{/* body */}</Dialog.Body>
647
+ <Dialog.Footer>
648
+ <Dialog.Actions
649
+ primary={{ label: "Save", onClick: handleSave }}
650
+ secondary={{ label: "Cancel", onClick: () => setOpen(false) }}
651
+ />
652
+ </Dialog.Footer>
653
+ </Dialog.Content>
654
+ </Dialog>
655
+ ```
656
+
657
+ | `Modal.Provider` part | Dialog equivalent |
658
+ | ------------------------------------- | --------------------------------------------------- |
659
+ | `Modal.Provider` | `Dialog` (`open`, `onRequestClose`) |
660
+ | `Modal.Activator` | `Dialog.Trigger` |
661
+ | `Modal.Content` (outer) | `Dialog.Content` |
662
+ | `Modal.Header title="…"` | `Dialog.Header` + `Dialog.Title` (+ `Dialog.Close`) |
663
+ | `Modal.Header` with children | `Dialog.Header` with children |
664
+ | inner `Content` / body children | `Dialog.Body` |
665
+ | `Modal.Actions` | `Dialog.Footer` + `Dialog.Actions` |
666
+ | `variant="sticky"` (header/actions) | Default — flat parts pin automatically |
667
+ | `variant="inline"` (scroll with body) | Wrap the part in `Dialog.ScrollArea` |
668
+ | `dismissible={false}` | `type="alertdialog"` |
669
+ | `ariaLabel` / `modalLabelledBy` | `Dialog.Title` auto-wires `aria-labelledby` |
670
+
671
+ `Modal.Activator` also controlled focus return. That's its own migration — see
672
+ [Focus return](#focus-return-modalactivator--finalfocus) below.
673
+
674
+ ***
675
+
676
+ ## Focus return (`Modal.Activator`) → `finalFocus`
677
+
678
+ `Modal.Activator` wrapped the element that focus should return to after close.
679
+ Under the hood floating-ui also gave you a **fallback for free**: it anchored a
680
+ hidden `<span>` next to the activator, so if the element that opened the modal
681
+ unmounted while it was open — the classic "Create…" action whose trigger is
682
+ replaced by a card on save — focus still landed at a sensible nearby spot
683
+ instead of falling to `document.body`.
684
+
685
+ Base UI's dialog dropped that anchored-span mechanism, so Dialog reintroduces
686
+ the fallback explicitly through the root's `finalFocus`. There are two cases:
687
+
688
+ **The trigger stays mounted** (most dialogs). Point `finalFocus` at a ref and
689
+ you're done — this covers the plain `Modal.Activator` redirect:
690
+
691
+ ```tsx
692
+ // Before
693
+ <Modal.Provider open={open} onRequestClose={() => setOpen(false)}>
694
+ <Modal.Content>…</Modal.Content>
695
+ <Modal.Activator>
696
+ <InputText
697
+ placeholder="Focus returns here"
698
+ value={value}
699
+ onChange={setValue}
700
+ />
701
+ </Modal.Activator>
702
+ </Modal.Provider>
703
+ ```
704
+
705
+ ```tsx
706
+ // After
707
+ const returnRef = useRef<HTMLInputElement>(null);
708
+
709
+ <Dialog
710
+ open={open}
711
+ onRequestClose={() => setOpen(false)}
712
+ finalFocus={returnRef}
713
+ >
714
+ <Dialog.Content>…</Dialog.Content>
715
+ </Dialog>;
716
+
717
+ <InputText
718
+ ref={returnRef}
719
+ placeholder="Focus returns here"
720
+ value={value}
721
+ onChange={setValue}
722
+ />;
723
+ ```
724
+
725
+ **The trigger can unmount** (the fallback floating-ui used to handle). Build
726
+ `finalFocus` with `getDialogReturnFocus({ trigger, boundary })`. The `boundary`
727
+ ref is the equivalent of the old anchored span: if the trigger is gone on close,
728
+ focus moves to the first focusable element inside `boundary` instead:
729
+
730
+ ```tsx
731
+ const triggerRef = useRef<HTMLButtonElement>(null);
732
+ const boundaryRef = useRef<HTMLDivElement>(null);
733
+
734
+ <div ref={boundaryRef}>
735
+ {created ? <Card>…</Card> : <Trigger ref={triggerRef} />}
736
+ </div>;
737
+
738
+ <Dialog
739
+ handle={handle}
740
+ finalFocus={getDialogReturnFocus({
741
+ trigger: triggerRef,
742
+ boundary: boundaryRef,
743
+ })}
744
+ >
745
+
746
+ </Dialog>;
747
+ ```
748
+
749
+ Resolution order on close: the `trigger` if still connected, otherwise the first
750
+ focusable inside `boundary`, otherwise `null` (Base UI's default). Keep
751
+ `boundary` scoped tightly around the region that swaps.
752
+
753
+ | Before (`Modal.Activator`) | After (`finalFocus`) |
754
+ | ------------------------------------------- | ----------------------------------------------------------- |
755
+ | Wrap the return target in `Modal.Activator` | `finalFocus={ref}` pointing at the return target |
756
+ | Floating-ui's automatic unmount fallback | `finalFocus={getDialogReturnFocus({ trigger, boundary })}` |
757
+ | (no equivalent — was implicit) | `boundary` names where focus lands when the trigger is gone |
758
+
759
+ See the focus-return section of the component notes for the full behavior.
760
+
761
+ ***
762
+
763
+ ## ConfirmationModal → Dialog
764
+
765
+ A confirmation is an alert dialog: `type="alertdialog"` blocks outside dismissal
766
+ and omits the close button, so the user must choose confirm or cancel. Map
767
+ `confirmLabel`/`onConfirm` to the primary action and `cancelLabel`/`onCancel` to
768
+ the secondary.
769
+
770
+ ```tsx
771
+ // Before
772
+ <ConfirmationModal
773
+ open={open}
774
+ title="Delete job #2121"
775
+ message="Deleting this job will remove all associated visits and invoices."
776
+ confirmLabel="Delete Job"
777
+ cancelLabel="Cancel"
778
+ variation="destructive"
779
+ onConfirm={handleDelete}
780
+ onCancel={() => setOpen(false)}
781
+ />
782
+ ```
783
+
784
+ ```tsx
785
+ // After
786
+ <Dialog
787
+ type="alertdialog"
788
+ size="small"
789
+ open={open}
790
+ onRequestClose={() => setOpen(false)}
791
+ >
792
+ <Dialog.Content>
793
+ <Dialog.Header>
794
+ <Dialog.Title>Delete job #2121</Dialog.Title>
795
+ </Dialog.Header>
796
+ <Dialog.Body style={{ paddingTop: "var(--space-base)" }}>
797
+ <Text>
798
+ Deleting this job will remove all associated visits and invoices.
799
+ </Text>
800
+ </Dialog.Body>
801
+ <Dialog.Footer>
802
+ <Dialog.Actions
803
+ primary={{
804
+ label: "Delete Job",
805
+ onClick: handleDelete,
806
+ variation: "destructive",
807
+ }}
808
+ secondary={{ label: "Cancel", onClick: () => setOpen(false) }}
809
+ />
810
+ </Dialog.Footer>
811
+ </Dialog.Content>
812
+ </Dialog>
813
+ ```
814
+
815
+ **Match the body spacing.** The old `ConfirmationModal` rendered its message
816
+ with `16px` (`--space-base`) of space below the title. `Dialog.Body` doesn't add
817
+ that gap by default, so add `16px` of top padding to the body to keep the same
818
+ spacing:
819
+
820
+ ```tsx
821
+ <Dialog.Body style={{ paddingTop: "var(--space-base)" }}>…</Dialog.Body>
822
+ ```
823
+
824
+ | ConfirmationModal prop | Dialog equivalent |
825
+ | ---------------------------------- | ------------------------------------------------ |
826
+ | `open` | `open` |
827
+ | `onCancel` / dismiss | `onRequestClose` (+ the secondary/cancel action) |
828
+ | `title` | `<Dialog.Title>` |
829
+ | `message` | `<Dialog.Body><Text>…</Text></Dialog.Body>` |
830
+ | `children` (in place of `message`) | `<Dialog.Body>` |
831
+ | `confirmLabel` + `onConfirm` | `Dialog.Actions primary={{ label, onClick }}` |
832
+ | `cancelLabel` + `onCancel` | `Dialog.Actions secondary={{ label, onClick }}` |
833
+ | `variation="destructive"` | `primary={{ …, variation: "destructive" }}` |
834
+ | `variation="work"` | Default primary button |
835
+ | `size="small" \| "large"` | `size="small"` / `size="base"` |
836
+ | (implicit alert behavior) | `type="alertdialog"` |
837
+
838
+ ### The imperative `ref.show()` pattern
839
+
840
+ `ConfirmationModal` exposed a `confirmationModalRef` with a `show({ … })` method
841
+ so one modal could be reused across a list of items. Dialog replaces that with a
842
+ detached trigger connected through a handle, passing the per-item data as
843
+ `payload`:
844
+
845
+ ```tsx
846
+ const handle = Dialog.createHandle();
847
+
848
+ // One Dialog, rendered once
849
+ <Dialog handle={handle} type="alertdialog" size="small">
850
+ {({ payload }) => (
851
+ <Dialog.Content>
852
+ <Dialog.Header>
853
+ <Dialog.Title>Remove {payload?.name}</Dialog.Title>
854
+ </Dialog.Header>
855
+ <Dialog.Body>
856
+ <Text>{payload?.name} will be removed from your client list.</Text>
857
+ </Dialog.Body>
858
+ <Dialog.Footer>
859
+ <Dialog.Actions
860
+ primary={{
861
+ label: "Remove Client",
862
+ variation: "destructive",
863
+ onClick: () => payload?.onConfirm(),
864
+ }}
865
+ secondary={{ label: "Cancel" }}
866
+ />
867
+ </Dialog.Footer>
868
+ </Dialog.Content>
869
+ )}
870
+ </Dialog>;
871
+
872
+ // A trigger per row, each passing its own payload
873
+ {
874
+ clients.map(client => (
875
+ <Dialog.Trigger key={client.id} handle={handle} payload={client}>
876
+ <Button label="Remove" />
877
+ </Dialog.Trigger>
878
+ ));
879
+ }
880
+ ```
881
+
882
+ If you'd rather not use a handle, hold the active item in state and drive a
883
+ single controlled `Dialog` — open it when a row's button is clicked and read the
884
+ stored item in the body.
885
+
886
+ ***
887
+
888
+ ## Size mapping
889
+
890
+ Dialog widths are max widths; the dialog narrows to fit smaller viewports.
891
+
892
+ | Source | Old width | Use in Dialog |
893
+ | -------------------------------- | --------- | ----------------------- |
894
+ | Modal default | 600px | `size="base"` (720px) |
895
+ | Modal `size="small"` | 400px | `size="small"` (440px) |
896
+ | Modal `size="large"` | 940px | `size="large"` (1032px) |
897
+ | Modal `size="fullScreen"` | 100dvw | `size="fullScreen"` |
898
+ | ConfirmationModal `size="small"` | — | `size="small"` |
899
+ | ConfirmationModal `size="large"` | — | `size="base"` |
900
+
901
+ Widths aren't pixel-identical to the old components — pick the closest size and
902
+ verify the result rather than expecting an exact match.
903
+
904
+
905
+ ## Props
906
+
907
+ ### Web
908
+
909
+ #### Dialog
910
+
911
+ | Prop | Type | Required | Default | Description |
912
+ |------|------|----------|---------|-------------|
913
+ | `children` | `ReactNode | ((state: { payload: Payload; }) => ReactNode)` | No | — | The content of the dialog. This can be a regular React node or a render function that receives the payload of the act... |
914
+ | `defaultTriggerId` | `string` | No | — | ID of the trigger that the dialog is associated with. This is useful in conjunction with the `defaultOpen` prop to cr... |
915
+ | `finalFocus` | `boolean | RefObject<HTMLElement> | ((closeType: InteractionType) => boolean | void | HTMLElement)` | No | — | @see {@link https://base-ui.com/react/components/dialog#DialogPopup-finalFocus} |
916
+ | `handle` | `DialogHandle<Payload>` | No | — | A handle to associate the dialog with a trigger. If specified, allows external triggers to control the dialog's open ... |
917
+ | `onOpenChange` | `(open: boolean, eventDetails: DialogRootChangeEventDetails) => void` | No | — | Event handler called when the dialog is opened or closed. |
918
+ | `onOpenChangeComplete` | `(open: boolean) => void` | No | — | Event handler called after any animations complete when the dialog is opened or closed. |
919
+ | `onRequestClose` | `(eventDetails: DialogRootChangeEventDetails) => void` | No | — | Called when the user asks to close (escape, backdrop, dismiss button). Receives BaseUI's change-event details (reason... |
920
+ | `open` | `boolean` | No | — | Whether the dialog is currently open. |
921
+ | `size` | `DialogSize` | No | `base` | Width of the dialog. On mobile when this is set to "fullScreen" the bottom sheet will be full screen (100% height). |
922
+ | `triggerId` | `string` | No | — | ID of the trigger that the dialog is associated with. This is useful in conjunction with the `open` prop to create a ... |
923
+ | `type` | `DialogType` | No | `dialog` | Type of the dialog. An `alertdialog` requires the user to choose an action before it can be closed (no outside/backdr... |
924
+
925
+ #### Dialog.Actions
926
+
927
+ | Prop | Type | Required | Default | Description |
928
+ |------|------|----------|---------|-------------|
929
+ | `align` | `DialogActionsAlign` | No | `right` | Layout for the right-hand action group. |
930
+ | `className` | `string` | No | — | Class name applied to the part's root element. |
931
+ | `primary` | `{ onClick?: never; external?: never; readonly name?: string; submit: never; readonly type?: ButtonType; readonly value?: string; readonly disabled?: boolean; readonly loading?: boolean; ... 17 more ...; readonly children?: never; } | ... 35 more ... | (({ ...; } | ... 35 more ... | ReactElement<...>) & ({ ...; } | ....` | No | — | Primary action (right). |
932
+ | `secondary` | `{ onClick?: never; external?: never; readonly name?: string; submit: never; readonly type?: ButtonType; readonly value?: string; readonly disabled?: boolean; readonly loading?: boolean; ... 17 more ...; readonly children?: never; } | ... 35 more ... | (({ ...; } | ... 35 more ... | ReactElement<...>) & ({ ...; } | ....` | No | — | Secondary action (right, subtle). |
933
+ | `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
934
+ | `tertiary` | `{ onClick?: never; external?: never; readonly name?: string; submit: never; readonly type?: ButtonType; readonly value?: string; readonly disabled?: boolean; readonly loading?: boolean; ... 17 more ...; readonly children?: never; } | ... 35 more ... | (({ ...; } | ... 35 more ... | ReactElement<...>) & ({ ...; } | ....` | No | — | Tertiary/destructive action (left). |
935
+
936
+ #### Dialog.Body
937
+
938
+ | Prop | Type | Required | Default | Description |
939
+ |------|------|----------|---------|-------------|
940
+ | `children` | `ReactNode` | Yes | — | Body content; describes the dialog (wired to `aria-describedby`). |
941
+ | `className` | `string` | No | — | Class name applied to the part's root element. |
942
+ | `fullBleed` | `boolean` | No | `false` | Removes body padding |
943
+ | `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
944
+
945
+ #### Dialog.Close
946
+
947
+ | Prop | Type | Required | Default | Description |
948
+ |------|------|----------|---------|-------------|
949
+ | `children` | `ReactNode` | No | — | Optional content for the dismiss control. An Atlantis `<Button>` is adapted automatically; any other valid element is... |
950
+ | `className` | `string` | No | — | Class name applied to the part's root element. |
951
+ | `nativeButton` | `boolean` | No | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if ... |
952
+ | `render` | `DialogCloseRender` | No | — | Renders a custom dismiss element in place of the default `ButtonDismiss` Pass an element or a function that receives ... |
953
+ | `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
954
+
955
+ #### Dialog.Content
956
+
957
+ | Prop | Type | Required | Default | Description |
958
+ |------|------|----------|---------|-------------|
959
+ | `className` | `string` | No | — | Class name applied to the part's root element. |
960
+ | `render` | `ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogPopupState>` | No | — | @see {@link https://base-ui.com/react/components/dialog#DialogPopup-render} |
961
+ | `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
962
+
963
+ #### Dialog.Footer
964
+
965
+ | Prop | Type | Required | Default | Description |
966
+ |------|------|----------|---------|-------------|
967
+ | `children` | `ReactNode` | Yes | — | Footer content. Commonly a `Dialog.Actions`, but the footer is a generic region — it can hold anything (a note, a che... |
968
+ | `className` | `string` | No | — | Class name applied to the part's root element. |
969
+ | `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
970
+
971
+ #### Dialog.Header
972
+
973
+ | Prop | Type | Required | Default | Description |
974
+ |------|------|----------|---------|-------------|
975
+ | `children` | `ReactNode` | Yes | — | Header content — commonly a `Dialog.Title` and a `Dialog.Close`. The header only owns layout; it does not impose typo... |
976
+ | `className` | `string` | No | — | Class name applied to the part's root element. |
977
+ | `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
978
+
979
+ #### Dialog.ScrollArea
980
+
981
+ | Prop | Type | Required | Default | Description |
982
+ |------|------|----------|---------|-------------|
983
+ | `children` | `ReactNode` | Yes | — | Content that scrolls together — typically a `Dialog.Body` and a non-pinned `Dialog.Header`/`Dialog.Footer`. |
984
+ | `className` | `string` | No | — | Class name applied to the part's root element. |
985
+ | `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
986
+
987
+ #### Dialog.Title
988
+
989
+ | Prop | Type | Required | Default | Description |
990
+ |------|------|----------|---------|-------------|
991
+ | `children` | `ReactNode` | Yes | — | Title text. Rendered as a `Heading` and auto-wired to the dialog's `aria-labelledby`. |
992
+ | `className` | `string` | No | — | Class name applied to the part's root element. |
993
+ | `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
994
+
995
+ #### Dialog.Trigger
996
+
997
+ | Prop | Type | Required | Default | Description |
998
+ |------|------|----------|---------|-------------|
999
+ | `children` | `ReactNode` | No | — | Trigger content; an Atlantis `<Button>` or raw text. Optional content forwarded to the rendered element. |
1000
+ | `className` | `string` | No | — | Class name applied to the part's root element. |
1001
+ | `handle` | `DialogHandle<Payload>` | No | — | Links a detached trigger to a `Dialog` with the matching `handle`, when the trigger can't be nested inside the `Dialog`. |
1002
+ | `id` | `string` | No | — | ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trig... |
1003
+ | `nativeButton` | `boolean` | No | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if ... |
1004
+ | `payload` | `Payload` | No | — | Data passed to the dialog when this trigger opens it (used with `handle`). |
1005
+ | `render` | `DialogTriggerRender` | No | — | Renders a custom trigger element in place of the default `<Button>`. |
1006
+ | `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |