VaneUI

VaneUI

Overlay Components

Modal

An accessible dialog component with focus trapping, scroll lock, and keyboard navigation. Built on Overlay with ARIA role="dialog" and aria-modal="true".

SourceEdit this page

An accessible dialog component with focus trapping, scroll lock, and keyboard navigation. Built on Overlay with ARIA role="dialog" and aria-modal="true". Sub-components: ModalHeader, ModalBody, ModalFooter, ModalCloseButton.

When to Use

  • Confirmations for destructive or non-reversible actions.
  • Multi-step or focused forms that should block interaction with the page behind.
  • Detail views that need to escape the surrounding layout (full-screen image, video player).
  • Required acknowledgements where dismissal must be intentional (closeOnOverlayClick={false}).

When NOT to Use

  • For passive notifications, toasts, or status messages — these should not block the user.
  • For inline disclosure of additional details — prefer Popup or an expandable section.

Customizing

Set app-wide Modal defaults with ThemeProvider's themeDefaults:

react-icon
import { ThemeProvider, Modal } from '@vaneui/ui';
<ThemeProvider themeDefaults={{
modal: { lg: true, border: true },
}}>
<Modal open={open} onClose={onClose}>Content</Modal>
</ThemeProvider>

Basic Modal

A controlled modal opened by a Button. The Modal portals to document.body, traps focus inside the dialog while open, locks body scroll, and closes on overlay click or Escape.

Modal content defaults: md, primary, outline, rounded, shadow, flex column, gap, wFull, overflowAuto, relative, noPadding (sub-components own their own padding).

react-icon
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<Modal open={open} onClose={() => setOpen(false)}>
<Stack>
<Text bold>Confirm Action</Text>
<Text>Are you sure?</Text>
<Row justifyEnd>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button primary filled onClick={() => setOpen(false)}>Confirm</Button>
</Row>
</Stack>
</Modal>

Compound Modal

Use ModalHeader, ModalBody, ModalFooter, and ModalCloseButton for full control over layout. When any of these are direct children, Modal renders them as-is without auto-wrapping. Each sub-component carries its own layout defaults:

  • ModalHeaderflex row, itemsCenter, justifyBetween, gap, padding
  • ModalBodyflex column, gap, padding, overflowAuto
  • ModalFooterflex row, itemsCenter, justifyEnd, gap, padding
  • ModalCloseButtonsecondary, transparent, noShadow, noRing
react-icon
import { Modal, ModalHeader, ModalBody, ModalFooter, ModalCloseButton, Button, Title, Input, Label, Stack } from "@vaneui/ui";
const [open, setOpen] = useState(false);
<Modal open={open} onClose={() => setOpen(false)}>
<ModalHeader>
<Title>Edit Profile</Title>
<ModalCloseButton />
</ModalHeader>
<ModalBody>
<Stack>
<Label>Name</Label>
<Input placeholder="Enter your name" />
<Label>Email</Label>
<Input placeholder="Enter your email" />
</Stack>
</ModalBody>
<ModalFooter>
<Button sm onClick={() => setOpen(false)}>Cancel</Button>
<Button sm primary filled onClick={() => setOpen(false)}>Save</Button>
</ModalFooter>
</Modal>

Convenience Props

Use the title, footer, and withCloseButton shorthand props to compose a structured modal without writing sub-components. When title is set, a close button is shown by default (toggle with withCloseButton). Children become the body.

react-icon
import { Modal, Button, Text, Row } from "@vaneui/ui";
const [open, setOpen] = useState(false);
<Modal
open={open}
onClose={() => setOpen(false)}
title="Quick Confirmation"
footer={
<Row justifyEnd>
<Button sm onClick={() => setOpen(false)}>Cancel</Button>
<Button sm primary filled onClick={() => setOpen(false)}>Confirm</Button>
</Row>
}
>
<Text>Are you sure you want to proceed?</Text>
</Modal>

Confirmation Dialog

A common pattern: a destructive action that requires explicit confirmation. Disable closeOnOverlayClick so users can't dismiss accidentally by clicking outside.

