Imperative confirm-style dialogs with title, description, and footer actions.
Introduction
useDialogAction opens preset confirm-style dialogs without declaring <Modal> in your template. Each entry renders a Modal shell with a Card header, body, and footer actions.
Mount BridgeUIHosts (with BridgeDialogHost) near the root of your app—typically inside BridgeUIProvider. The hook returns open, close, closeTop, update, isOpen, and stackSize.
Use the framework selector in the site header to switch between React and Vue.
Import
import { BridgeUIHosts } from "@bridge-ui/vue/Actions";
import { useDialogAction } from "@bridge-ui/vue/Actions";import { BridgeUIHosts } from "@bridge-ui/react/Actions";
import { useDialogAction } from "@bridge-ui/react/Actions";Setup
Mount BridgeUIHosts with the dialog host inside BridgeUIProvider or open() will do nothing.
Wrap your app (or layout) with BridgeUIProvider and BridgeUIHosts:
<script setup lang="ts">
import { BridgeUIProvider } from "@bridge-ui/vue";
import { BridgeUIHosts } from "@bridge-ui/vue/Actions";
</script>
<template>
<BridgeUIProvider :global="{}" :components="{}">
<BridgeUIHosts :dialog="{ modal: { transition: 'fade' } }">
<!-- routes / pages -->
</BridgeUIHosts>
</BridgeUIProvider>
</template>import { BridgeUIProvider } from "@bridge-ui/react";
import { BridgeUIHosts } from "@bridge-ui/react/Actions";
export function App() {
return (
<BridgeUIProvider global={{}} components={{}}>
<BridgeUIHosts dialog={{ modal: { transition: "fade" } }}>
{/* routes / pages */}
</BridgeUIHosts>
</BridgeUIProvider>
);
}Basic usage
Call open() with title, description, and actions for the footer buttons. open() returns an entry id you pass to close(id).
<script setup lang="ts">
import { useDialogAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
const dialog = useDialogAction();
const open = () => {
const id = dialog.open({
title: "Confirm action",
description: "Preset dialog with Card header, body, and footer.",
actions: {
reject: { label: "Cancel" },
accept: { label: "Confirm", onClick: () => dialog.close(id) },
},
});
};
</script>
<template>
<Button v-on:click="open">Open dialog</Button>
</template>
import { useDialogAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
const Basic = () => {
const dialog = useDialogAction();
const open = () => {
const id = dialog.open({
title: "Confirm action",
description: "Preset dialog with Card header, body, and footer.",
actions: {
reject: { label: "Cancel" },
accept: { label: "Confirm", onClick: () => dialog.close(id) },
},
});
};
return <Button onClick={open}>Open dialog</Button>;
};
export default Basic;
Modal options
Pass modal on open() to control shell options such as size, transition, and align. Use color for semantic tinting on the Card content.
<script setup lang="ts">
import { useDialogAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
const dialog = useDialogAction();
const open = () => {
const id = dialog.open({
color: "success",
title: "Custom shell",
description: "modal.size and modal.transition from open().",
modal: {
size: "sm",
transition: "scale",
},
actions: {
accept: { label: "OK", onClick: () => dialog.close(id) },
},
});
};
</script>
<template>
<Button v-on:click="open">Open with modal options</Button>
</template>
import { useDialogAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
const ModalOptions = () => {
const dialog = useDialogAction();
const open = () => {
const id = dialog.open({
color: "success",
title: "Custom shell",
description: "modal.size and modal.transition from open().",
modal: {
size: "sm",
transition: "scale",
},
actions: {
accept: { label: "OK", onClick: () => dialog.close(id) },
},
});
};
return <Button onClick={open}>Open with modal options</Button>;
};
export default ModalOptions;
Destructive dialogs
Combine color: "error" with reject/accept actions for delete or irreversible confirmations.
<script setup lang="ts">
import { useDialogAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
const dialog = useDialogAction();
const open = () => {
const id = dialog.open({
color: "error",
title: "Delete item?",
modal: { size: "md" },
description: "This action cannot be undone.",
actions: {
reject: { label: "Cancel" },
accept: {
label: "Delete",
onClick: () => dialog.close(id),
},
},
});
};
</script>
<template>
<Button v-on:click="open">Open destructive dialog</Button>
</template>
import { useDialogAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
const Destructive = () => {
const dialog = useDialogAction();
const open = () => {
const id = dialog.open({
color: "error",
title: "Delete item?",
modal: { size: "md" },
description: "This action cannot be undone.",
actions: {
reject: { label: "Cancel" },
accept: {
label: "Delete",
onClick: () => dialog.close(id),
},
},
});
};
return <Button onClick={open}>Open destructive dialog</Button>;
};
export default Destructive;
close / closeTop / stack
Multiple dialogs can be open at once. Use close(id) for a specific entry, closeTop() for the topmost layer, and stackSize / isOpen(id) to inspect state.
<script setup lang="ts">
import { useDialogAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import { ref } from "vue";
const dialog = useDialogAction();
const lastId = ref<null | string>(null);
const openStack = () => {
const firstId = dialog.open({
title: "Stack layer 1",
description: "Open a second layer, then use closeTop().",
actions: {
accept: { label: "Close", onClick: () => dialog.close(firstId) },
},
});
const secondId = dialog.open({
title: "Stack layer 2",
description: "Topmost dialog in the imperative stack.",
actions: {
accept: { label: "Close", onClick: () => dialog.close(secondId) },
},
});
lastId.value = secondId;
};
</script>
<template>
<div class="flex flex-wrap gap-2">
<Button
variant="outline"
:disabled="!lastId"
v-on:click="lastId && dialog.close(lastId)"
>
close(lastId)
</Button>
<Button variant="outline" v-on:click="dialog.closeTop()">
closeTop()
</Button>
<Button variant="outline" v-on:click="openStack">
Open 2-layer stack
</Button>
</div>
</template>
import { useDialogAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import { useState } from "react";
const Stack = () => {
const dialog = useDialogAction();
const [lastId, setLastId] = useState<null | string>(null);
const openStack = () => {
const firstId = dialog.open({
title: "Stack layer 1",
description: "Open a second layer, then use closeTop().",
actions: {
accept: { label: "Close", onClick: () => dialog.close(firstId) },
},
});
const secondId = dialog.open({
title: "Stack layer 2",
description: "Topmost dialog in the imperative stack.",
actions: {
accept: { label: "Close", onClick: () => dialog.close(secondId) },
},
});
setLastId(secondId);
};
return (
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
disabled={!lastId}
onClick={() => {
if (!lastId) {
return;
}
dialog.close(lastId);
}}
>
close(lastId)
</Button>
<Button variant="outline" onClick={() => dialog.closeTop()}>
closeTop()
</Button>
<Button variant="outline" onClick={openStack}>
Open 2-layer stack
</Button>
</div>
);
};
export default Stack;
Callbacks
onClose runs when a dismiss starts (overlay, Escape, footer actions, or close ). onClosed runs after the leave animation.
<script setup lang="ts">
import { useDialogAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import { ref } from "vue";
const onCloseCount = ref(0);
const onClosedCount = ref(0);
const dialog = useDialogAction();
const open = () => {
const id = dialog.open({
title: "Callbacks",
onClose: () => {
onCloseCount.value += 1;
},
onClosed: () => {
onClosedCount.value += 1;
},
description:
"Any close path runs onClose, then onClosed after the leave animation.",
actions: {
reject: { label: "Cancel" },
accept: { label: "Confirm", onClick: () => dialog.close(id) },
},
});
};
</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 { useDialogAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import { useState } from "react";
const Callbacks = () => {
const dialog = useDialogAction();
const [onCloseCount, setOnCloseCount] = useState(0);
const [onClosedCount, setOnClosedCount] = useState(0);
const open = () => {
const id = dialog.open({
title: "Callbacks",
onClose: () => {
setOnCloseCount((count) => count + 1);
},
onClosed: () => {
setOnClosedCount((count) => count + 1);
},
description:
"Any close path runs onClose, then onClosed after the leave animation.",
actions: {
reject: { label: "Cancel" },
accept: { label: "Confirm", onClick: () => dialog.close(id) },
},
});
};
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
Patch an open entry with update(id, { props, modal }) to change content or shell options without closing and reopening.
<script setup lang="ts">
import { useDialogAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
import { ref } from "vue";
const dialog = useDialogAction();
const updateDemoId = ref<null | string>(null);
const open = () => {
const id = dialog.open({
title: "Before update",
modal: { size: "sm", transition: "scale" },
description: "Use the buttons below to call update().",
actions: {
accept: { label: "Close", onClick: () => dialog.close(id) },
},
});
updateDemoId.value = id;
};
const patchProps = () => {
if (!updateDemoId.value) {
return;
}
dialog.update(updateDemoId.value, {
props: {
color: "warning",
title: "After props update",
description: "Title and description patched via update({ props }).",
actions: {
accept: {
label: "Got it",
onClick: () => dialog.close(updateDemoId.value!),
},
},
},
});
};
const patchShell = () => {
if (!updateDemoId.value) {
return;
}
dialog.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 { useDialogAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
import { useState } from "react";
const Update = () => {
const dialog = useDialogAction();
const [updateDemoId, setUpdateDemoId] = useState<null | string>(null);
const open = () => {
const id = dialog.open({
title: "Before update",
modal: { size: "sm", transition: "scale" },
description: "Use the buttons below to call update().",
actions: {
accept: { label: "Close", onClick: () => dialog.close(id) },
},
});
setUpdateDemoId(id);
};
const patchProps = () => {
if (!updateDemoId) {
return;
}
dialog.update(updateDemoId, {
props: {
color: "warning",
title: "After props update",
description: "Title and description patched via update({ props }).",
actions: {
accept: {
label: "Got it",
onClick: () => dialog.close(updateDemoId!),
},
},
},
});
};
const patchShell = () => {
if (!updateDemoId) {
return;
}
dialog.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
Set modal.persistent: true to ignore Escape and overlay clicks. The user must use a footer action to dismiss.
<script setup lang="ts">
import { useDialogAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
const dialog = useDialogAction();
const open = () => {
const id = dialog.open({
title: "Persistent dialog",
modal: { persistent: true },
description: "Escape and overlay clicks do not close this dialog.",
actions: {
accept: { label: "Close", onClick: () => dialog.close(id) },
},
});
};
</script>
<template>
<Button v-on:click="open">Open persistent dialog</Button>
</template>
import { useDialogAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
const Persistent = () => {
const dialog = useDialogAction();
const open = () => {
const id = dialog.open({
title: "Persistent dialog",
modal: { persistent: true },
description: "Escape and overlay clicks do not close this dialog.",
actions: {
accept: { label: "Close", onClick: () => dialog.close(id) },
},
});
};
return <Button onClick={open}>Open persistent dialog</Button>;
};
export default Persistent;
Nested stack
Open a second dialog from a footer action while the first stays mounted—useful for multi-step confirmations.
<script setup lang="ts">
import { useDialogAction } from "@bridge-ui/vue/Actions";
import { Button } from "@bridge-ui/vue/Components/Button";
const dialog = useDialogAction();
const open = () => {
const outerId = dialog.open({
title: "Outer dialog",
description: "Open another dialog on top from the page.",
actions: {
reject: {
label: "Close outer",
onClick: () => dialog.close(outerId),
},
accept: {
label: "Open nested",
onClick: () => {
const innerId = dialog.open({
title: "Nested dialog",
modal: { size: "sm", transition: "scale" },
description: "Second layer opened via open() while outer stays.",
actions: {
accept: {
label: "Close nested",
onClick: () => dialog.close(innerId),
},
},
});
},
},
},
});
};
</script>
<template>
<Button v-on:click="open">Open nested example</Button>
</template>
import { useDialogAction } from "@bridge-ui/react/Actions";
import { Button } from "@bridge-ui/react/Components/Button";
const Nested = () => {
const dialog = useDialogAction();
const open = () => {
const outerId = dialog.open({
title: "Outer dialog",
description: "Open another dialog on top from the page.",
actions: {
reject: {
label: "Close outer",
onClick: () => dialog.close(outerId),
},
accept: {
label: "Open nested",
onClick: () => {
const innerId = dialog.open({
title: "Nested dialog",
modal: { size: "sm", transition: "scale" },
description: "Second layer opened via open() while outer stays.",
actions: {
accept: {
label: "Close nested",
onClick: () => dialog.close(innerId),
},
},
});
},
},
},
});
};
return <Button onClick={open}>Open nested example</Button>;
};
export default Nested;
API
| Method / property | Description |
|---|---|
| open(options) | Opens a dialog. 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 props and/or modal shell options on an open entry. |
| isOpen(id) | Whether the entry is currently open. |
| stackSize | Number of open dialog entries. |
open options
| Option | Type | Description |
|---|---|---|
| title | string | Dialog headline. |
| description | string | Body text below the title. |
| color | AlertColor | Semantic color for the dialog. |
| actions | { accept?, reject? } | Footer buttons with label and optional onClick. |
| modal | ModalOptions | Shell options: size, transition, persistent, etc. |
| onClose | () => void | Called when a dismiss starts. |
| onClosed | () => void | Called after the leave animation. |