Modal
A modal interrupts. Everything behind it stops being reachable — by pointer, by keyboard and in the accessibility tree — until it closes, which is why it is the right shape for a question that has to be answered and the wrong one for anything else.
import { Modal, ModalBackdrop, ModalContainer, ModalDialog } from "ropav";The layers
There are three, and each does one job. ModalBackdrop dims the page and takes the click that dismisses it. ModalContainer positions the dialog and decides how it scrolls. ModalDialog is the box. Splitting them is what lets a tall dialog scroll its own body on a phone and the whole page on a desktop, without either being a special case.
modal-basic.vue
<script setup lang="ts">
import {
Button,
Modal,
ModalBackdrop,
ModalBody,
ModalClose,
ModalCloseTrigger,
ModalContainer,
ModalDialog,
ModalFooter,
ModalHeader,
ModalHeading,
} from "ropav";
</script>
<template>
<Modal>
<Button variant="secondary">Open</Button>
<ModalBackdrop>
<ModalContainer>
<ModalDialog class="dialog">
<ModalCloseTrigger />
<ModalHeader>
<ModalHeading>Delete this project?</ModalHeading>
</ModalHeader>
<ModalBody>Everything in it goes with it. This cannot be undone.</ModalBody>
<ModalFooter>
<ModalClose>
<Button variant="tertiary">Cancel</Button>
</ModalClose>
<ModalClose>
<Button variant="danger">Delete</Button>
</ModalClose>
</ModalFooter>
</ModalDialog>
</ModalContainer>
</ModalBackdrop>
</Modal>
</template>
<style scoped>
.dialog {
@media (width >= 40rem) {
max-width: 360px;
}
}
</style>The first child of Modal is the trigger — anything at all; it does not need a special component. ModalClose wraps whatever should close the dialog, and ModalCloseTrigger is the ready-made × in the corner.
Placement and size
ModalContainer takes placement, size and scroll. scroll is the one worth choosing deliberately: it decides whether a dialog taller than the window scrolls inside itself or takes the page with it.
Dismissal
ModalBackdrop takes is-dismissable and is-keyboard-dismiss-disabled. Turn dismissal off only when losing the dialog would lose work — a half-finished form, an operation in flight. A dialog that cannot be escaped is a trap for anyone who opened it by accident.
Accessibility
- Focus moves into the dialog when it opens and back to the trigger when it closes, and it cannot leave while the dialog is open.
- The page behind is inert, so a screen reader cursor cannot wander into it.
ModalHeadinglabels the dialog. Give every modal one, even when the heading is obvious from the body.- Scrolling behind the dialog is locked, and released when it closes.
API
Modal
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | — | |
isDisabled | boolean | — | |
isOpen | boolean | — | |
state | OverlayTriggerState | — | An open state held outside the modal, for a caller driving several overlays from one place. Equivalent to passing isOpen and listening to openChange, and takes precedence over both. |
Slots: default.
| Event | Payload |
|---|---|
openChange | boolean |
update:isOpen | boolean |
ModalBackdrop
| Prop | Type | Default | Description |
|---|---|---|---|
isDismissable | boolean | true | Whether an interaction outside the dialog closes the modal. |
isEntering | boolean | — | Forces the entry state on both elements, for a caller driving the animation itself. |
isExiting | boolean | — | Forces the exit state on both elements. |
isKeyboardDismissDisabled | boolean | false | |
portalContainer | string | HTMLElement | document.body | Where the modal is rendered. |
shouldCloseOnInteractOutside | ((element: Element) => boolean) | — | Filters which outside elements dismiss the modal. |
style | string | CSSProperties | — | Inline style for the backdrop. Declared rather than left to fallthrough because the viewport custom properties are merged **over** it: three rules in the stylesheet size the modal from them, and a caller's style winning would leave the modal with no height at all. |
variant | "blur" | "opaque" | "transparent" | "opaque" | How the page behind the modal is treated. |
Slots: default.
ModalBody
Slots: default.
ModalClose
Slots: default.
ModalCloseTrigger
ModalCloseTrigger takes nothing but class.
ModalContainer
| Prop | Type | Default | Description |
|---|---|---|---|
placement | "auto" | "bottom" | "center" | "top" | "auto" | |
scroll | "inside" | "outside" | "inside" | Whether the dialog scrolls its own body or the page scrolls behind it. |
size | "xs" | "sm" | "md" | "lg" | "cover" | "full" | "md" |
Slots: default.
ModalDialog
The default slot receives:
| Slot prop | Type |
|---|---|
close | () => void |
ModalFooter
Slots: default.
ModalHeader
Slots: default.
ModalHeading
| Prop | Type | Default | Description |
|---|---|---|---|
level | number | 2 | Heading level. |
Slots: default.
ModalIcon
Slots: default.
ModalTrigger
Slots: default.