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.
Playground
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>Examples
Default
A basic modal with a header, body, and footer driven by trigger and close buttons.
Sizes
Five panel widths — sm, md, lg, xl, and full.
Rounded
Five corner-rounding options applied to the panel and its controls.
Confirmation
A destructive confirmation with a leading icon and an error-intent action.
Form
A modal hosting form fields for editing data.
Non-dismissible
A modal that ignores backdrop and Escape dismissal and hides the X button.
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
Meter
Gauge displaying a known value within a fixed range — battery, disk usage, score, capacity. Linear or circular, four sizes, full intent palette with optional threshold-based auto-coloring, locale-aware formatting, and a multi-segment MeterGroup variant for stacked usage.
Navigation Menu
A full top-bar navigation pattern — logo, centre menu with mega-menu popups, and right-aligned action slots for language selector and auth CTAs. Ships two desktop trigger treatments (`pill`, `underline`), a `mode="mobile"` variant with a right-side drawer + submenu drill-in, richer mega-menu rows (tag + badge), and a theme-aware logo that swaps between light and dark sources. Built on @base-ui/react with 5 sizes, 5 rounded options, and LTR/RTL support.