suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Patterns19
  • Overview
  • Action Bar
  • Action Menu
  • App Shell
  • Command Palette
  • Confirm
  • Context Actions
  • Date Picker
  • Flyout
  • Modal
  • Navbar
  • Paged List
  • Panel
  • Picker
  • Preview
  • Search Picker
  • Segment Picker
  • Split View
  • Table List
  • Trail

suiss/ui · Pattern

Modal

A modal window that opens as a centered dialog on desktop and a bottom drawer on mobile.

Modal is an open-source pattern from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

import {
  Modal,
  ModalBody,
  ModalClose,
  ModalContent,
  ModalDescription,
  ModalFooter,
  ModalHeader,
  ModalTitle,
  ModalTrigger,
} from "@/components/patterns/modal"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function ModalDemo() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <Modal>
        <ModalTrigger render={<Button variant="outline" />}>
          Edit profile
        </ModalTrigger>
        <ModalContent>
          <ModalHeader>
            <ModalTitle>Edit profile</ModalTitle>
            <ModalDescription>
              Update your name and username. Click save when you are done.
            </ModalDescription>
          </ModalHeader>
          <ModalBody className="grid gap-4">
            <div className="grid gap-2">
              <Label htmlFor="modal-name">Name</Label>
              <Input id="modal-name" defaultValue="Ada Lovelace" />
            </div>
            <div className="grid gap-2">
              <Label htmlFor="modal-username">Username</Label>
              <Input id="modal-username" defaultValue="@ada" />
            </div>
          </ModalBody>
          <ModalFooter>
            <ModalClose render={<Button variant="secondary" />}>
              Cancel
            </ModalClose>
            <Button>Save changes</Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </div>
  )
}

Modal presents focused content that blocks the rest of the page, such as a form or a share sheet. On desktop it renders a Dialog. Below the 48rem breakpoint it renders a bottom Drawer with a swipe handle, and each part switches to its drawer counterpart: the header, title, description and close map to the drawer's parts, ModalBody gains padding, and ModalFooter stacks its buttons in reverse order so the primary action sits on top.

Use it for tasks that need the user's full attention before returning to the page.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/modal

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add patterns modal

Manual

  1. Add the cn helper.

  2. Add the pieces it builds on: Dialog, Drawer, use-mobile.

  3. Copy the source into components/patterns/modal.tsx:

components/patterns/modal.tsx
"use client"

import * as React from "react"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import {
  Drawer,
  DrawerClose,
  1. Update the import paths to match your project.

Usage

import { Modal, ModalTrigger, ModalContent, ModalHeader, ModalBody, ModalFooter, ModalTitle, ModalDescription, ModalClose } from "@/components/patterns/modal"
<div className="flex min-h-svh items-center justify-center">
  <Modal>
    <ModalTrigger render={<Button variant="outline" />}>
      Edit profile
    </ModalTrigger>
    <ModalContent>
      <ModalHeader>
        <ModalTitle>Edit profile</ModalTitle>
        <ModalDescription>
          Update your name and username. Click save when you are done.
        </ModalDescription>
      </ModalHeader>
      <ModalBody className="grid gap-4">
        <div className="grid gap-2">
          <Label htmlFor="modal-name">Name</Label>
          <Input id="modal-name" defaultValue="Ada Lovelace" />
        </div>
        <div className="grid gap-2">
          <Label htmlFor="modal-username">Username</Label>
          <Input id="modal-username" defaultValue="@ada" />
        </div>
      </ModalBody>
      <ModalFooter>
        <ModalClose render={<Button variant="secondary" />}>
          Cancel
        </ModalClose>
        <Button>Save changes</Button>
      </ModalFooter>
    </ModalContent>
  </Modal>
</div>
<div className="flex min-h-svh items-center justify-center">
  <Modal>
    <ModalTrigger asChild>
      <Button variant="outline">Edit profile</Button>
    </ModalTrigger>
    <ModalContent>
      <ModalHeader>
        <ModalTitle>Edit profile</ModalTitle>
        <ModalDescription>
          Update your name and username. Click save when you are done.
        </ModalDescription>
      </ModalHeader>
      <ModalBody className="grid gap-4">
        <div className="grid gap-2">
          <Label htmlFor="modal-name">Name</Label>
          <Input id="modal-name" defaultValue="Ada Lovelace" />
        </div>
        <div className="grid gap-2">
          <Label htmlFor="modal-username">Username</Label>
          <Input id="modal-username" defaultValue="@ada" />
        </div>
      </ModalBody>
      <ModalFooter>
        <ModalClose asChild>
          <Button variant="secondary">Cancel</Button>
        </ModalClose>
        <Button>Save changes</Button>
      </ModalFooter>
    </ModalContent>
  </Modal>
</div>

ModalContent shows a close button by default; set showCloseButton to false to hide it, or pass closeLabel to show text instead of the icon.

The modal can be controlled with open and onOpenChange or uncontrolled with defaultOpen. Pass render to ModalTrigger or ModalClose to use your own Button.

The modal can be controlled with open and onOpenChange or uncontrolled with defaultOpen. Set asChild on ModalTrigger or ModalClose to use your own Button.

Examples

With close button

Relies on the default corner close button as the only way to dismiss besides Escape.

import {
  Modal,
  ModalBody,
  ModalContent,
  ModalDescription,
  ModalFooter,
  ModalHeader,
  ModalTitle,
  ModalTrigger,
} from "@/components/patterns/modal"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

export default function ModalWithCloseButton() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <Modal>
        <ModalTrigger render={<Button variant="outline" />}>
          Share project
        </ModalTrigger>
        <ModalContent>
          <ModalHeader>
            <ModalTitle>Share project</ModalTitle>
            <ModalDescription>
              Anyone with the link can view this project.
            </ModalDescription>
          </ModalHeader>
          <ModalBody>
            <Input
              readOnly
              aria-label="Project link"
              defaultValue="https://example.com/p/1042"
            />
          </ModalBody>
          <ModalFooter>
            <Button>Copy link</Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </div>
  )
}

