Starter Docs
UI

Dialog

A native modal dialog in shadcn's parts, with sizes, right and bottom sheets and a morph from its trigger. Every animation is CSS.

Share project

Anyone with the link can view this project. Only members can edit it.

https://rabtx.dev/p/q3-roadmap

Installation

Add the registry to components.json, once

components.json
{
  "registries": {
    "@rabtx": "http://localhost:3002/r/{name}.json"
  }
}

Add the component

npx shadcn@latest add @rabtx/dialog

Or, without the namespace, by URL:

npx shadcn@latest add http://localhost:3002/r/dialog.json

The files land in your components/ui and are styled by your own theme. Add --overwrite to replace a shadcn component with the same name.

Usage

Installed from the registry, import from @/components/ui/dialog; inside this monorepo, from @rabtx/ui/dialog.

import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@rabtx/ui/dialog";

export function Share() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="secondary">Share</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Share project</DialogTitle>
          <DialogDescription>Anyone with the link can view it.</DialogDescription>
        </DialogHeader>
        …
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="ghost">Cancel</Button>
          </DialogClose>
          <Button>Copy link</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

The parts are shadcn's, with the same data-slot names. DialogContent is a real <dialog> opened with showModal(), so the browser does the hard parts: the panel sits in the top layer above everything, the rest of the page is inert, focus moves in and stays in, Escape closes it and focus goes back to the trigger. closedby="any" adds a press on the backdrop (light dismiss). The panel is named by DialogTitle and described by DialogDescription without ids passed by hand.

Form

Wrap the fields and the footer in a form. Control open to close it after a successful submit; an invalid field keeps it open and the browser points at the field.

Edit profile

This is how teammates see you.

@

Letters, numbers and dashes.

Signed in as Shabir Khan

Destructive confirm

showClose={false} and dismissible={false} leave the two buttons as the only way out (Escape still cancels). The safe action comes first, so it takes the first focus.

Delete “Q3 roadmap”?

The project, its 24 tasks and every file in it are deleted for everyone. This cannot be undone.

Morph

<Dialog morph> grows the panel out of its trigger and folds it back on close. It is a View Transition: the trigger and the panel share a view-transition-name for the change, the box moves on the panel spring, and the trigger hides while the panel is open. Without View Transitions, with reduced motion or data-motion="off", it opens with the usual scale and fade (or at once).

Upgrade to Team

The panel grows out of the button and folds back into it.

Seats
12
Billing
Yearly
Total
$1,152

Sheet

side="right" is a full-height panel against the right edge; side="bottom" rises from the bottom, full width on a phone and capped on a wide screen. Same parts, same behavior.

Notifications

Choose what reaches your inbox.

Move to

Pick the board for these 3 tasks.

Long content

The panel never grows past the viewport less 1rem a side; past that it scrolls. To keep the header and the actions in view, give the middle its own scroll area, focusable so the keyboard can scroll it.

Terms of service

Last updated 1 November 2026.

1. Section 1. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

2. Section 2. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

3. Section 3. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

4. Section 4. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

5. Section 5. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

6. Section 6. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

7. Section 7. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

8. Section 8. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

9. Section 9. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

10. Section 10. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

11. Section 11. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

12. Section 12. You keep ownership of everything you put in a project. We store it to run the service, back it up, and delete it within 30 days of a request. Nothing here limits rights you have by law.

Nested

A dialog opened from a dialog stacks above it with its own backdrop. Escape and a backdrop press close only the top one, and focus returns to the button in the first dialog.

Workspace settings

Acme · 12 members

A dialog opened from here stacks on top. Escape closes only the top one.

API

Dialog holds the state and renders nothing itself:

Prop

Type

DialogContent takes everything a <dialog> takes, plus:

Prop

Type

DialogTrigger and DialogClose render a button, or with asChild pass their props to the one child (our Button, a link styled as a button). DialogTitle is an h2, DialogDescription a p, and DialogHeader and DialogFooter are divs.

Outside React, dialogClass() and dialogParts from @rabtx/ui/dialog/styles give the class names, and the Invoker Commands API opens it with no script at all:

<button command="show-modal" commandfor="share">Share</button>
<dialog id="share" class="rx-surface rx-dialog rx-dialog-md" closedby="any" aria-labelledby="share-title">
  <div class="rx-dialog-header">
    <h2 id="share-title" class="rx-dialog-title">Share project</h2>
  </div>
  <div class="rx-dialog-footer">
    <button command="close" commandfor="share">Done</button>
  </div>
</dialog>

DialogTrigger renders those same command attributes until the page hydrates, so a modal dialog already opens before React is running.

Theming

The Dialog reads only the theme tokens.

PartTokens
Panelfill --popover, text --popover-foreground, Flat edge --border
Floating depth--surface-ring, --surface-edge-top, --surface-edge-bottom, --surface-sheen, --surface-shade
Description, close button--muted-foreground; close hover --accent, --accent-foreground
Backdrop--background pulled toward black at 45%, with a 2px blur
Focus--ring
Corners--radius-box; the close button --radius-item
Motion--spring-panel, --spring-press, --motion-ease

There is no cast shadow: the raised edge and the backdrop separate the panel from the page.

How the motion works

EffectCSS
Enter@starting-style gives [open] its first frame (opacity 0, scale 0.96, 8px down; a sheet starts off-screen) and opacity, scale and translate transition on the panel spring
Exitshorter (160ms, sheets 220ms) and eased; display and overlay transition with allow-discrete, so the panel stays in the top layer until it has faded
Backdrop::backdrop fades its color and blur in the same way
Close buttonpops in from 0.8 just after the panel, and presses down on the press spring
Morphdocument.startViewTransition(); the group moves on --spring-panel and each picture keeps its own size, clipped by the box
Scroll lock:root:has(.rx-dialog:modal) { overflow: hidden }

JavaScript only calls showModal() and close() and, for the morph, starts the View Transition. Reduced motion and data-motion="off" drop every transition and the morph.

Browser support

  • Chromium 135+: everything, including light dismiss with closedby, the invoker before hydration, the exit animation and the morph.
  • Safari 18 and Firefox 129+: the dialog, its enter animation, focus and Escape work; light dismiss falls back to a small click handler; without overlay the exit is shorter or instant; the morph uses the View Transitions API where present and the scale and fade elsewhere.

Accessibility

  • The panel is a native modal dialog: role, aria-modal, the inert page, the focus move and its return come from the browser. aria-labelledby and aria-describedby point at the title and the description when they are present.
  • The trigger has aria-haspopup="dialog", aria-expanded and aria-controls.
  • The close button is a real button named "Close" (closeLabel). It is last in the panel, so the first focus lands on the content, not on the corner.
  • Give every dialog a DialogTitle. A dialog that must be answered can turn off light dismiss and the close button, but Escape always cancels.

On this page