ComponentsOverlays

Dialog

A modal window over the page. Use it for decisions that need an answer now, short forms, previews, and search. Focus stays inside until it closes, then returns to where it was.

Usage

Dialog usageTSX
import { Button } from "@tulipfarm/sprout/button";
import {
  Dialog,
  DialogBody,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@tulipfarm/sprout/dialog";

<Dialog open={open} onOpenChange={(next) => !pending && setOpen(next)}>
  <DialogTrigger render={<Button variant="destructive" size="sm" />}>
    Remove member
  </DialogTrigger>
  <DialogContent finalFocus={() => trigger.current ?? heading.current}>
    <DialogHeader>
      <DialogTitle>Remove kofi@meadow.example?</DialogTitle>
      <DialogDescription>
        They lose access to this workspace now.
      </DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <Button variant="outline" disabled={pending} onClick={() => setOpen(false)}>
        Cancel
      </Button>
      <Button variant="destructive" disabled={pending} aria-busy={pending} onClick={remove}>
        {pending ? "Saving change…" : "Confirm removal"}
      </Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

Dialog works controlled with open and onOpenChange, or uncontrolled with defaultOpen. To block Escape and outside clicks while work is saving, ignore the change in onOpenChange. You can also mount the dialog only while it is open.

DialogContent portals an overlay and the popup, and adds a corner close button. Turn it off with showCloseButton={false}. DialogFooter showCloseButton adds an outline Close button after your actions.

Sizes

size is "default" (24rem, scrolls as a whole), "form" (36rem, the header stays put and DialogBody scrolls), "split" (64rem, no padding, for two panes), or "fullscreen" (covers the viewport). Below 40rem every size fills the width minus a 1rem gutter.

Sizes

Each size with the content it is used for. Default fits short forms and confirmations, form scrolls its middle, split holds two panes, and fullscreen covers the viewport.

defaultCreate a team: short form, inline submit

formRename a file: opened from a menu, focus returns to it

splitInvite members: form beside a live canvas
fullscreenFile viewer: custom header and close button

Confirmations

Pass finalFocus a function so focus goes back to the trigger, or to a section heading with tabIndex={-1} if the row is gone. Errors stay inside the dialog as an Alert, so the person can try again.

Confirmations

Default-size dialogs opened from row actions. Escape and outside clicks are ignored while a change is saving, and focus returns to the trigger or to the section heading if the row disappears.

Members and teams

  • Ana Ruizana@meadow.example · Owner
  • Kofi Mensahkofi@meadow.example · Member
  • Priya Natarajanpriya.natarajan@meadow.example · Admin
  • Field operationsTeam · 6 members

States

Dialogs often open before their data arrives. Show a skeleton with role="status", an error with a retry, and an empty StatePanel when there is nothing to choose. Leave out the description until it has something to say.

Loading, empty, error, and blocked states

Pick a starting state, then open the dialog. The body scrolls between a fixed header and footer, and the footer offers the recovery that fits the state.

Delete foreverSkeleton, blocker list, retry, failure
Delete skillDescription appears once loaded
CompactionMounted only while open
Move chatEmpty and error state panels, select, checkbox

Flows

The footer can hold a hint as a direct <p>, a submit button tied to a form with form="…", or a back step. Without a footer, put actions in a right-aligned row at the end of the body. A second dialog can open on top; when it closes, move focus to the control the person needs next.

Form dialogs

Form-size dialogs with a scrolling body and a footer bar. Multi-step flows swap the body and footer in place; a second dialog can open on top and hand focus back.

Save a copySubmit from the footer with form=, retry, success
MoveFooter hint, two steps
Use in chatStacked sharing dialog, focus hand-back
DetailsLong file name, swaps to sharing
Add to KnowledgeIcon action, slow-progress alert
Paste textBody rendered as the form
File textLong read-only text, no footer

Danger zone

Close workspaceTyped confirmation; focus goes to this heading

Search

Top placementTSX
<DialogContent
  data-slot="search-dialog"
  placement="top"
  showCloseButton={false}
  className="flex max-w-2xl flex-col gap-0 p-0"
>
  <DialogTitle className="sr-only">Search Meadow</DialogTitle>
  …
</DialogContent>

<DialogContent
  placement="top"
  style={{ "--sprout-dialog-top": "33.333%" }}
  className="gap-0 p-0"
  showCloseButton={false}
>
  <DialogHeader className="sr-only">…</DialogHeader>
  …
</DialogContent>

Search and command palettes

placement="top" pins the dialog near the top so results grow downward. The corner button is off; search closes from its input row and both titles are screen-reader only.

searchFilters, results, empty state, key hints
commandTop third, no padding, sr-only header

API

Dialog, DialogTrigger, DialogPortal, DialogClose, DialogOverlay, DialogTitle, and DialogDescription wrap the Base UI dialog parts and take their props, including render. DialogHeader, DialogBody, and DialogFooter render a div and accept render, so a body can be the form.

DialogContent takes size, placement("center" or "top"; top reads --sprout-dialog-top, default 12dvh), showCloseButton, closeLabel, container, and every Base UI popup prop such as initialFocus and finalFocus. It sets data-slot="dialog-content" (overridable), data-size, and data-placement.

Accessibility

The popup has role="dialog" and is named by DialogTitle. Every dialog needs a title, even if it is visually hidden with sr-only. Tab and Shift+Tab stay inside. Escape and outside clicks close it unless you ignore the change. Focus moves to the first focusable element, or to initialFocus.

Opening fades and scales in over 180ms; closing takes 120ms. Both turn off when reduced motion is requested, and fullscreen dialogs only fade.

Theme tokens

Dialogs use --sprout-overlay, --sprout-surface, --sprout-canvas, --sprout-line, --sprout-text, --sprout-muted, --sprout-shadow-overlay, and --sprout-radius-panel. They need no brand color, so they look the same with the neutral and TulipFarm themes in light and dark mode.