With close label

Replaces the close icon with a text button using closeLabel.

import {
  Modal,
  ModalBody,
  ModalContent,
  ModalDescription,
  ModalFooter,
  ModalHeader,
  ModalTitle,
  ModalTrigger,
} from "@/components/patterns/modal"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

export default function ModalWithCloseLabel() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <Modal>
        <ModalTrigger render={<Button variant="outline" />}>
          Share project
        </ModalTrigger>
        <ModalContent closeLabel="Close">
          <ModalHeader>
            <ModalTitle>Share project</ModalTitle>
            <ModalDescription>
              Anyone with the link can view this project.
            </ModalDescription>
          </ModalHeader>
          <ModalBody>
            <Input
              readOnly
              aria-label="Project link"
              defaultValue="https://example.com/p/1042"
            />
          </ModalBody>
          <ModalFooter>
            <Button>Copy link</Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </div>
  )
}

Floating

Insets the mobile drawer from the screen edges with floating.

import {
  Modal,
  ModalBody,
  ModalClose,
  ModalContent,
  ModalDescription,
  ModalFooter,
  ModalHeader,
  ModalTitle,
  ModalTrigger,
} from "@/components/patterns/modal"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function ModalFloating() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <Modal floating>
        <ModalTrigger render={<Button variant="outline" />}>
          Edit profile
        </ModalTrigger>
        <ModalContent>
          <ModalHeader>
            <ModalTitle>Edit profile</ModalTitle>
            <ModalDescription>
              Update your name and username. Click save when you are done.
            </ModalDescription>
          </ModalHeader>
          <ModalBody className="grid gap-4">
            <div className="grid gap-2">
              <Label htmlFor="modal-name">Name</Label>
              <Input id="modal-name" defaultValue="Ada Lovelace" />
            </div>
            <div className="grid gap-2">
              <Label htmlFor="modal-username">Username</Label>
              <Input id="modal-username" defaultValue="@ada" />
            </div>
          </ModalBody>
          <ModalFooter>
            <ModalClose render={<Button variant="secondary" />}>
              Cancel
            </ModalClose>
            <Button>Save changes</Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </div>
  )
}

API Reference

Anatomy9 parts
<Modal>
<ModalTrigger />
<ModalContent>
<ModalHeader>
<ModalTitle />
<ModalDescription />
</ModalHeader>
<ModalBody />
<ModalFooter>
<ModalClose />
</ModalFooter>
</ModalContent>
</Modal>
01

Modal

The root that renders a Dialog on desktop and a Drawer on mobile.

PropTypeDefaultDescription
onOpenChange(open: boolean) => void-Called when the modal opens or closes.
floatingboolean-Insets the mobile drawer from the screen edges with fully rounded corners.

Also accepts the props of React.ComponentProps<typeof Dialog>.

02

ModalTrigger

The button that opens the modal.

PropTypeDefaultDescription
classNamestring-Classes applied to the trigger.

Also accepts the props of React.ComponentProps<typeof DialogTrigger>.

03

ModalContent

The popup window, with a close button by default.

PropTypeDefaultDescription
classNamestring-Classes applied to the dialog or drawer popup.
styleReact.CSSProperties-Inline styles applied to the dialog or drawer popup.

Also accepts the props of React.ComponentProps<typeof DialogContent>.

04

ModalHeader

Groups the title and description.

Also accepts the props of React.ComponentProps<"div">.

05

ModalBody

The main content area, padded on mobile.

Also accepts the props of React.ComponentProps<"div">.

06

ModalFooter

Holds the modal's actions; stacked with the primary action on top on mobile.

Also accepts the props of React.ComponentProps<"div">.

07

ModalTitle

The modal's title, used as its accessible name.

Also accepts the props of React.ComponentProps<typeof DialogTitle>.

08

ModalDescription

Supporting text, used as the modal's accessible description.

Also accepts the props of React.ComponentProps<typeof DialogDescription>.

09

ModalClose

A button that closes the modal.

Also accepts the props of React.ComponentProps<typeof DialogClose>.

Accessibility

  • The popup has the dialog role, named by ModalTitle and described by ModalDescription.
  • Escape closes the modal and returns focus to the trigger.
  • The default close button is labelled Close.

Notes

Parts carry data-slot values from modal through modal-close. ModalContent accepts className and style as plain values only, because the same props are forwarded to either the dialog or the drawer popup.

NextNavbarPreviousFlyout

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • With close button
    • With close label
    • Floating
  4. 04API Reference
    • Modal
    • ModalTrigger
    • ModalContent
    • ModalHeader
    • ModalBody
    • ModalFooter
    • ModalTitle
    • ModalDescription
    • ModalClose
  5. 05Accessibility
  6. 06Notes