Imperative modal API for opening custom components in an overlay shell.
Introduction
useModalAction opens modals imperatively with any Vue or React component as content—typically a Card. The Modal shell (overlay, portal, backdrop, transitions) is rendered by BridgeModalHost.
Use this when you need to open overlays from business logic, nested components, or callbacks without threading show state through props.
Use the framework selector in the site header to switch between React and Vue.
Import
import { BridgeUIHosts } from "@bridge-ui/vue/Actions";
import { useModalAction } from "@bridge-ui/vue/Actions";import { BridgeUIHosts } from "@bridge-ui/react/Actions";
import { useModalAction } from "@bridge-ui/react/Actions";Setup
Mount BridgeUIHosts with BridgeModalHost inside BridgeUIProvider. Without the host, open() will not show a modal. See useDialogAction for a full layout example.
Basic usage
Pass a component and props to open(). The returned id is passed to close(id) from your content or page logic.
<script setup lang="ts">
import { useModalAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import CardModalContent from "./CardModalContent.vue";
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: { transition: "fade" },
props: {
title: "Imperative modal",
onClose: () => modal.close(id),
body: "Opened with useModalAction().open(). The shell is rendered by BridgeModalHost.",
},
});
};
</script>
<template>
<Button v-on:click="open">Open modal</Button>
</template>
import { useModalAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import CardModalContent from "./CardModalContent";
const Basic = () => {
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: { transition: "fade" },
props: {
title: "Imperative modal",
onClose: () => modal.close(id),
children:
"Opened with useModalAction().open(). The shell is rendered by BridgeModalHost.",
},
});
};
return <Button onClick={open}>Open modal</Button>;
};
export default Basic;
Modal options
Configure the shell with modal: size, blur, transition, align, persistent, and autoFocus.
<script setup lang="ts">
import { useModalAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import CardModalContent from "./CardModalContent.vue";
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: {
blur: "md",
size: "lg",
transition: "slide-up",
},
props: {
title: "Custom shell options",
onClose: () => modal.close(id),
body: "modal.size, modal.blur, and modal.transition from open().",
},
});
};
</script>
<template>
<Button v-on:click="open">Open with modal options</Button>
</template>
import { useModalAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import CardModalContent from "./CardModalContent";
const ModalOptions = () => {
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: {
blur: "md",
size: "lg",
transition: "slide-up",
},
props: {
title: "Custom shell options",
onClose: () => modal.close(id),
children: "modal.size, modal.blur, and modal.transition from open().",
},
});
};
return <Button onClick={open}>Open with modal options</Button>;
};
export default ModalOptions;
Component props
The props object is forwarded to your content component—use it for titles, form state, or onClose callbacks.
<script setup lang="ts">
import { useModalAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import GreetingContent from "./GreetingContent.vue";
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: GreetingContent,
modal: {
size: "sm",
transition: "scale",
},
props: {
name: "Bridge UI",
onClose: () => modal.close(id),
},
});
};
</script>
<template>
<Button v-on:click="open">Open with component props</Button>
</template>
import { useModalAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import GreetingContent from "./GreetingContent";
const ComponentProps = () => {
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: GreetingContent,
modal: {
size: "sm",
transition: "scale",
},
props: {
name: "Bridge UI",
onClose: () => modal.close(id),
},
});
};
return <Button onClick={open}>Open with component props</Button>;
};
export default ComponentProps;
autoFocus
Set modal.autoFocus: true to focus the first focusable element when the modal opens.
<script setup lang="ts">
import { useModalAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import CardModalContent from "./CardModalContent.vue";
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: {
size: "sm",
autoFocus: true,
transition: "fade",
},
props: {
title: "autoFocus",
onClose: () => modal.close(id),
body: "modal.autoFocus focuses the first focusable element on open.",
},
});
};
</script>
<template>
<Button v-on:click="open">Open with autoFocus</Button>
</template>
import { useModalAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import CardModalContent from "./CardModalContent";
const AutoFocus = () => {
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: {
size: "sm",
autoFocus: true,
transition: "fade",
},
props: {
title: "autoFocus",
onClose: () => modal.close(id),
children:
"modal.autoFocus focuses the first focusable element on open.",
},
});
};
return <Button onClick={open}>Open with autoFocus</Button>;
};
export default AutoFocus;
close / closeTop / stack
Imperative modals stack like dialogs. Use close(id), closeTop(), and inspect stackSize / isOpen(id).
<script setup lang="ts">
import { useModalAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import { ref } from "vue";
import CardModalContent from "./CardModalContent.vue";
const modal = useModalAction();
const lastId = ref<null | string>(null);
const openStack = () => {
const firstId = modal.open({
component: CardModalContent,
modal: {
size: "md",
transition: "fade",
},
props: {
title: "Stack layer 1",
onClose: () => modal.close(firstId),
body: "Open a second layer, then use closeTop().",
},
});
const secondId = modal.open({
component: CardModalContent,
modal: {
size: "sm",
transition: "scale",
},
props: {
title: "Stack layer 2",
onClose: () => modal.close(secondId),
body: "This is the topmost modal in the imperative stack.",
},
});
lastId.value = secondId;
};
</script>
<template>
<div class="flex flex-wrap gap-2">
<Button
variant="outline"
:disabled="!lastId"
v-on:click="lastId && modal.close(lastId)"
>
close(lastId)
</Button>
<Button variant="outline" v-on:click="modal.closeTop()">
closeTop()
</Button>
<Button variant="outline" v-on:click="openStack">
Open 2-layer stack
</Button>
</div>
</template>
import { useModalAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import { useState } from "react";
import CardModalContent from "./CardModalContent";
const Stack = () => {
const modal = useModalAction();
const [lastId, setLastId] = useState<null | string>(null);
const openStack = () => {
const firstId = modal.open({
component: CardModalContent,
modal: {
size: "md",
transition: "fade",
},
props: {
title: "Stack layer 1",
onClose: () => modal.close(firstId),
children: "Open a second layer, then use closeTop().",
},
});
const secondId = modal.open({
component: CardModalContent,
modal: {
size: "sm",
transition: "scale",
},
props: {
title: "Stack layer 2",
onClose: () => modal.close(secondId),
children: "This is the topmost modal in the imperative stack.",
},
});
setLastId(secondId);
};
return (
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
disabled={!lastId}
onClick={() => {
if (!lastId) {
return;
}
modal.close(lastId);
}}
>
close(lastId)
</Button>
<Button variant="outline" onClick={() => modal.closeTop()}>
closeTop()
</Button>
<Button variant="outline" onClick={openStack}>
Open 2-layer stack
</Button>
</div>
);
};
export default Stack;
Callbacks
onClose and onClosed on open() fire for every dismiss path, including overlay clicks and close(id).
<script setup lang="ts">
import { useModalAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import { ref } from "vue";
import CardModalContent from "./CardModalContent.vue";
const onCloseCount = ref(0);
const onClosedCount = ref(0);
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: { transition: "fade" },
onClose: () => {
onCloseCount.value += 1;
},
onClosed: () => {
onClosedCount.value += 1;
},
props: {
title: "Callbacks",
onClose: () => modal.close(id),
body: "Any close path (overlay, Escape, footer, close(id)) runs onClose, then onClosed after the leave animation.",
},
});
};
</script>
<template>
<div class="flex flex-col gap-4">
<p class="text-secondary-500 text-sm">
onClose / onClosed counts: {{ onCloseCount }} / {{ onClosedCount }}
</p>
<Button v-on:click="open">Open with callbacks</Button>
</div>
</template>
import { useModalAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import { useState } from "react";
import CardModalContent from "./CardModalContent";
const Callbacks = () => {
const modal = useModalAction();
const [onCloseCount, setOnCloseCount] = useState(0);
const [onClosedCount, setOnClosedCount] = useState(0);
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: { transition: "fade" },
onClose: () => {
setOnCloseCount((count) => count + 1);
},
onClosed: () => {
setOnClosedCount((count) => count + 1);
},
props: {
title: "Callbacks",
onClose: () => modal.close(id),
children:
"Any close path (overlay, Escape, footer, close(id)) runs onClose, then onClosed after the leave animation.",
},
});
};
return (
<div className="flex flex-col gap-4">
<p className="text-secondary-500 text-sm">
onClose / onClosed counts: {onCloseCount} / {onClosedCount}
</p>
<Button onClick={open}>Open with callbacks</Button>
</div>
);
};
export default Callbacks;
update
Call update(id, { props, modal }) to patch content or shell options on an open modal.
<script setup lang="ts">
import { useModalAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import { ref } from "vue";
import CardModalContent from "./CardModalContent.vue";
const modal = useModalAction();
const updateDemoId = ref<null | string>(null);
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: {
size: "sm",
transition: "scale",
align: "middle-center",
},
props: {
title: "Before update",
body: "Use the buttons below to patch props or modal shell via update().",
onClose: () => {
modal.close(id);
updateDemoId.value = null;
},
},
});
updateDemoId.value = id;
};
const patchProps = () => {
if (!updateDemoId.value) {
return;
}
modal.update(updateDemoId.value, {
props: {
title: "After props update",
body: "Title and body patched via update({ props }).",
onClose: () => {
modal.close(updateDemoId.value!);
updateDemoId.value = null;
},
},
});
};
const patchShell = () => {
if (!updateDemoId.value) {
return;
}
modal.update(updateDemoId.value, {
modal: {
size: "lg",
align: "top-center",
transition: "slide-up",
},
});
};
</script>
<template>
<div class="flex flex-wrap gap-2">
<Button v-on:click="open">Open update demo</Button>
<Button variant="outline" v-on:click="patchProps" :disabled="!updateDemoId">
update props
</Button>
<Button variant="outline" v-on:click="patchShell" :disabled="!updateDemoId">
update modal shell
</Button>
</div>
</template>
import { useModalAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import { useState } from "react";
import CardModalContent from "./CardModalContent";
const Update = () => {
const modal = useModalAction();
const [updateDemoId, setUpdateDemoId] = useState<null | string>(null);
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: {
size: "sm",
transition: "scale",
align: "middle-center",
},
props: {
title: "Before update",
onClose: () => {
modal.close(id);
setUpdateDemoId(null);
},
children:
"Use the buttons below to patch props or modal shell via update().",
},
});
setUpdateDemoId(id);
};
const patchProps = () => {
if (!updateDemoId) {
return;
}
modal.update(updateDemoId, {
props: {
title: "After props update",
children: "Title and body patched via update({ props }).",
onClose: () => {
modal.close(updateDemoId);
setUpdateDemoId(null);
},
},
});
};
const patchShell = () => {
if (!updateDemoId) {
return;
}
modal.update(updateDemoId, {
modal: {
size: "lg",
align: "top-center",
transition: "slide-up",
},
});
};
return (
<div className="flex flex-wrap gap-2">
<Button onClick={open}>Open update demo</Button>
<Button variant="outline" onClick={patchProps} disabled={!updateDemoId}>
update props
</Button>
<Button variant="outline" onClick={patchShell} disabled={!updateDemoId}>
update modal shell
</Button>
</div>
);
};
export default Update;
persistent
With modal.persistent: true, only explicit actions in your content dismiss the modal.
<script setup lang="ts">
import { useModalAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import CardModalContent from "./CardModalContent.vue";
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: {
persistent: true,
transition: "fade",
},
props: {
title: "Persistent",
onClose: () => modal.close(id),
body: "Escape and overlay clicks do not close this modal.",
},
});
};
</script>
<template>
<Button v-on:click="open">Open persistent</Button>
</template>
import { useModalAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import CardModalContent from "./CardModalContent";
const Persistent = () => {
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: CardModalContent,
modal: {
persistent: true,
transition: "fade",
},
props: {
title: "Persistent",
onClose: () => modal.close(id),
children: "Escape and overlay clicks do not close this modal.",
},
});
};
return <Button onClick={open}>Open persistent</Button>;
};
export default Persistent;
Nested stack
Modal content can call useModalAction() again to open another layer on top.
<script setup lang="ts">
import { useModalAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import OuterStackContent from "./OuterStackContent.vue";
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: OuterStackContent,
props: {
onClose: () => modal.close(id),
},
modal: {
size: "lg",
transition: "fade",
},
});
};
</script>
<template>
<Button v-on:click="open">Open nested example</Button>
</template>
import { useModalAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import OuterStackContent from "./OuterStackContent";
const Nested = () => {
const modal = useModalAction();
const open = () => {
const id = modal.open({
component: OuterStackContent,
props: {
onClose: () => modal.close(id),
},
modal: {
size: "lg",
transition: "fade",
},
});
};
return <Button onClick={open}>Open nested example</Button>;
};
export default Nested;
API
| Method / property | Description |
|---|---|
| open(options) | Opens a modal with component and props. Returns an entry id. |
| close(id) | Closes the entry with the given id. |
| closeTop() | Closes the topmost entry in the stack. |
| update(id, patch) | Patches component props and/or modal shell options. |
| isOpen(id) | Whether the entry is currently open. |
| stackSize | Number of open modal entries. |
open options
| Option | Type | Description |
|---|---|---|
| component | Component | Content component rendered inside the modal panel. |
| props | object | Props passed to the content component. |
| modal | ModalOptions | Shell: size, blur, transition, persistent, autoFocus, etc. |
| onClose | () => void | Called when a dismiss starts. |
| onClosed | () => void | Called after the leave animation. |