Raft 设计系统

组件 · 浮层

Dialog

A centered overlay on a scrim for a decision or a few fields people must finish before they continue, with a wide size for a large form filled in one pass.

稳定也叫 Modal
import { Dialog } from "@botiverse/raft-ui";
本页尚未翻译
译文完成之前,这里显示英文原文。 阅读英文版

A dialog is a centered overlay on a scrim for something people must finish before they continue: a decision, or a small task of a few fields, such as creating a channel. It has a title, a body and at most two buttons. Its wide size holds a large form filled in one pass. To confirm an action, use AlertDialog.

dialog/compositions/create-channel

When to use

SituationUse
A small task in 1 to 5 fields (Create Channel)Dialog
A large form filled in one pass: registering or editing an app, configuring an agentDialog with size="wide"
Task detailsA workspace overlay on Dialog, 960px wide and full screen on a phone, outside the four sizes (Tasks)
Confirming an action that cannot be undone, or that interruptsAlertDialog
An action that can be reversed in placeNo dialog: do it; when a Toast offers Undo is in Feedback
Details or settings people look at and edit for a whileDrawer

Which overlay fits which job is in Choosing a component.

Anatomy

  • Dialog: the root. Owns the open state. Required.
  • DialogTrigger: the button that opens it.
  • DialogContent: the centered surface, portaled, with the scrim (DialogOverlay) behind it. Required.
  • DialogHeader: the title row, with an optional DialogClose at its end. The × is pulled out so its icon box, not the button, ends on the dialog's 20px margin (Alignment).
  • DialogTitle: the title. Required.
  • DialogDescription: a sentence under the title or in the body.
  • DialogBody: the content. It scrolls when the dialog reaches the window height.
  • DialogFooter: the buttons, aligned to the end.
  • DialogClose: closes the dialog. Without children it is the × icon button; with render it turns a Cancel button into a close button.
  • DialogNav: the section list of a wide dialog.
  • DialogSurface: the dialog's frame without Base UI behind it, for a centered modal whose app owns the backdrop, Esc and focus, such as search.
  • DialogPortal and DialogOverlay: lower-level parts.

Variants

Pick the size by what the dialog holds, not by what looks right. The four sizes are far enough apart that each holds one kind of content (cindyz, 2026-10-21):

