layerpro
v4.1.0
Published
The real custom modals, alert, confirm, prompt... for your browser
Maintainers
Readme
LayerPro
LayerPro is a browser window system: it generates OS-style windows - draggable, resizable, maximizable, dockable - that can host any content, including React components.
It also replaces the browser's native alert(), confirm(), and prompt() with beautiful, fully customizable modals that work everywhere - no more blocking the main thread, no more default browser styling.
LayerPro is not only a replacement for alert/prompt/confirm: the same window engine is used to build rich, interactive windows on top of your app.
Table of Contents
- Features
- Installation
- Quick Start
- Usage
- Options
- Global Overrides
- Methods
- Styling & z-index
- Overriding the CSS
- Dock
- Security
- License
Features
- Zero runtime dependencies - no jQuery, pure vanilla DOM helpers;
react/react-domare peer dependencies, only loaded when React bodies are used, powered byreattach - Native
<dialog>windows - every popup is a real<dialog>element, so the platform owns the window semantics (open state, implicit dialog role) while LayerPro keeps the geometry, the drag and the stacking order - OS-style window engine - not just modals: every popup is a real window with a title bar, drag handle, resize handles, maximize/restore, minimize-to-dock, focus stacking, and a z-order you control
- Hosts any content - plain text, HTML strings, or full React components (with Context bridging and DOM auto-healing), rendered inside the window body
- Multiple modal types - alert, confirm, prompt, message - built on top of the same window engine, so they inherit the same styling, dock, and accessibility features
- Fully customizable - appearance, behavior, and content
- Dockable windows (taskbar-style) - click a dock icon to restore, focus, or minimize, just like an OS taskbar
- Draggable & resizable - instant, with no transition lag while resizing
- CSS-only maximize - uses
position: fixed+ insets, so it follows the viewport with no JS resize listeners - Fade in/out transitions for open/close
- Full TypeScript support
Installation
npm install layerproOr use directly in HTML:
<script src="https://cdn.jsdelivr.net/npm/layerpro"></script>Quick Start
Import LayerPro once, in your app's entry point:
import "layerpro";That's it - layerpro.popup(), layerpro.alert(), layerpro.confirm(), layerpro.prompt(), layerpro.message() and layerpro.version() are now available. Import layerpro/global instead if you also want the browser's own alert/confirm/prompt overridden (see Global Overrides).
The popup alias is a shorthand for layerpro.popup:
import "layerpro";
// Both work identically:
layerpro.popup.open({ id: 'myPopup', body: 'Hello World' });
popup.open({ id: 'myPopup', body: 'Hello World' });Usage
Basic Modals
layerpro.alert("Hello World!"); // Promise<void>
layerpro.message("Operation complete!"); // Promise<void>
const confirmed = await layerpro.confirm("Are you sure?"); // Promise<boolean>
const name = await layerpro.prompt("What's your name?"); // Promise<string>Note: with the default
import "layerpro"entry the browser's ownalert/confirm/promptare not replaced. Useimport "layerpro/global"to opt in to the global overrides, or call thelayerpro.*methods directly as shown above.
Custom Popups
popup.open({
id: 'myModal',
name: 'My Modal',
body: '<p>Your content here</p>',
width: 450,
height: 350,
buttons: {
confirm: { text: "OK", cb: () => {} },
cancel: { text: "Cancel", cb: () => {} }
}
});OS-style Windows
A popup is not limited to a modal dialog. Turn on movable, resizable, maximize, iconize and dockable and it becomes a real OS-style window: draggable by its title bar, resizable from the edges, maximizable, minimizable into the dock, and stackable with the other windows on the page.
popup.open({
id: 'dashboard',
name: 'Dashboard',
icon: '📊',
body: '<h2>Live metrics</h2><p>Anything you want inside.</p>',
width: 640,
height: 420,
movable: true,
resizable: true,
maximize: true,
iconize: true,
dockable: true,
close: true
});Combined with a React component, this becomes a real application window - a dashboard, a settings panel, a file browser - rendered inside the browser page with native-looking chrome:
popup.open({
id: 'settingsWindow',
name: 'Settings',
icon: '⚙',
body: <SettingsPanel />,
width: 720,
height: 480,
movable: true,
resizable: true,
maximize: true,
iconize: true,
dockable: true,
close: true
});Tip:
modal: trueis what turns a window into a blocking dialog. Leave it off for windows the user should be able to move around while working on the page.
React Components
LayerPro's React adapter is powered by reattach, a zero-dependency library for mounting React components anywhere in the DOM. It handles root creation, automatic root reuse on duplicate-id reopen, Context Bridging across detached roots, and auto-healing DOM recovery - all without extra setup.
popup.open({
id: 'reactModal',
name: 'React Modal',
body: <MyComponent />,
width: 500,
height: 400
});Advanced Options (React)
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| inheritContext | boolean | false | Replays captured host React Context into the popup's root. Use captureContext() in your app to capture contexts, then set inheritContext: true on the popup. |
| resilient | boolean | false | Enables a MutationObserver that auto-heals the React mount container if external DOM mutations (htmx, jQuery, CMS AJAX) remove or replace it. |
// Host app: capture context
import { captureContext } from 'reattach'
function App() {
const theme = useContext(ThemeContext)
captureContext(ThemeContext, theme)
return <Toolbar />
}
// Popup: inherit host context
popup.open({
id: 'contextual',
body: <ThemedWidget />,
inheritContext: true,
resilient: true,
});The body Parameter
| Type | Example |
|------|---------|
| Plain text | body: "Hello" |
| HTML string | body: "<p>Hello</p>" |
| React element | body: <Component /> |
Note: in React, always use elements (
<Component />), not function calls (Component()).
Targeting a window
A layer is drawn in the document of the page that loaded LayerPro. The
target option names the window a layer belongs to, so a window opened with
native: true can host its own layers instead of seeing them land on the
opener:
// Open a real browser window
await layerpro.popup.open({ id: 'desk', name: 'Desktop', native: true })
// Open a layer inside that window
await layerpro.popup.open({ id: 'detail', body: 'Inside the desk', target: 'desk' })
// The same thing, through the handle bound to that window
const desk = layerpro.window('desk')
await desk.popup.open({ id: 'detail2', body: 'Also inside' })
// The modals accept a target too
await layerpro.confirm('Save the changes?', { target: 'desk' })A target is the id of a native window, a Window object, or a
{ win, doc } scope. A target that cannot be reached (unknown id, closed
window, another origin) produces a warning and the layer is drawn in the
current document.
Options
{
id: string, // Unique ID (auto-generated as popup_1, popup_2, ... if omitted)
body: any, // Content (text, HTML, or React)
name: string, // Modal title
icon: string, // Icon glyph - HTML entity (e.g. "⚠") or HTML string
// Dimensions (defaults: width 340, height 220; minWidth 340, minHeight 220)
width: number,
height: number,
minWidth: number,
minHeight: number,
maxWidth: number,
maxHeight: number,
// Remote content (same-origin only, enforced). Rendered as raw HTML;
// sanitize server-side if the endpoint is not fully trusted.
source: string,
// React adapter (via reattach)
inheritContext: boolean,
resilient: boolean,
// Position (numbers are px; strings applied as-is, e.g. "10%")
top: number | string,
left: number | string,
right: number | string,
bottom: number | string,
center: boolean, // Center the popup (adds a small cascade offset per open window)
// Animation
fadeIn: number, // open fade duration (ms, default 100)
fadeOut: number, // close fade duration (ms, default 100)
timer: number, // auto-close after N ms
// Features
movable: boolean,
resizable: boolean,
maximize: boolean, // double-click title bar to maximize
iconize: boolean, // allow minimize to dock
dockable: boolean, // show an icon in the dock bar
close: boolean, // show the close button
modal: boolean, // full-screen overlay that blocks pointer events beneath
// Buttons
buttons: {
confirm?: { text: string; cb?: () => void },
cancel?: { text: string; cb?: () => void }
}
// The window the layer is drawn in: the id of a native window, a Window
// object, or a { win, doc } scope. Without it the layer is drawn in the
// document of the page that loaded LayerPro.
target?: string | number | Window | LayerScope
}
modalinjects a full-viewport overlay (popup_modal_<id>) that intercepts every pointer event - the page underneath becomes unclickable until the popup closes. Usemodal: truefor alert/confirm/prompt/message pop ups.
Duplicate id - opening a popup whose
idalready exists brings that window to the front and refreshes itsbody/buttonsinstead of doing nothing. The refresh reuses the existing button row: the row is emptied and refilled, never stacked.
Global Overrides (Opt-in in 4.0)
In 4.0, import "layerpro" no longer overrides the native alert, confirm,
and prompt globals. The library exposes its own methods on window.layerpro
and provides Promise-based alternatives. To override the browser globals, use
the explicit entrypoint:
// Default entry - library installed, globals NOT overridden
import "layerpro";
layerpro.alert("Hello World!"); // Promise<void>
const confirmed = await layerpro.confirm("Sure?"); // Promise<boolean>
const name = await layerpro.prompt("Name?"); // Promise<string>
layerpro.message("Done!"); // Promise<void>Override browser globals (opt-in)
If you need window.alert / window.confirm / window.prompt / window.message
replaced with LayerPro's Promise-based versions, import the global entrypoint:
import "layerpro/global"; // installs Promise-based overrides on window
// These now return Promises instead of native values:
await alert("Hello World!"); // Promise<void>
const ok = await confirm("Sure?"); // Promise<boolean>
const val = await prompt("Name?"); // Promise<string>⚠️ Important: overridden
confirm()returnsPromise<boolean>, not a synchronousboolean. Code that doesif (confirm("..."))must beawait-ed. Only uselayerpro/globalif you control all call sites.
Methods
| Method | Description |
|--------|-------------|
| popup.open(props) | Open a modal; returns Promise<GenLayerProps> - resolves when the popup closes. |
| popup.close(id) | Close (and unmount) a modal. |
| popup.center(id, offset?) | Center a modal in the viewport; optional pixel offset. |
| popup.maximize(props) | Toggle maximize (pure CSS). |
| popup.iconize(id) | Toggle minimize to the dock. |
| popup.index(id) | Bring a modal to the front (select). |
| popup.movable(props) | Enable dragging. |
| popup.resizable(props) | Enable resizing. |
| layerpro.installGlobals() | Override window.alert/confirm/prompt/message with LayerPro's Promise-based versions. |
| layerpro.window(target) | A handle bound to one window: win, doc and a popup API whose layers are drawn in that window. |
| layerpro.version() | Returns the current LayerPro version string (read from app.json). |
| layerpro.config(opts) | Runtime config. Currently supports { nonce: string } for CSP; sets window.__layerproNonce consumed by the injected <style>. Call before importing LayerPro for the nonce to be present at injection. |
Styling & z-index
Popups live in a #popup container whose base z-index comes from the --popup-z-index CSS variable (default 1000). Apps with their own stacking contexts can override it to shift the whole stack:
:root { --popup-z-index: 100000; }The title bar, window surface, internal area, and body all read from CSS custom properties, so theming needs no !important or fragile class selectors:
| Variable | Applies to |
|----------|-----------|
| --bar-background | title bar |
| --bar-selected-background | active/selected title bar |
| --bar-color | title bar text |
| --window-background | popup surface |
| --window-shadow | popup shadow |
| --window-border-radius | popup border radius |
| --internal-background | body wrapper |
| --body-color | body text color |
| --modal-background | modal overlay |
| --popup-z-index | base z-index for the stack |
Additional variables available for buttons, inputs, resize handles, and the dock:
| Variable | Applies to |
|----------|-----------|
| --button-background-color / --button-color | confirm/cancel buttons |
| --input-background-color / --input-color | prompt input field |
| --inputButton-fontSize | button/input font size |
| --resize-background-color / --resize-shadow | resize handles |
| --dock-background-color / --dock-height | taskbar strip |
| --dock-sideLeft / --dock-sideCenter / --dock-sideRight | dock alignment |
| --dock-icon-width | dock icon width |
| --transition-time | fade/hover transitions |
| --border-radius / --corner-size / --corner-border-radius | resize handle shape |
| --hover-opacity / --hover-filter | hover feedback |
Overriding the CSS
LayerPro ships its styles injected at runtime into a <style> tag (no separate CSS import needed). There are three ways to restyle it, from the most recommended to the most surgical.
No inline styles. LayerPro never writes a plain inline style on the elements it renders. Every declaration lives in the stylesheet and is driven by a class (the state) or by a CSS custom property (the value). That means you can restyle any part of a window with a plain CSS rule - nothing needs
!importantto win over an inline style.
1. CSS custom properties (recommended)
Every visual detail is driven by a CSS variable declared on :root. Override the variable and every popup updates - no selectors, no !important:
:root {
--bar-background: #1f2937;
--bar-selected-background: #2563eb;
--bar-color: #f9fafb;
--window-background: #111827;
--window-border-radius: 4px;
--internal-background: #0b1220;
--body-color: #e5e7eb;
--button-background-color: #2563eb;
--button-color: #ffffff;
}Variables can also be scoped per popup subtree, because each window is rendered inside #popup:
#popup .popup_window_myDarkPopup {
--window-background: #000;
--bar-background: #000;
}2. Target the generated classes
Every element LayerPro creates carries a stable class alias next to its id-suffixed class, so you can target windows, bars, bodies, and buttons without depending on generated ids:
| Element | Stable class | Id-suffixed class |
|---------|--------------|-------------------|
| Window surface (<dialog>) | .popup_window | .popup_window_<id> |
| Title bar | .popup_bar | .popup_bar_<id> |
| Drag handle (title) | .popup_drag | .popup_drag_<id> |
| Icon slot | .popup_icon | .popup_icon_<id> |
| Option buttons | .popup_option | .popup_option_<id> |
| Internal area | .popup_internal | .popup_internal_<id> |
| Body | .popup_body | .popup_body_<id> |
| Modal overlay | .popup_modal | .popup_modal_<id> |
| Positioned wrapper | .popup_overflow | .popup_overflow_<id> |
| Confirm/cancel row | .buttons | - |
| Selected/focused window | .zHigh, .popup_bar_selected | - |
The confirm/cancel row built from the buttons option lives in the internal
area (.popup_internal), next to the body, and not inside the body itself:
a body renderer (React) owns the body container and empties it when it
commits, so a row kept inside the body would be wiped by the first render.
The built-in alert/confirm/prompt/message modals carry their own row
inside their body markup, as part of the modal content. Target the row with
.buttons (or .lp_confirm / .lp_cancel for the single buttons), never
with .popup_body .buttons.
/* square windows, no rounded corners */
#popup .popup_window {
border-radius: 0;
}
/* restyle the confirm/cancel row */
#popup .buttons button {
border-radius: 6px;
background: #2563eb;
}
/* make the close button red on hover */
#popup .popup_option .close:hover {
background: #dc2626;
}
/* change the dock strip */
#popup_dock {
background: #111827;
}Note: some internal rules use
!important(for example the active-window border and the built-in icons). For those, use the cascade layer approach below instead of!important.
State classes
The visual states of a window are classes too, so you can restyle them like any other element:
| Class | Applied when | Effect |
|-------|--------------|--------|
| .popup_maximized | the window is maximized | fills the viewport above the dock |
| .popup_iconized | the window is minimized to the dock | collapsed on the taskbar |
| .popup_dimmed | another window has the focus | pushed to the back (opacity .5) |
| .popup_content_hidden | another window has the focus | body content not rendered |
| .popup_content_visible | the window has the focus | body content always rendered |
| .lp_hidden | the element is hidden | display: none |
| .lp_fade_in / .lp_fade_out | during a fade | opacity transition |
| .lp_no_pointer | while dragging/resizing | pointer-events: none |
| .lp_no_touch | on the drag/resize handles | touch-action: none |
| .lp_no_transition | while resizing | transitions disabled |
/* a maximized window with a different look */
#popup .popup_maximized {
inset: 0;
bottom: 0;
}
/* a dimmed window that stays readable */
#popup .popup_dimmed {
opacity: .8;
}The geometry of a window (position and size) is carried by the custom properties below, so it can be overridden too:
| Property | Value |
|----------|-------|
| --lp-top / --lp-left | window position |
| --lp-right / --lp-bottom | window position (alternative anchors) |
| --lp-width / --lp-height | window size |
| --lp-min-width / --lp-min-height | minimum size |
| --lp-max-width / --lp-max-height | maximum size |
| --lp-z-index | stacking order |
/* every window opens at a fixed position */
#popup .popup_overflow {
--lp-left: 50%;
--lp-top: 100px;
}3. Cascade layers (full control)
All LayerPro styles live inside the CSS cascade layer layerpro, so a host app can restyle the popups without !important or fragile selectors.
/* host app: plain rules beat every layer */
#popup { border-radius: 4px; }Precedence rules:
Unlayered host rules always win over anything inside
layerpro.Host layers must be declared before
layerproto outrank it. Declare the full order in the host stylesheet, otherwise the host layer is created after LayerPro's and loses:@layer app, layerpro;!importantinverts layer order. A host!importantrule is weaker than LayerPro's!importantrules, because LayerPro's live inside a layer and host!importantrules are unlayered. Do not rely on!importantto override LayerPro - use plain rules or a host layer declared beforelayerpro.
Dock
Popups that can be minimized share a dock, a taskbar-like strip pinned to the bottom of the viewport. The dock is mounted directly in <body> and it stays visible while the page scrolls.
While the dock is mounted LayerPro puts the layerpro-dock class on <html> and <body>:
html.layerpro-dock { height: 100%; overflow: hidden; }
body.layerpro-dock { position: absolute; top: 0; right: 0; bottom: var(--dock-height); left: 0; overflow-y: auto; }The body is lifted from the bottom, so the strip used by the dock stays free and the page content is never covered by it. The class is removed as soon as the last popup is closed, so the host page is left exactly as it was. No inline style is written on the host page.
Note: while the dock is open the body is the scrolling element, so
window.scrollYstays0and page level scroll listeners do not fire. Host the scrolling in an inner container if the page needs it.
Security
- Same-origin fetch guard - the
sourceprop only accepts same-origin URLs; cross-origin requests are rejected before any network call is made, preventing SSRF and data exfiltration. - HTML escaping - all user-provided text content (button labels, body content, titles) is automatically escaped via
escapeHtml()to prevent XSS. - CSP-friendly - no inline scripts or
eval()are used. React components are rendered viareattach.mount()(backed bycreateRoot()). For strict CSP environments, the injected stylesheet auto-detects the nonce fromwindow.__layerproNonce, existing<style>/<script>elements, ordocument.body.dataset.nonce. Alternatively, import the stylesheet as a static file viaimport 'layerpro/style.css'to avoid inline style injection entirely. Uselayerpro.config({ nonce })for explicit nonce control. - No global side effects -
import 'layerpro'does NOT overridewindow.alert/confirm/prompt. Useimport 'layerpro/global'orlayerpro.installGlobals()to opt in to the global overrides.
License
MIT License
Copyright (c) Dario Passariello
