affectUI › Components › Feedback

Dialog

A modal window on a dimmed backdrop – title and quiet line, content that scrolls under a fixed header and footer, Escape and backdrop close, the focus kept inside and handed back, a sheet on phones, and an AlertDialog to confirm with a red button.

Free and open source (MIT). Install: npx shadcn@latest add https://www.affectui.com/r/dialog.json · Import from @/components/ui/dialog

When to use

  • A task that needs the whole attention before going on: create a task, edit an agent, invite people, connect an app
  • A confirmation before something that can't be undone – delete an agent, cancel a subscription, revoke a key (use AlertDialog)
  • Content that should open over the page and come back to the same place when it closes

When not to use

  • A short form or a few settings next to the button that asks for them – use the Popover, which doesn't block the page
  • A list of actions or choices – use the Dropdown Menu
  • Searching and running commands – use the Command Menu
  • A short status after an action, like "Saved" or "Deleted · Undo" – use the Toaster; an undo is often kinder than a confirm
  • Nothing to show yet – use the Empty State

Usage

import { useState } from "react";
import { Dialog } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

export function EditAgent({ agent, onSave }: { agent: { name: string }; onSave: (name: string) => void }) {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState(agent.name);

  return (
    <Dialog
      open={open}
      onOpenChange={setOpen}
      trigger={<Button variant="secondary">Edit</Button>}
      title="Edit agent"
      description="Changes apply from its next run."
      footer={
        <>
          <Button variant="secondary" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <Button
            onClick={() => {
              onSave(name);
              setOpen(false);
            }}
          >
            Save
          </Button>
        </>
      }
    >
      <input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} />
    </Dialog>
  );
}

Props

NameTypeDefaultDescription
openbooleanShows it – pass it with onOpenChange to control it, or leave both out and use trigger
defaultOpenbooleanfalseOpen from the start when it isn't controlled
onOpenChange(open: boolean) => voidCalled with false on Escape, a backdrop press, the × (and AlertDialog's buttons), with true from the trigger
triggerReactElementA button that opens it – it keeps its own onClick, gets aria-haspopup and aria-expanded, and the focus comes back to it on close
titleReactNodeRequired. The heading – also the dialog's accessible name (aria-labelledby)
descriptionReactNodeA quiet line under the title, read out with it (aria-describedby)
childrenReactNodeThe content – it scrolls when it is taller than the window, a hairline shows at the edge that has more
footerReactNodeThe actions on the gray tray below the card, aligned to the end – they stay in view while the content scrolls and wrap rather than squeeze
size"sm" | "md" | "lg""md" (AlertDialog "sm")400, 480 or 720px wide – never wider than the window
widthnumber | stringAny width instead of a size, e.g. 560 or "40rem"
closeOnBackdropbooleantrueA press on the backdrop closes it. Off, the card gives a small nudge instead. AlertDialog never closes this way
closeButtonbooleantrueThe × in the corner. AlertDialog has none – its Cancel is the way out
closeLabelstring"Close"The ×'s accessible name and tooltip – for other languages
initialFocusRefObject<HTMLElement>The control that gets the focus on open. By default: an element with data-autofocus, else the first control in the content, then in the footer
containedbooleanfalseStays inside its nearest positioned parent – an app frame, a preview – instead of covering the window; Tab still goes round inside
...dialog propsComponentProps<"dialog">className, style, id, aria-* … go to the <dialog> layer

Keyboard

  • Tab: Moves through the controls inside and goes round – the page behind can't be reached
  • Esc: Closes it (AlertDialog: cancels) – a menu or popover opened inside closes first
  • Enter: Clicks the focused button – in AlertDialog that is the safe one first

Accessibility

  • A native <dialog> opened with showModal(): the top layer, everything behind it inert, the focus kept inside by the browser
  • Named by its title and described by its description; AlertDialog has role="alertdialog"
  • On open the focus moves to the first control (or initialFocus); on close it goes back to the trigger, or to whatever had it before
  • AlertDialog puts the focus on the safe button, never closes on a backdrop press, and its busy confirm sets aria-busy
  • contained keeps aria-modal and loops Tab itself, since the page around it isn't inert

Responsive

  • On phones (or in a frame narrower than 520px) it becomes a sheet from the bottom: full width, the actions within the thumb's reach and above the home bar, AlertDialog's two buttons sharing the row
  • Long content scrolls inside while the title and the actions stay put; long titles and unbroken words wrap
  • The page under it stops scrolling while it is open, without shifting sideways
  • Touch screens get bigger buttons and a bigger ×
  • Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"

Requirements

  • React 19

Related

  • Popover – A small floating card attached to its trigger – a title and quiet line, any content, flips to stay on screen, closes on a press outside or Escape and hands the focus back
  • Dropdown Menu – Actions and choices behind any trigger – icons, shortcuts, checks, red items and submenus, a gliding highlight, type-ahead and the full menu keyboard
  • Command Menu – Search and run anything from one field – tool actions with logos and ⌘1–⌘5, everyday commands with icons
  • Toaster – Calm toasts from anywhere – a stack that fans out on hover, promise toasts that morph from spinner to check, swipe to dismiss
  • Button – Primary, secondary, ghost and destructive buttons with icons, sizes and a shimmering loading state