VariantWidthUse for
size="sm"400pxA decision: every confirmation and acknowledgement, including one with a few options to choose from or a short list of what it affects (Reset Agent, Change Visibility, the restart options). It is the width of AlertDialog
size="md" (default)448pxA small task: 1 to 5 fields, a list to pick from, a short help text that is only read (Create Channel, Add Members, role help)
size="lg"640pxContent that needs width: a list with several columns, a command or code, key-value rows, a long text to read, a flow of steps (Connect Computer, a computer's release notes)
size="wide"896pxThe large form dialog, in sections, with a fixed height of 760px or the window height less 32px, whichever is smaller
  • Every confirmation is one width, 400px, whether it is built on Dialog or AlertDialog. A confirmation whose content no longer fits 400px isn't a confirmation any more: it is a small task, at md.
  • sm, md and lg are the small dialog: its height follows its content, up to the window less 32px, and a click on the scrim closes it. wide is the large form dialog.
  • Set the width with size only. Product code never passes its own max-w-* to DialogContent. Content that fits none of the four sizes isn't a dialog: make it a page or a Drawer.
  • Only these overlays have a width outside the four sizes, each set by its own rule. Nothing else may add one:
    • Task details, 960px: a workspace overlay (Tasks).
    • Search, 720px, on DialogSurface, its top edge over the top bar's field (Search).
    • The showcase frame, 720px wide and about 520px high, shared by steps 3 to 5 of onboarding and the Upgrade to Pro dialog, and used by nothing else (Onboarding).
    • AnnouncementDialog, 576px: its picture and one-paragraph text read best there; at 640px the text runs past 80 characters a line.
    • An attachment preview, as wide as the file needs, up to 768px.
  • On a narrow screen every size is as wide as itself or the window less 32px, whichever is smaller, so at 390px each one is 358px wide. Its height and the wide dialog's sections are in Behavior.

The large form dialog uses bar on DialogHeader, DialogBody and DialogFooter: the header and footer stay in place and the body scrolls between them. The body holds DialogNav on the left, a line-seam after it, and one FormSection per section on the right. Its structure and rules are in Forms.

Behavior

  • A centered dialog appears and disappears without animation, always over the scrim (Motion). On a phone a dialog with fields is a sheet that slides up instead (On a phone).
  • Esc, × and Cancel close a dialog. A click on the scrim closes a small dialog.
  • Don't close a large form dialog on a click outside it: pass disablePointerDismissal on Dialog, so a half-filled form isn't lost to a stray click.
  • A centered dialog on a narrow screen keeps a 16px margin on each side and never grows taller than the window less 32px.
  • Below 768px the wide dialog hides DialogNav and the sections follow one another.
  • A dialog opened from a dialog or a sheet draws its own, lighter scrim over the one below (Stacked overlays). Render it inside the first one and this happens by itself. layer={1} is only for a dialog rendered outside the first one's tree: it lifts the dialog and its scrim above the first.

On a phone

Below 768px a dialog stays centered unless it has several fields, a long list or steps, in which case it becomes a sheet (stdrc and Artea, 2026-10-21). On a desktop and a tablet every dialog stays centered.

What the dialog holdsOn a phone
A decision: a statement and buttons, at most a few fixed choices, nothing typed (every confirmation; archive, stop, reset)The centered dialog, as on a desktop
One input or one text box and its buttons: a confirmation that asks for a typed name, naming a section, editing a display name or a description, a reason to rejectThe centered dialog, as on a desktop. While the keyboard is open it moves up, so the keyboard doesn't cover it
Several fields, a long or searchable list to pick from, or steps: forms, pickers, wizardsA sheet 90% of the screen tall
Content that is only read: role help, release notes, an app's details. It is short information, so it stays a dialogThe centered dialog, as on a desktop

The sheet:

  • Rises from the bottom over the scrim, with radius-dialog on its top corners. The strip of page left above it shows that it is a layer over the page, not a new page.
  • Its head is a pushed page's head (Navigation): × at the start, the title, and the dialog's one commit button at the end, at size md with the same color and words as on a desktop. It has no footer: × is Cancel. In a flow of steps the button at the end is the step's (Next, then the last step's action), and × still closes.
  • The body scrolls under the head and keeps the phone's 16px margin and the home indicator's inset at the bottom.
  • × and Esc close it. A tap on the scrim doesn't, so a half-filled form isn't lost to a stray tap, as with a large form dialog on a desktop. When something has been typed, ask before closing.
  • Dragging the sheet down to close it is planned, for when the sheet moves into raft-ui on top of Drawer's drag.
  • It slides up as it opens, like every bottom sheet (Motion).

Raft Chat builds these forms in the app for now (NarrowDialog in slock); they move into raft-ui as a phone presentation of Dialog.

Content

  • Title the dialog with the words of the button that opened it, or Edit and the item's name. Label its main button with the action it performs, without the object: usually the title's verb (Create Channel → Create); an Edit title's button is Save, and the other one Cancel (Writing).
  • Set body text in foreground-muted, below a title in foreground. Their type styles are in Typography.

Accessibility

  • Dialog wraps the Base UI Dialog. It provides the dialog role, the focus trap, Esc and focus return to the trigger.
  • Give every dialog a DialogTitle. It becomes the dialog's accessible name, and it is what a screen reader announces on open.
  • DialogDescription becomes the dialog's description.
  • The × of DialogClose is labeled "Close". Pass aria-label to change it.
  • DialogSurface has no behavior of its own. The app that uses it must add the dialog role, aria-modal, a name, the focus trap, Esc and the scrim.

Rules

  • Put at most two buttons in the footer: Cancel on the left (variant="ghost"), the confirm button on the right.
  • Make the confirm button pink (variant="primary") when it does something. Color it by its consequence when it interrupts or can't be undone (AlertDialog).
  • Put anything people must read before they act in a Banner inside the body (Feedback).
  • Pad every size by 20px. Its width comes from size (Variants), never from a class of your own.
  • Don't pass overlay={false} in product code: every centered dialog has the scrim, without exception.

