Modal

Modal is a dialog component displayed as an overlay above the main content — available in two types: standard Modal and AlertModal for actions requiring explicit user confirmation.

2
Types
6
Sizes
2
Scroll Mode
Radix
Base

Playground

Preview
SMModal
Modal
Type
Size
Scroll
Content
Options
Button Style
Cancel
Confirm
const [open, setOpen] = useState(false)

<Modal open={open} onOpenChange={setOpen}>
  <ModalTrigger asChild>
    <Button variant="outline">Open Modal</Button>
  </ModalTrigger>
  <ModalContent>
    <ModalHeader>
      <ModalTitle>Modal Title</ModalTitle>
      <ModalDescription>This is a description of the modal.</ModalDescription>
    </ModalHeader>
    <ModalBody>
      <p>This is a modal dialog. You can place any content here, including forms, confirmations, or informational messages.</p>
    </ModalBody>
    <ModalFooter>
      <ModalClose asChild>
        <Button variant="ghost" size="md" pressEffect={false}>Cancel</Button>
      </ModalClose>
      <Button size="md" pressEffect={false}>Confirm</Button>
    </ModalFooter>
  </ModalContent>
</Modal>

Sizes

xsSimple confirmation or alert
max-width360px
sm (default)Standard (default)
max-width480px
mdStandard form
max-width600px
lgComplex form
max-width760px
xlTable or dashboard
max-width960px
fullFull width
max-widthFull
Padding (all sizes)
Header24px · bottom 0
Body24px
Footer24px · top 0
Closeright 16px, top 16px

Features

Scroll Behavior

"outside": entire modal scrolls within the viewport. "inside": only Body scrolls — Header and Footer stay fixed.

{/* Outside scroll — entire modal scrolls within viewport */}
<ModalContent scrollBehavior="outside">
  <ModalHeader>
    <ModalTitle>Outside Scroll</ModalTitle>
    <ModalDescription>The entire modal scrolls within the viewport.</ModalDescription>
  </ModalHeader>
  <ModalBody>
    <p>Long content here...</p>
  </ModalBody>
</ModalContent>

{/* Inside scroll — only body scrolls; header stays fixed */}
<ModalContent scrollBehavior="inside">
  <ModalHeader>
    <ModalTitle>Inside Scroll</ModalTitle>
    <ModalDescription>Only the body scrolls; header and footer stay fixed.</ModalDescription>
  </ModalHeader>
  <ModalBody>
    <p>Long content here...</p>
  </ModalBody>
</ModalContent>

Sidebar Layout

Available for lg / xl / full sizes. Split ModalBody with p-0 flex to create a left-nav + right-content layout.

const [active, setActive] = useState(0)
const navItems = ['General', 'Security', 'Notifications']

<ModalContent size="lg" scrollBehavior="inside" showCloseButton={false}>
  <ModalBody className="p-0 flex min-h-0">
    {/* Left sidebar */}
    <div className="w-48 shrink-0 flex flex-col">
      <div className="p-3">
        <ModalClose asChild>
          <Button variant="ghost" size="md" pressEffect={false} className="w-9 h-9 p-0">
            <X className="icon-sm" />
            <span className="sr-only">Close</span>
          </Button>
        </ModalClose>
      </div>
      <nav className="flex-1 px-2 pb-4 space-y-1">
        {navItems.map((item, i) => (
          <button key={item} onClick={() => setActive(i)}
            className={cn("w-full text-left px-3 py-2 text-md rounded-lg transition-colors",
              active === i
                ? "bg-background-muted text-foreground font-semibold"
                : "text-text-muted hover:text-foreground hover:bg-background-muted"
            )}>
            {item}
          </button>
        ))}
      </nav>
    </div>
    <Divider orientation="vertical" color="muted" className="mx-0 h-auto" />
    {/* Right content */}
    <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
      <div className="px-6 pt-6 pb-4 shrink-0">
        <ModalTitle>{navItems[active]}</ModalTitle>
      </div>
      <div className="flex-1 px-6 pb-6 overflow-y-auto">
        <p className="text-md text-text-muted">Content area...</p>
      </div>
    </div>
  </ModalBody>
</ModalContent>

Close Button

Set to false to hide the close button. Use closeIcon prop to replace the default icon.

{/* With close button (default) — X only */}
<ModalContent showCloseButton>
  <ModalHeader>
    <ModalTitle>Title</ModalTitle>
  </ModalHeader>
  <ModalBody>...</ModalBody>
</ModalContent>

{/* Without close button — footer actions provide exit */}
<ModalContent showCloseButton={false}>
  <ModalHeader>
    <ModalTitle>Title</ModalTitle>
  </ModalHeader>
  <ModalBody>...</ModalBody>
  <ModalFooter>
    <ModalClose asChild>
      <Button variant="ghost" size="md" pressEffect={false}>Cancel</Button>
    </ModalClose>
    <Button size="md" pressEffect={false}>Confirm</Button>
  </ModalFooter>
</ModalContent>

API

Component Structure

ModalRadix Dialog
.Trigger.ContentProps.Header.Title.Description.Body.Footer.CloseProps.Overlay.Portal
AlertModalRadix AlertDialog
.Trigger.ContentProps.Header.Title.Description.Body.Footer.Action.Cancel.Overlay.Portal

Props

ModalContent

size"sm"
"xs" | "sm" | "md" | "lg" | "xl" | "full"

Maximum content width (360/480/600/760/960/full)

scrollBehavior"outside"
"inside" | "outside"

Scroll position when content overflows

showCloseButtontrue
boolean

Show the built-in close button

closeIconBuilt-in SVG icon
ReactNode

Custom close icon

AlertModalContent

size"sm"
"xs" | "sm"

Maximum content width (360/480)

ModalClose

asChildfalse
boolean

Use child element as the trigger

Anatomy

1
2
3
4
Modal title
5
Modal description
6
7
8
1
Overlay
Background overlay
2
Content
Dialog panel
3
Header
Title area
4
Title
Heading text
5
Description
Supplemental description
6
Close
Close button
7
Body
Main content
8
Footer
Action buttons

Best Practices

Do

  • Use AlertModal for destructive actions
  • Always set a title and description for the modal
  • Use scrollBehavior="inside" for long content
  • Choose a size appropriate for the content

Don't

  • Don't nest a modal inside another modal
  • Don't use a modal for simple notifications — use Toast instead
  • Don't create a modal with no way to close it
  • Reserve AlertModal for actions requiring confirmation — don't use it for routine interactions

Accessibility

Keyboard

EscClose modal
TabMove focus within modal

ARIA / WCAG

  • role="dialog" Set automatically by Radix Dialog
  • role="alertdialog" Set automatically for AlertModal
  • aria-labelledby Auto-linked to ModalTitle
  • aria-describedby Auto-linked to ModalDescription
  • Focus trap — restricts focus within the modal
  • sr-only label on the close button