rapidUI

Dialog

Modal window with header, footer, title and description, built on Base UI Dialog.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/dialog.json

Installs to @/components/ui/dialog.

Manual installation

Install the dependencies:

pnpm add @base-ui/react@^1.8.0 lucide-react@^1.48.0

Add the components it builds on: button.

Copy the source into your project:

components/ui/dialog.tsx
'use client'

import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
import { XIcon } from 'lucide-react'
import * as React from 'react'

import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'

function Dialog({ ...props }: DialogPrimitive.Root.Props) {
	return <DialogPrimitive.Root data-slot="dialog" {...props} />
}

function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
	return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}

function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
	return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}

function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
	return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}

function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
	return (
		<DialogPrimitive.Backdrop
			data-slot="dialog-overlay"
			className={cn(
				'fixed inset-0 isolate z-50 bg-black/50 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',
				className,
			)}
			{...props}
		/>
	)
}

function DialogContent({
	className,
	children,
	showCloseButton = true,
	...props
}: DialogPrimitive.Popup.Props & {
	showCloseButton?: boolean
}) {
	return (
		<DialogPortal>
			<DialogOverlay />
			<DialogPrimitive.Popup
				data-slot="dialog-content"
				className={cn(
					'fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto rounded-xl bg-background p-4 text-sm text-foreground ring-1 ring-border duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
					className,
				)}
				{...props}
			>
				{children}
				{showCloseButton && (
					<DialogPrimitive.Close
						data-slot="dialog-close"
						render={
							<Button
								variant="ghost"
								className="absolute top-2 right-2 text-muted-foreground active:bg-muted/70"
								size="icon-sm"
							/>
						}
					>
						<XIcon />
						<span className="sr-only">Close</span>
					</DialogPrimitive.Close>
				)}
			</DialogPrimitive.Popup>
		</DialogPortal>
	)
}

function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
	return <div data-slot="dialog-header" className={cn('flex flex-col gap-2', className)} {...props} />
}

function DialogFooter({
	className,
	showCloseButton = false,
	children,
	...props
}: React.ComponentProps<'div'> & {
	showCloseButton?: boolean
}) {
	return (
		<div
			data-slot="dialog-footer"
			className={cn(
				'-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end',
				className,
			)}
			{...props}
		>
			{children}
			{showCloseButton && <DialogPrimitive.Close render={<Button variant="outline" />}>Close</DialogPrimitive.Close>}
		</div>
	)
}

function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
	return (
		<DialogPrimitive.Title
			data-slot="dialog-title"
			className={cn('font-heading text-base leading-none font-medium', className)}
			{...props}
		/>
	)
}

function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
	return (
		<DialogPrimitive.Description
			data-slot="dialog-description"
			className={cn(
				'text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground',
				className,
			)}
			{...props}
		/>
	)
}

export {
	Dialog,
	DialogClose,
	DialogContent,
	DialogDescription,
	DialogFooter,
	DialogHeader,
	DialogOverlay,
	DialogPortal,
	DialogTitle,
	DialogTrigger,
}

A modal dialog component that overlays content on top of the main view. Built on top of Base UI Dialog, it follows WAI-ARIA design patterns and provides a fully accessible dialog experience.

Features

  • Focus management
  • Keyboard navigation
  • Screen reader support
  • Responsive design
  • Customizable content
  • Animated transitions
  • Blurred backdrop overlay
  • Built-in close button (toggle with showCloseButton on DialogContent)
  • Optional footer close button (showCloseButton on DialogFooter)
  • Mobile-friendly
  • ARIA compliant

Usage

import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogFooter,
  DialogTitle,
  DialogDescription,
  DialogClose,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Field, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'

// Basic usage
export function BasicDialog() {
  return (
    <Dialog>
      <DialogTrigger render={<Button />}>Edit contact details</DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Contact details</DialogTitle>
          <DialogDescription>
            We use these details to reach you about your cases.
          </DialogDescription>
        </DialogHeader>
        <Field>
          <FieldLabel htmlFor="email">Email</FieldLabel>
          <Input id="email" type="email" defaultValue="anna.lindqvist@example.se" />
        </Field>
        <DialogFooter>
          <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
          <Button>Save changes</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

// Controlled dialog
export function ControlledDialog() {
  const [open, setOpen] = React.useState(false)

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger render={<Button />}>Send supplement</DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Send supplement to case 2026-04812?</DialogTitle>
        </DialogHeader>
        <DialogFooter>
          <Button variant="outline" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <Button onClick={() => { sendSupplement(); setOpen(false) }}>
            Send
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

Examples

Basic Dialog

A form in a dialog for editing a resident's contact details.

Controlled Dialog

A confirmation that closes itself after sending, using the controlled open state. The page below the button updates when the files are sent.

With Custom Content

A case summary with a definition list, a separator and the case officer's contact details.

The corner close button is hidden with showCloseButton={false} on DialogContent, and DialogFooter renders an outline "Close" button with showCloseButton. Here it closes a dialog for sharing a public agenda.

Scrollable Content

A privacy notice in a scrollable region, with the confirmation and footer always in view.

Props

Dialog

  • defaultOpen - The open state when initially rendered
  • open - The controlled open state
  • onOpenChange - Event handler for open state changes
  • modal - Whether the dialog is modal (blocks interactions outside)

DialogTrigger

  • render - Element to render instead of the default <button>, e.g. render={<Button variant="outline" />}
  • children - The trigger's content

DialogContent

  • children - Content to display in the dialog
  • className - Additional CSS classes (default max width is sm:max-w-sm; override with e.g. sm:max-w-lg)
  • showCloseButton - Whether to render the close button in the top-right corner (default true)
  • initialFocus / finalFocus - Element to focus when the dialog opens / closes

DialogClose

  • render - Element to render instead of the default <button>, e.g. render={<Button variant="outline" />}
  • Closes the dialog when clicked

DialogHeader

  • className - Additional CSS classes
  • children - Header content

DialogFooter

  • className - Additional CSS classes
  • showCloseButton - Whether to append an outline "Close" button (default false)
  • children - Footer content

DialogTitle

  • className - Additional CSS classes
  • children - Title text

DialogDescription

  • className - Additional CSS classes
  • children - Description text

Accessibility

The Dialog component follows WAI-ARIA guidelines:

  • Uses role="dialog" or role="alertdialog"
  • Manages focus trap within dialog
  • Supports keyboard navigation:
    • Esc: Close dialog
    • Tab: Navigate focusable elements
    • Shift+Tab: Navigate focusable elements (reverse)
  • Announces dialog title and description
  • Provides clear action buttons
  • Maintains focus on trigger after closing

Best Practices

  1. Content Organization:

    • Use clear, descriptive titles
    • Keep content focused and concise
    • Provide clear actions
    • Group related information
  2. Visual Design:

    • Maintain proper spacing
    • Use consistent styling
    • Consider mobile layouts
    • Ensure readable text
  3. Interaction:

    • Provide clear feedback
    • Consider animations
    • Manage focus properly
  4. Accessibility:

    • Write descriptive labels
    • Use semantic markup
    • Test with screen readers
    • Verify keyboard navigation
  • Alert Dialog
  • Sheet
  • Drawer
  • Popover
  • Sheet