react-icon
const [open, setOpen] = useState(false);
<Button danger filled onClick={() => setOpen(true)}>Delete Account</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
closeOnOverlayClick={false}
title="Delete account?"
footer={
<Row justifyEnd>
<Button sm onClick={() => setOpen(false)}>Cancel</Button>
<Button sm danger filled onClick={() => setOpen(false)}>Delete</Button>
</Row>
}
>
<Text>This action is permanent and cannot be undone.</Text>
</Modal>

Form Modal

Modals can host forms. Focus trapping keeps Tab / Shift+Tab navigation inside the modal, and pressing Escape closes it (unless disabled). Use initialFocus to direct keyboard focus to a specific field on open.

react-icon
const [open, setOpen] = useState(false);
const nameRef = useRef<HTMLInputElement>(null);
<Button onClick={() => setOpen(true)}>Edit Profile</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
initialFocus={nameRef}
title="Edit Profile"
footer={
<Row justifyEnd>
<Button sm onClick={() => setOpen(false)}>Cancel</Button>
<Button sm primary filled onClick={() => setOpen(false)}>Save</Button>
</Row>
}
>
<Stack>
<Label>Name</Label>
<Input ref={nameRef} placeholder="Enter your name" />
<Label>Email</Label>
<Input type="email" placeholder="you@example.com" />
<Checkbox>Send me email updates</Checkbox>
</Stack>
</Modal>

Modal Sizes

Size props control modal content width (via the --fs-unit / --py-unit / --br-unit chain) — font-size, padding, gap, and border-radius all scale together.

react-icon
<Modal open={open} onClose={onClose} sm>Small modal</Modal>
<Modal open={open} onClose={onClose} lg>Large modal</Modal>

Modal Appearances

Apply appearance and variant props to style the content surface (border, text, background).

react-icon
<Modal open={open} onClose={onClose} primary filled>Primary modal</Modal>
<Modal open={open} onClose={onClose} danger filled>Danger modal</Modal>

Blur Overlay

Pass overlayProps={{ blur: true }} to add a backdrop-filter blur behind the modal.

react-icon
<Modal open={open} onClose={onClose} overlayProps={{ blur: true }}>
<Text>Blurred background</Text>
</Modal>

Non-dismissible Modal

Disable closeOnOverlayClick and closeOnEscape to force the user to take an explicit action (typically a button in the footer) before the modal can close.

react-icon
<Modal
open={open}
onClose={onClose}
closeOnOverlayClick={false}
closeOnEscape={false}
>
<Text>Must click a button to close</Text>
</Modal>

Full Screen Modal

Set fullScreen to make the modal fill the entire viewport. Full-screen modals have no border-radius (sharp is applied automatically) and use a transparent overlay — useful for immersive experiences or mobile-optimized views.

react-icon
<Modal open={open} onClose={() => setOpen(false)} fullScreen>
<ModalHeader>
<Title>Full Screen View</Title>
<ModalCloseButton />
</ModalHeader>
<ModalBody>
<Text>Content fills the entire viewport.</Text>
</ModalBody>
</Modal>

Accessibility & Advanced Props

Modal ships accessibility features enabled by default. The dialog renders with role="dialog" and aria-modal="true", and is automatically wired with aria-labelledby (when ModalHeader is used) and aria-describedby (when ModalBody is used).

PropDefaultDescription
scrollLocktrueLock body scroll when modal is open
focusTraptrueTrap Tab / Shift+Tab focus inside the modal
returnFocustrueReturn focus to the trigger element on close
initialFocusRef to the element that should receive focus on open
portaltrueRender via portal into document.body
keepMountedfalseKeep DOM node mounted when closed
noAnimationfalseDisable enter/exit transitions
transitionDuration200Animation duration in ms
react-icon
{/* Custom focus target */}
<Modal open={open} onClose={onClose} initialFocus={inputRef}>
<Input ref={inputRef} placeholder="Auto-focused" />
</Modal>
{/* Keep mounted for animation or state preservation */}
<Modal open={open} onClose={onClose} keepMounted transitionDuration={300}>
<Text>Stays in DOM when closed</Text>
</Modal>

More in Overlay Components