Component reference
Modal
A complete native modal dialog over the Dialog primitives: the host owns the open state and every business action, while the Modal owns the native showModal/close mechanics, Escape and backdrop dismissal, focus return and the generated title/description ids.
Preview
Title, body, actions, and focus return
The host owns the open signal: the trigger opens a native modal, Escape, the backdrop and the header close entry ask the host to close it, and focus returns to the trigger.
render() {
const open = signal(false);
return (
<Stack align="start">
<Button
onClick={() => {
open.value = true;
}}
>
Rename workspace
</Button>
<p>The dialog edits one name and keeps its draft until the review is done.</p>
<Modal
open={open}
onOpenChange={(next) => {
open.value = next;
}}
title="Rename workspace"
description="The name appears in the sidebar, shared links and browser titles."
actions={[
<Button
variant="quiet"
onClick={() => {
open.value = false;
}}
>
Cancel
</Button>,
<Button
onClick={() => {
open.value = false;
}}
>
Save name
</Button>,
]}
>
<Field label="Workspace name" htmlFor="modal-workspace-name">
<Input id="modal-workspace-name" value="Meowland workspace" />
</Field>
</Modal>
</Stack>
);
}Installation
Usage
import { Modal } from "meowtail";Examples
The other stories that show this component.
Long body scrolling
The title and close entry stay in the panel header while a long generated review scrolls in the body; the action row sits after the content.
render() {
const open = signal(false);
return (
<Stack align="start">
<Button
onClick={() => {
open.value = true;
}}
>
Review long summary
</Button>
<Modal
open={open}
onOpenChange={(next) => {
open.value = next;
}}
title="Review generated summary"
description="The source order and the operator comments stay together in the review."
actions={[
<Button
variant="quiet"
onClick={() => {
open.value = false;
}}
>
Close review
</Button>,
<Button
onClick={() => {
open.value = false;
}}
>
Accept summary
</Button>,
]}
>
<Stack gap="compact">
{Array.from({ length: 24 }, (_, index) => (
<p>
Item {index + 1}: The generated summary keeps the source order, notes which files
changed, and leaves the operator's comments beside the matching paragraph.
</p>
))}
</Stack>
</Modal>
</Stack>
);
},
});
export const ModalNonDismissibleStory = defineStory({
id: "modal-non-dismissible",
title: "Modal · Explicit close only",
group: "Overlays and feedback",
groupOrder: 30,
component: "Modal",
variant: "Non-dismissible review",
description:
"dismissible={false} removes the header close entry and ignores Escape and backdrop clicks, so only the host action can finish the review.",
covers: ["dismissible false", "no header close", "Escape ignored", "backdrop ignored"],
render() {
const open = signal(false);
return (
<Stack align="start">
<Button
onClick={() => {
open.value = true;
}}
>
Start final review
</Button>
<Modal
open={open}
onOpenChange={(next) => {
open.value = next;
}}
title="Final review in progress"
description="Escape and the backdrop are disabled until the review completes."
dismissible={false}
actions={
<Button
onClick={() => {
open.value = false;
}}
>
Mark review complete
</Button>
}
>
<p>
Finishing the review records the current summary. The dialog closes only from the
explicit action so an accidental Escape does not lose the review.
</p>
</Modal>
</Stack>
);
},
});Explicit close only
dismissible={false} removes the header close entry and ignores Escape and backdrop clicks, so only the host action can finish the review.
render() {
const open = signal(false);
return (
<Stack align="start">
<Button
onClick={() => {
open.value = true;
}}
>
Start final review
</Button>
<Modal
open={open}
onOpenChange={(next) => {
open.value = next;
}}
title="Final review in progress"
description="Escape and the backdrop are disabled until the review completes."
dismissible={false}
actions={
<Button
onClick={() => {
open.value = false;
}}
>
Mark review complete
</Button>
}
>
<p>
Finishing the review records the current summary. The dialog closes only from the
explicit action so an accidental Escape does not lose the review.
</p>
</Modal>
</Stack>
);
}API reference
Props
| Prop | Type | Default |
|---|---|---|
openRequiredRequired; the host owns the state. A Signal keeps the dialog in sync after mount. | MaybeSignal<boolean> | – |
onOpenChangeRequiredCalled with false when the reader dismisses the dialog (Escape, backdrop, header close entry, or a native close). The owner decides whether open changes; a controlled host that ignores the call keeps the dialog open. | (open: boolean) => void | – |
titleRequiredThe dialog heading and accessible name. | Child | – |
descriptionOptional supporting line under the heading; it is wired to the dialog description. | Child | – |
actionsOptional bottom action row; omit it for a body-only dialog. | Child | – |
dismissibleDefault true; false removes the header close entry and Escape/backdrop dismissal. | boolean | true |
closeLabelAccessible name of the header close entry; default "Close". | string | "Close" |
idId of the transparent wrapper and base for the generated title/description ids. A generated base is used when omitted. Fixed for the mount. | string | – |
childrenRequired | Child | – |
Also takes className, children and the attributes and events of its root element.
DOM parts
Registry
The component is registered as modal in the meowtail component registry.