Forge Dialog: dialogs built on the native <dialog> element, with no z-index left to hunt
An admin page needs to ask "are you sure you want to delete this?". The browser's built-in confirm can do it, but it freezes the whole page and cannot be styled; the usual replacements drag in a framework. Forge Dialog is how we solved that for our own products: no runtime dependencies, built on the native dialog element, with React, Vue and Svelte adapters inside the same package.
A familiar situation: an admin page has a delete button, and before deleting for real it needs to ask the user one question.
The quickest route is the browser's built-in confirm(). It works immediately and needs no installation. But it carries three problems anyone who has shipped it has met: it blocks the entire page until the user clicks, freezing everything else; it cannot be styled, so a system-looking box appears in the middle of your product; and on mobile it displays alongside the site's domain name, which reads more like a security warning than an ordinary question.
The usual escape is installing a dialog library. But most options either tie themselves to one framework, or bundle every feature into a single download — you need a confirmation box and receive the code for drawers, command palettes and image viewers as well.
Forge Dialog is how we solved this for our own products, released as open source under MIT. This article covers the three design decisions behind it.
Decision 1 — build on the native `<dialog>` element instead of drawing our own overlay
This is the most consequential technical difference, and it resolves a problem anyone who has built a modal has lost time to: z-index.
The traditional approach creates a div layered over the page and pushes it up with z-index. The trouble is that the number only means anything within the parent's stacking context. A fixed navigation bar, an ancestor carrying transform or filter, a mapping library that sets z-index: 9999 — any of them can hide your dialog, or clip it when it extends past the edge. The usual remedy is raising the number until it finally appears, and a few months later somebody does the same for a different component.
The browser's native <dialog> element leaves that race entirely. When opened as a modal, the browser lifts it into the top layer — a plane outside the page's ordinary stacking flow. No element on the page can cover it whatever its z-index, and no ancestor clips it. Two more things come free: the rest of the page is automatically marked inert, and the Escape key closes the dialog the way the operating system leads people to expect.
The practical point: when a dialog is hidden or clipped inside a complex layout, that is almost always a property of how it was built rather than of the library. Moving to the native element removes that whole class of bug instead of treating each case.
Decision 2 — return a promise rather than blocking the page
The browser's confirm() freezes everything. Forge Dialog replaces it with functions that return a promise, so the rest of the interface keeps running while the dialog is open:
const ok = await confirm("Delete this item?", { title: "Please confirm" })— resolvestrueorfalse, and resolvesfalsewhen the user presses Escape or clicks outside.const name = await prompt("What is your name?", { validate: (v) => v.trim() ? true : "Name is required" })— resolves the entered string, ornullif cancelled. Thevalidatefunction checks inside the dialog rather than closing first and reporting an error afterwards.await alert("Saved.", { title: "Success" })— a single-button box.
For anything more involved, open() returns an object with close(), update() and whenClosed(), so a dialog's content can change while it is open — useful for steps that load data.
The difference sounds small but changes how the product feels: with the old function, loading images stop, countdowns freeze, and animations stall until the user clicks. With the new one they do not.
Decision 3 — fourteen entry points, so you pay for what you use
Most dialog libraries publish a single bundle: import it and you get everything. If your product only needs a confirmation box, you still download the drawer, command palette and image viewer code.
Forge Dialog splits by feature. Import what you need:
forgedialog/alert,forgedialog/confirm,forgedialog/prompt— the three basic dialogs, each entry pulling in only its own code.forgedialog/core— the full engine, for cases that needopen()and deeper customisation.forgedialog/workflows,/presentation,/interactions,/animations,/appearance— advanced feature groups, imported only when used.
The styling is split the same way: instead of one CSS file there is forgedialog/style/core.css for the core plus separate files for forms, toasts, lightboxes, command palettes and dragging.
Framework adapters ship inside the package — and why we changed approach
For Forge Select we published the React and Vue wrappers as two separate npm packages. For Forge Dialog we did not: the adapters sit inside the main package as subpath entries.
- React:
import { useDialog } from "forgedialog/react" - Vue:
import { useDialog } from "forgedialog/vue" - Svelte:
import ... from "forgedialog/svelte" - No framework at all:
forgedialog/web-component
The reason for the change is practical: separate packages mean two version numbers that have to stay aligned. Every time the main package releases, the wrapper has to follow, and users have to remember to upgrade both. Inside one package there is no version drift, and React and Vue are declared as *optional* peer dependencies — installing Forge Dialog in a framework-free project pulls in nothing.
Safe by default with content
Any component that accepts content and renders it onto a page is a potential attack surface. Forge Dialog picks strict defaults:
- A string passed to
contentalways renders as text. If it contains HTML tags, the user sees those tags rather than the markup they would produce. - Injecting real HTML requires passing a filter:
open({ html: content, sanitizeHtml: (h) => DOMPurify.sanitize(h) }). Without a filter, nothing is injected. - The
unsafeHtmlfield remains for content your own system generated and trusts, but it has to be called by exactly that name. That is deliberate: a field namedunsafeHtmldoes not get used by accident in a hurry.
More than dialogs
The same engine builds other components that tend to be rewritten from scratch on every project: drawers sliding in from a screen edge, mobile bottom sheets with swipe-to-dismiss, lightboxes, toast notifications, keyboard-invoked command palettes, and multi-step wizards that remember their state so users return to where they left off.
Appearance customisation is per-dialog rather than global: separate colour and opacity for surface, title, content and border; corner radius settable per corner; shadows composed from angle, distance and blur rather than written as a CSS string. Dialogs can be dragged by mouse or keyboard, constrained to the viewport, and remember their position for next time.
On accessibility, two points worth noting: motion effects respect the operating system's reduced-motion setting, and dialogs are draggable with the arrow keys rather than by mouse only.
Where it stands, plainly
Forge Dialog is at version 0.7.0, published to npm on 1 September 2026. That is its first npm release, although the repository history goes back to 0.4. A version number below 1.0 means what it usually means: the programming interface can still change between minor releases, and we record every change in the CHANGELOG with migration notes.
If you need a library that has been stable for years for a system nobody will touch again, this is not it yet. If you are building a product and want a component that depends on no framework, pulls in nothing, and whose source you can read in an afternoon, it is worth a try.
Try it in two minutes
- See the live demo and documentation at forgedialog.konexforge.com.
- Install with
npm install forgedialog, or use it straight from a<script>tag if the project has no build step. - Full source on GitHub under MIT — usable in commercial projects, no permission needed, no attribution required.
Forge Dialog and our other open-source projects are listed in full — versions, licences, how to contribute — on the Community page. It is also the kind of component we reuse in the Development layer of projects that need a dependable interface layer without rebuilding one.
Related articles
Forge Select: a zero-dependency, framework-agnostic open-source replacement for Select2
Select2 has served the web community for years, but it's built on jQuery — a dependency that's increasingly hard to justify on a modern stack. Forge Select is the select/combobox component we built ourselves: a 5-file core, zero runtime dependencies, yet full-featured enough for real products — virtual scroll, tree select, tags, and full accessibility.
Autumn Note: why we built our own zero-dependency, open-source rich-text editor
Most internal systems — CMS, admin portals, knowledge bases — need a formatted text editor somewhere. Instead of pulling in jQuery or a heavy commercial library, we built Autumn Note: a vanilla ES2022 WYSIWYG editor, zero dependencies, MIT-licensed and open source.
KonexForge Themes: a VS Code theme family with a 'forge' aesthetic — not another One Dark clone
Engineers stare at their editor all day, yet the VS Code theme market is saturated with variants that look nearly identical to One Dark Pro. KonexForge Themes is the open-source set of 4 themes we built ourselves — not to sell, but because a distinct visual identity is part of engineering culture too.