Modal
An accessible dialog overlay built on Base UI. Compound component pattern with Header, Body, Footer, Title, Description, and Close. Five sizes. Five corner-rounding options. LTR and RTL support.
Installation
pnpm add @tessinaui/uiUsage
import {
Modal,
ModalTrigger,
ModalClose,
ModalContent,
ModalHeader,
ModalTitle,
ModalDescription,
ModalBody,
ModalFooter,
} from "@tessinaui/ui";<Modal>
<ModalTrigger className="...">Open</ModalTrigger>
<ModalContent>
<ModalHeader>
<ModalTitle>Confirm changes</ModalTitle>
<ModalDescription>This action cannot be undone.</ModalDescription>
</ModalHeader>
<ModalBody>
<p>Additional content goes here.</p>
</ModalBody>
<ModalFooter>
<ModalClose className="...">Cancel</ModalClose>
<ModalClose className="...">Save</ModalClose>
</ModalFooter>
</ModalContent>
</Modal>Playground
Showcase
API Reference
Modal
Root component. Wraps Dialog.Root from Base UI and provides size and dir context to all children.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "xl" | "full" | "md" | Max-width of the popup panel |
width | "narrow" | "default" | "wide" | "default" | Additional width constraint — narrow (xs), default (from size), wide (3xl) |
dir | "ltr" | "rtl" | "ltr" | Text direction — applied to the popup and all its children |
open | boolean | — | Controlled open state |
defaultOpen | boolean | false | Uncontrolled initial open state |
onOpenChange | (open: boolean) => void | — | Callback when open state changes |
dismissible | boolean | true | Whether clicking the backdrop or pressing Escape closes the modal |
modal | boolean | true | Whether the dialog uses modal behavior (focus trap, scroll lock) |
ModalTrigger
Re-export of Dialog.Trigger. Renders a <button> that opens the modal. Apply styles via className or swap the element via the render prop:
<ModalTrigger render={<Button />}>Open modal</ModalTrigger>ModalContent
Renders the popup panel inside a Portal. Includes the backdrop automatically.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "xl" | "full" | from <Modal> | Overrides the size from context |
width | "narrow" | "default" | "wide" | from <Modal> | Overrides the width from context |
rounded | "full" | "lg" | "md" | "sm" | "none" | "full" | Corner rounding of the panel |
className | string | — | Extra classes on the popup |
All other native <div> attributes (forwarded to Dialog.Popup) are accepted.
ModalHeader
| Prop | Type | Default | Description |
|---|---|---|---|
icon | React.ReactNode | — | Optional leading icon beside the title |
showClose | boolean | true | Whether to render the built-in X close button. Set to false to use a custom <ModalClose> |
ModalTitle
Renders Dialog.Title (linked to aria-labelledby). Accepts all <div> attributes plus className.
ModalDescription
Renders Dialog.Description (linked to aria-describedby). Accepts all <p> attributes plus className.
ModalBody
Scrollable content wrapper. Uses flex-1 overflow-y-auto — grows to fill space between header and footer.
ModalFooter
Flex row aligned to the end (justify-end). Accepts className to override layout if needed.
ModalClose
Re-export of Dialog.Close. Renders a <button> that closes the modal. Apply styles via className or swap via render prop:
<ModalClose render={<Button variant="secondary" />}>Cancel</ModalClose>ModalBackdrop
Pre-styled backdrop. Included inside <ModalContent> by default — only use directly for custom layouts.
Sizes
| Size | Max-width | Use case |
|---|---|---|
sm | 384 px | Confirmations, alerts, short forms |
md | 460 px | Standard dialogs (default) |
lg | 512 px | Medium-complexity forms |
xl | 576 px | Rich forms, previews |
full | 100% − 2rem | Full-screen panels |
Animations
The modal uses CSS transitions driven by Base UI's data attributes:
| Attribute | When applied | Effect |
|---|---|---|
data-starting-style | First frame of opening | opacity 0, scale 0.96 |
| (none) | Open steady state | opacity 1, scale 1 |
data-ending-style | During close animation | opacity 0, scale 0.96 |
The backdrop fades in/out independently with transition-opacity.
Controlled vs Uncontrolled
Uncontrolled — use <ModalTrigger> and <ModalClose> for open/close:
<Modal>
<ModalTrigger className="...">Open</ModalTrigger>
<ModalContent>
<ModalFooter>
<ModalClose className="...">Close</ModalClose>
</ModalFooter>
</ModalContent>
</Modal>Controlled — manage open state yourself:
const [open, setOpen] = useState(false);
<Modal open={open} onOpenChange={setOpen}>
<ModalTrigger onClick={() => setOpen(true)} className="...">Open</ModalTrigger>
<ModalContent>
<ModalFooter>
<button onClick={() => setOpen(false)}>Cancel</button>
<button onClick={() => { doSomething(); setOpen(false); }}>Confirm</button>
</ModalFooter>
</ModalContent>
</Modal>Accessibility
- Renders as
role="dialog"witharia-modal="true" <ModalTitle>is linked viaaria-labelledby<ModalDescription>is linked viaaria-describedby- Focus is trapped inside the dialog when open
- Scroll is locked on
<body>when open - Escape key closes the modal (unless
dismissible={false}) - Focus returns to the trigger when the modal closes
- The backdrop provides a click-outside dismiss target
Card
A flexible surface container for grouping related content. Four variants (elevated/outlined/filled/ghost), five intents, six rounded options, five sizes, horizontal layout, interactive states, and LTR/RTL support.
Drawer
An accessible slide-in panel built on Base UI Dialog. Compound component pattern with Header, Body, Footer, Title, and Description. Four sides (right, left, top, bottom). Five sizes. Five corner-rounding options. Auto-stacking footer for sheets. LTR and RTL support.