API

Dialog

Prop类型默认值
actionsRefA ref to imperative actions. - `unmount`: Manually unmounts the dialog. Call this after any externally controlled closing animation finishes. - `close`: Closes the dialog imperatively when called.RefObject<DialogRootActions | null>
childrenThe content of the dialog. This can be a regular React node or a render function that receives the `payload` of the active trigger.ReactNode | PayloadChildRenderFunction<unknown>
defaultOpenWhether the dialog is initially open.booleanfalse
defaultTriggerIdID of the trigger that the dialog is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open dialog.string
disablePointerDismissalWhether to prevent the dialog from closing on outside presses. For non-modal dialogs, this also prevents the dialog from closing when focus moves outside of it.booleanfalse
handleA handle to associate the dialog with a trigger. If specified, allows external triggers to control the dialog's open state. Can be created with the Dialog.createHandle() method.BaseDialog.Handle<unknown>
modalDetermines if the dialog enters a modal state when open. - `true`: user interaction is limited to just the dialog: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the dialog, but document page scroll is not locked and pointer interactions outside of it remain enabled.boolean | "trap-focus"true
onOpenChangeEvent handler called when the dialog is opened or closed.(open: boolean, eventDetails: BaseDialog.Root.ChangeEventDetails) => void
onOpenChangeCompleteEvent handler called after any animations complete when the dialog is opened or closed.(open: boolean) => void
openWhether the dialog is currently open.boolean
triggerIdID of the trigger that the dialog is associated with. This is useful in conjunction with the `open` prop to create a controlled dialog. There's no need to specify this prop when the dialog is uncontrolled (that is, when the `open` prop is not set).string

DialogBody

Prop类型默认值
barA pinned bar of a scrolling (`size="wide"`) dialog.booleanfalse

Also takes the attributes of its element.

DialogClose

Prop类型默认值
childrenReactNode
size`md` (32px) and `icon-md` for rows, cards, banners, toolbars and panel headers; `lg` and `icon-lg` (the control height) for dialog footers, forms and beside a field; `xl` for a full-width call to action. The size follows the place: see Button › Size by place."md" | "lg" | "xl" | "icon-md" | "icon-lg" | DeprecatedButtonSize"md"
variant"primary" | "accent" | "warning" | "danger" | "default" | "outline" | "information" | "muted" | "success" | "danger-secondary" | "danger-outline" | "ghost" | "on-frame"
nativeButtonWhether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).booleantrue
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

DialogContent

Prop类型默认值
layer0 | 1
overlayfalse | DialogOverlayProps
portalPropsAlertDialogPortalProps
size"sm" | "md" | "lg" | "wide""md"
finalFocusDetermines the element to focus when the dialog is closed.boolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | HTMLElement | null | void)
initialFocusDetermines the element to focus when the dialog is opened. By default, focus moves to the first tabbable element inside the popup, except when the dialog is opened by touch — then the popup itself is focused to avoid opening the virtual keyboard.boolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | HTMLElement | null | void)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

DialogDescription

Prop类型默认值
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

DialogFooter, DialogHeader

Prop类型默认值
barA pinned bar of a scrolling (`size="wide"`) dialog.booleanfalse

Also takes the attributes of its element.

DialogNav

The section list of a wide (`size="wide"`) dialog, inside a `bar` body.
Prop类型默认值
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

DialogOverlay

Prop类型默认值
layer0 | 1
forceRenderWhether the backdrop is forced to render even when nested.booleanfalse
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

DialogPortal

Prop类型默认值
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null>
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden.booleanfalse
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

DialogSurface, DialogTitle

The dialog's frame — the popover layer, the dialog radius, the seam ring and the float shadow — for a centred modal whose lifecycle the app owns (its own backdrop, Escape and focus handling). It neither positions nor animates itself: the host centres it, and a centred modal has no motion. DialogContent draws the same frame for a Base UI dialog.
Prop类型默认值
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

DialogTrigger

