@risklight/modal
@risklight/modal
Modals as a stack, not as markup. Open a modal from anywhere, await a typed result, and veto closing with async guards. One framework-agnostic core with adapters for Vue 3 / Nuxt, React and plain JS.
Documentation · Live demo · Why this package
npm install @risklight/modal
Vue / Nuxt
import { createVueModal } from '@risklight/modal/vue'
createApp(App).use(createVueModal()).mount('#app')
<template>
<RouterView />
<ModalContainer />
</template>
const modal = useModal()
const ok = await modal.prompt<boolean>(ConfirmDelete, { title: 'Delete the report?' })
React
const modal = createReactModal()
export const App = () => (
<ModalProvider manager={modal}>
<Routes />
<ModalContainer />
</ModalProvider>
)
const ok = await useModal().prompt<boolean>(ConfirmDelete, { title: 'Delete the report?' })
Plain JS
Markup stays in HTML:
<template data-modal="confirm">
<div>
<h2 data-prop="question"></h2>
<button data-resolve="true">Yes</button>
<button data-close>No</button>
</div>
</template>
import { createVanillaModal } from '@risklight/modal/vanilla'
const modal = createVanillaModal()
const ok = await modal.prompt('confirm', { question: 'Delete?' })
Features
- Imperative and typed.
open,pushandprompt<R>from anywhere. Props are inferred from the component, andpromptresolves with a typed value ornull. - Async close guards. They run on Escape, backdrop,
close(), stack replacement and route changes.close()resolvestrueorfalseinstead of throwing on a veto. - Stacks and namespaces. Modals in modals, toasts with auto-close,
singleShow, a named registry, andbeforeOpenhooks at four levels. - Routing everywhere. vue-router, react-router (
ModalRoute) and a built-in History router for plain JS. Modals bound to a route close on navigation, go back on close, fall back to a page for deep links, and let a guard block leaving. - Animations everywhere. The same enter and leave classes in Vue, React and plain JS. A closing modal stays visible until its transition ends.
- Accessible by default.
role="dialog"on the surface, labelled by its heading.- A focus trap that returns focus to the opener.
- An
inertbackground, withallowOutsidefor portaled popovers. - Escape on
keydown, and backdrop closing that ignores text-selection drags.
- Practical. Scroll lock with scrollbar compensation, dragging by a handle, CSP nonce, SSR-safe managers per request.
- Small and tested. Pure ESM with strict types. 600+ tests cover the core, DOM behaviour, every adapter and SSR, and each adapter is also checked in a real browser.
Packages
Pick one adapter:
| Import | For |
|---|---|
@risklight/modal/vue |
Vue 3 and Nuxt |
@risklight/modal/react |
React 18 and 19 |
@risklight/modal/react-router |
ModalRoute for react-router 7 and 8 |
@risklight/modal/vanilla |
Plain JS: no framework, server-rendered templates, Alpine, htmx, jQuery |
Low-level entry points. You only need them to build an integration for another framework (docs):
| Import | What it is |
|---|---|
@risklight/modal |
The manager: stacks, guards, prompts. No DOM. |
@risklight/modal/dom |
Browser behaviour: focus trap, inert, Escape, scroll lock, dragging |
@risklight/modal/style.css |
Default styles, if you disable injection |
vue, vue-router, react and react-router are optional peer dependencies. It needs Node 22.12+ and is ESM only; require() works on Node 22.12+.
Development
npm ci
npm run lint
npm run typecheck
npm test
npm run build
npm run docs:dev
Releases are published from a vX.Y.Z tag through npm trusted publishing with provenance.
License
MIT. See LICENSE. The project started as a rewrite of jenesius-vue-modal by Jenesius.
Related Packages
A storybook decorator that allows you to use routing-aware components in your st...
A reusable Vue 3 modal dialog component with focus trap and ARIA support
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.1.1 | ^3.3.0 | MIT | |
| 0.1.0 | ^3.3.0 | MIT |