bifrost-modals
v1.0.0
Published
Headless promise-based modal manager for React — registry, stack, and useModal without UI chrome
Maintainers
Readme
bifrost-modals

Headless promise-based modal manager for React. Owns registration, stack, and promise lifecycle — not portals, focus traps, or styling. Pair with any Dialog UI (Radix, shadcn, custom).
Install
npm install bifrost-modals
# peer: react >= 18, react-dom >= 18Quick start
Mount one provider at the app root:
import { BifrostProvider } from "bifrost-modals";
export function App() {
return (
<>
<Routes />
<BifrostProvider />
</>
);
}Create a modal and open it:
import Bifrost, { useModal } from "bifrost-modals";
type TConfirmProps = { title: string };
export const ConfirmModal = Bifrost.create<TConfirmProps>(({ title }) => {
const modal = useModal<TConfirmProps, boolean>();
return (
<dialog open={modal.visible}>
<h2>{title}</h2>
<button
type="button"
onClick={() => {
modal.resolve(true);
modal.hide();
}}
>
OK
</button>
<button
type="button"
onClick={() => {
modal.resolve(false);
modal.hide();
}}
>
Cancel
</button>
</dialog>
);
});
// Open
const ok = await Bifrost.show(ConfirmModal, { title: "Delete?" });Close contract
modal.resolve(value)/modal.reject(reason)— settle the promise (does not close UI)modal.hide()— setvisibletofalse(keep mounted briefly for exit animation)modal.remove()— unmount (often on close-animation end /onCloseAutoFocus)
String IDs (dedupe)
Bifrost.register("upgrade-modal", UpgradeModal);
Bifrost.show("upgrade-modal", { plan: "pro" });Showing an already-visible id reuses the same promise and merges props.
Multimodality
Multiple different modal ids can be open at once. Stack order is the mounted modals[] order (later = higher in the tree).
useModal().stackIndex is the 0-based index in that stack. Use it if your UI needs explicit z-index layering:
const modal = useModal();
// e.g. with Radix / shadcn Dialog overlay + content:
<div style={{ zIndex: 50 + modal.stackIndex }} />This package does not manage focus, Escape, scroll lock, or portals — leave those to your Dialog primitive.
SSR
show / hide / remove / resolve / reject no-op or resolve gracefully when window is undefined. The Provider renders no modals on the server.
API
| Export | Role |
|--------|------|
| Bifrost.create(Component) | Register + return wrapped component |
| Bifrost.register(id, Component) | String-id registration |
| Bifrost.show(component \| id, props?) | Open → Promise |
| Bifrost.hide / remove | Close / unmount by component or id |
| Bifrost.Provider / BifrostProvider | Mount point |
| useModal() | { visible, stackIndex, show, hide, remove, resolve, reject } |
License
MIT