Prop类型默认值
handleA handle to associate the trigger with a dialog. Can be created with the Dialog.createHandle() method.BaseDialog.Handle<unknown>
idID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for the dialog in controlled mode (with the DialogRoot `triggerId` prop).string
nativeButtonWhether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).booleantrue
payloadA payload to pass to the dialog when it is opened.{}
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

AnnouncementDialog

实验性
Prop类型默认值
actionsRefA ref to imperative actions. - `unmount`: Manually unmounts the dialog. Call this after any externally controlled closing animation finishes. - `close`: Closes the dialog imperatively when called.RefObject<DialogRootActions | null>
childrenThe content of the dialog. This can be a regular React node or a render function that receives the `payload` of the active trigger.ReactNode | PayloadChildRenderFunction<unknown>
defaultOpenWhether the dialog is initially open.booleanfalse
defaultTriggerIdID of the trigger that the dialog is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open dialog.string
disablePointerDismissalWhether to prevent the dialog from closing on outside presses. For non-modal dialogs, this also prevents the dialog from closing when focus moves outside of it.booleanfalse
handleA handle to associate the dialog with a trigger. If specified, allows external triggers to control the dialog's open state. Can be created with the Dialog.createHandle() method.BaseDialog.Handle<unknown>
modalDetermines if the dialog enters a modal state when open. - `true`: user interaction is limited to just the dialog: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the dialog, but document page scroll is not locked and pointer interactions outside of it remain enabled.boolean | "trap-focus"true
onOpenChangeEvent handler called when the dialog is opened or closed.(open: boolean, eventDetails: BaseDialog.Root.ChangeEventDetails) => void
onOpenChangeCompleteEvent handler called after any animations complete when the dialog is opened or closed.(open: boolean) => void
openWhether the dialog is currently open.boolean
triggerIdID of the trigger that the dialog is associated with. This is useful in conjunction with the `open` prop to create a controlled dialog. There's no need to specify this prop when the dialog is uncontrolled (that is, when the `open` prop is not set).string

AnnouncementDialogTrigger

实验性
Prop类型默认值
handleA handle to associate the trigger with a dialog. Can be created with the Dialog.createHandle() method.BaseDialog.Handle<unknown>
idID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for the dialog in controlled mode (with the DialogRoot `triggerId` prop).string
nativeButtonWhether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).booleantrue
payloadA payload to pass to the dialog when it is opened.{}
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

AnnouncementDialogClose

实验性
Prop类型默认值
childrenReactNode
size`md` (32px) and `icon-md` for rows, cards, banners, toolbars and panel headers; `lg` and `icon-lg` (the control height) for dialog footers, forms and beside a field; `xl` for a full-width call to action. The size follows the place: see Button › Size by place."md" | "lg" | "xl" | "icon-md" | "icon-lg" | DeprecatedButtonSize"md"
variant"primary" | "accent" | "warning" | "danger" | "default" | "outline" | "information" | "muted" | "success" | "danger-secondary" | "danger-outline" | "ghost" | "on-frame"
nativeButtonWhether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).booleantrue
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

改动

  • 0.8.3 · FixedA Dialog or AlertDialog opened from a sheet or another dialog draws its own, lighter scrim, so it no longer sits undimmed on the surface below. A sheet opened from a dialog does the same; a sheet over a sheet is unchanged. Nesting follows the modal roots, as in Base UI, and a modal={false} parent doesn't count.
  • 0.6.0 · Breaking ChangesProps that no longer change anything are removed: Badge uppercase; Checkbox size, variant, color; Status pulse, attention (status never animates); Switch / SwitchThumb size; SidebarItem variant; SidebarItemCount / SidebarItemIndicator variant and appearance (the count is always the pink sticker, the indicator the yellow @); NotificationCenterActionButton variant; DrawerContent width (one-go forms use the wide Dialog); AppRailItemIndicator attention; AppRailItemAttention children; Steps size, track, renderIndicator, renderTraveler; FilePreviewBadge type, color. Remove them at call sites.
  • 0.5.28 · AddedAdded the composable AnnouncementDialog through raft-ui/wip, built on existing Dialog and Button primitives, with a label, picture, scrolling body and fixed footer. Narrow dialogs stack both actions at full width.