Tessera UI

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/ui

Usage

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

Preview

Showcase

Preview

API Reference

Root component. Wraps Dialog.Root from Base UI and provides size and dir context to all children.

PropTypeDefaultDescription
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
openbooleanControlled open state
defaultOpenbooleanfalseUncontrolled initial open state
onOpenChange(open: boolean) => voidCallback when open state changes
dismissiblebooleantrueWhether clicking the backdrop or pressing Escape closes the modal
modalbooleantrueWhether 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.

PropTypeDefaultDescription
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
classNamestringExtra classes on the popup

All other native <div> attributes (forwarded to Dialog.Popup) are accepted.

ModalHeader

PropTypeDefaultDescription
iconReact.ReactNodeOptional leading icon beside the title
showClosebooleantrueWhether 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

SizeMax-widthUse case
sm384 pxConfirmations, alerts, short forms
md460 pxStandard dialogs (default)
lg512 pxMedium-complexity forms
xl576 pxRich forms, previews
full100% − 2remFull-screen panels

Animations

The modal uses CSS transitions driven by Base UI's data attributes:

AttributeWhen appliedEffect
data-starting-styleFirst frame of openingopacity 0, scale 0.96
(none)Open steady stateopacity 1, scale 1
data-ending-styleDuring close animationopacity 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" with aria-modal="true"
  • <ModalTitle> is linked via aria-labelledby
  • <ModalDescription> is linked via aria-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

On this page