
apertura is a vanilla JavaScript modal library that morphs the element that opens a dialog into the dialog itself.
A clicked button lifts from its original position, travels across the viewport, expands into the modal, then returns when the dialog closes.
Features
- Trigger-to-dialog morph with reverse closing motion.
- Spring animation for position and size changes.
- Confirm, alert, prompt, and custom-content dialogs.
- Center, anchor, in-place, and bottom placements.
- Snappy, floaty, and cinematic motion presets.
- Promise-based dialog results and async close checks.
- Drag, overlay, and Escape-key dismissal controls.
- Focus trapping, focus restoration, and scroll locking.
- CSS custom properties for colors, spacing, typography, and sizing.
See It In Action
How To Use It
Installation
apertura is not published on npm yet. Current usage starts from a local clone. The repository uses ES modules and Vite for its demos.
If a Vite application imports src/index.js from a local copy, that entry loads the core stylesheet as part of the module. The package-style apertura and apertura/style.css imports belong to the project’s built package interface.
git clone https://github.com/antuuanyf/apertura.git cd apertura npm install # Vanilla JavaScript demo npm run demo # Angular demo npm run demo:angular
Basic Usage
Assume the repository is available under ./vendor/apertura/ in a Vite project. Pass the clicked element through origin to start the morph from that control.
modal.open() returns a Promise-like handle. The confirm action resolves true, cancel resolves false, and Escape, an overlay click, or drag dismissal resolves undefined. Leave out origin when no visible control caused the dialog to open.
import { modal } from './vendor/apertura/src/index.js';
document.querySelector('#remove-account').addEventListener('click', async (event) => {
const confirmed = await modal.open({
origin: event.currentTarget,
title: 'Remove this account?',
description: 'Saved preferences will be deleted.',
confirmLabel: 'Remove',
cancelLabel: 'Keep',
variant: 'danger'
});
if (confirmed) {
removeAccount();
}
});
Advanced Usages
Confirm, Alert, and Prompt
modal.confirm() creates an OK/Cancel dialog. modal.alert() keeps the confirm action and removes Cancel. modal.prompt() adds a text field and resolves with its entered value.
const approved = await modal.confirm({
origin: event.currentTarget,
title: 'Publish this page?',
variant: 'warning'
});
await modal.alert({
origin: event.currentTarget,
title: 'Changes saved'
});
const projectName = await modal.prompt({
origin: event.currentTarget,
title: 'Project name',
placeholder: 'Dashboard redesign',
defaultValue: 'New project'
});
Placement and Size
center is the default placement. anchor keeps the dialog close to its trigger, inplace expands over that location, and bottom uses a sheet layout. A missing origin sends anchor, inplace, and bottom back to centered placement.
The built-in width presets are 320px for sm, 440px for md, and 640px for lg, each constrained by the viewport. A number becomes a pixel width, and a CSS max-width string is also accepted.
modal.open({
origin: event.currentTarget,
title: 'Notification settings',
placement: 'anchor',
size: 'sm'
});
modal.open({
origin: event.currentTarget,
title: 'Responsive editor',
placement: 'bottom',
size: 'min(720px, calc(100vw - 40px))'
});
Custom Content
content accepts an HTML element or an HTML string. A render callback has direct access to the dialog body and receives { close, id }. Call close() with the value that should resolve the dialog Promise. The callback may return a cleanup function.
const result = await modal.open({
origin: event.currentTarget,
render(body, { close }) {
const input = document.createElement('input');
input.placeholder = 'Enter a label';
const save = document.createElement('button');
save.type = 'button';
save.textContent = 'Save';
save.addEventListener('click', () => close(input.value));
body.append(input, save);
}
});
Check Before Closing
beforeClose runs before the dialog leaves the screen. Return false to keep it open.
modal.open({
origin: event.currentTarget,
title: 'Save changes?',
confirmLabel: 'Save',
async beforeClose(result) {
if (result !== true) {
return;
}
const saved = await saveSettings();
return saved ? true : false;
}
});
Manage Open Dialogs
Every open() call returns a handle with an id. update() patches an active dialog, close() closes a specific dialog or the topmost dialog, and closeAll() closes the stack. destroy() resolves pending dialogs with undefined and removes the modal host.
const dialog = modal.open({
title: 'Uploading file',
description: 'Preparing upload...'
});
modal.update(dialog.id, {
description: 'Upload is almost complete.'
});
modal.close(dialog.id, 'finished');
modal.close();
modal.closeAll();
modal.destroy();
Isolated Instances
createModal() creates another modal store and host. Set mountTo when the instance is created if its layer should live under a specific application root.
import { createModal } from './vendor/apertura/src/index.js';
const workspaceDialogs = createModal({
mountTo: '#workspace'
});
workspaceDialogs.open({
title: 'Workspace settings'
});
Dialog Options
origin(HTMLElement | null): Element used as the opening morph origin. Default isnull.originStyle(object | null): Overridesbackground,boxShadow, orborderRadiusread from the origin.closeOrigin(HTMLElement | null): Element used as the closing destination. Defaults toorigin.title(string): Heading for the default dialog card.description(string): Body text for the default dialog card.confirmLabel(string | null): Confirm button label. Default isOK.nullhides the button.cancelLabel(string | null): Cancel button label. Default isCancel.nullhides the button.variant(string): Confirm-button variant. Built-in values areneutral,danger,success, andwarning.dismissible(boolean): Controls Escape, overlay click, and drag dismissal. Default istrue.gesture(boolean): Controls drag dismissal. Default istrue.labelledBy(string): Sets the ID used byaria-labelledby.ariaLabel(string): Supplies a fallback accessible label when noaria-labelledbyrelationship is available.content(HTMLElement | string | null): Replaces the default card body with a DOM node or HTML string.render(function | null): Builds custom body content and receives{ close, id }.morph(string | object | null): Usessnappy,floaty,cinematic, or a custom Morph configuration.size(string | number | null): Acceptssm,md,lg, a pixel number, or a CSS max-width value.placement(string): Acceptscenter,anchor,inplace, orbottom.beforeClose(function | null): Runs before closing and may keep the dialog open by returningfalse.placeholder(string): Placeholder used bymodal.prompt().defaultValue(string): Initial value used bymodal.prompt().
Host Configuration
modal.configure() updates animation, stacking, placement, and Morph values for the shared host. Set mountTo when calling createModal() because the mount location belongs to instance creation.
modal.configure({
placementGap: 12,
placementPadding: 20,
morph: {
stiffness: 160,
damping: 16
}
});
overlayDuration(number): Overlay fade duration. Default is0.35.enterDuration(number): Non-morph opening duration. Default is0.5.enterDistance(number): Opening vertical offset. Default is16.enterScale(number): Initial non-morph scale. Default is0.94.enterBlur(number): Initial non-morph blur. Default is8.exitDuration(number): Non-morph closing duration. Default is0.28.exitDistance(number): Closing vertical offset. Default is10.exitBlur(number): Closing blur. Default is4.underScale(number): Scale used for dialogs below the active dialog. Default is0.96.underY(number): Vertical offset used for dialogs below the active dialog. Default is10.underDuration(number): Stack transition duration. Default is0.4.underSpring(object): Stack spring. Defaults to stiffness180, damping22, and velocity0.placementGap(number): Gap used by positioned dialogs. Default is8.placementPadding(number): Viewport padding used by placement calculations. Default is16.morph(object): Global Morph configuration.mountTo(string | HTMLElement): Mount point supplied when creating an isolated modal instance.
Morph Presets And Configuration
snappy, floaty, and cinematic change the main spring values. A custom object patches the current global Morph configuration for one dialog.
modal.open({
origin: event.currentTarget,
title: 'Fast response',
morph: 'snappy'
});
modal.open({
origin: event.currentTarget,
title: 'Custom motion',
morph: {
damping: 10,
velocity: 1800
}
});
stiffness(number): Travel spring stiffness. Default is144.damping(number): Travel spring damping. Default is14.velocity(number): Initial travel velocity. Default is2400.sizeStiffness(number): Width and height spring stiffness. Default is180.sizeDamping(number): Width and height spring damping. Default is22.sizeDuration(number): Size transition duration value. Default is0.32.radiusDuration(number): Corner transition duration. Default is0.32.contentDuration(number): Content scale and blur duration. Default is0.32.colorDuration(number): Background transition duration. Default is0.4.colorDelay(number): Background transition delay. Default is0.15.shadowDuration(number): Shadow transition duration. Default is0.6.shadowDelay(number): Shadow transition delay. Default is0.05.contentScale(number): Initial content scale. Default is2.contentBlur(number): Initial content blur. Default is8.maxDuration(number): Opening Morph safety limit in milliseconds. Default is1100.closeDamping(number): Travel damping during close. Default is20.closeSizeDamping(number): Size damping during close. Default is26.closeVelocity(number): Initial closing velocity. Default is1400.closeContentDuration(number): Content hiding duration. Default is0.16.closeHandoffDuration(number): Final shell-to-origin handoff duration. Default is0.18.closeMaxDuration(number): Closing Morph safety limit in milliseconds. Default is900.closeRestDelta(number): Closing spring position threshold. Default is0.8.closeRestSpeed(number): Closing spring speed threshold. Default is8.
API Methods
// Open a dialog and return a Promise-like handle.
const dialog = modal.open(options);
// Open the standard helper dialogs.
const confirmed = await modal.confirm(options);
await modal.alert(options);
const value = await modal.prompt(options);
// Close one dialog or the topmost dialog.
modal.close(dialog.id, 'saved');
modal.close();
// Close every active dialog.
modal.closeAll();
// Patch an active dialog.
modal.update(dialog.id, {
description: 'Updated content'
});
// Change host configuration.
modal.configure({
placementGap: 12
});
// Remove the host and resolve pending dialogs.
modal.destroy();
Styling And Customization
Define the variables in your own stylesheet to change the theme.
-root {
--apr-bg: #18181b;
--apr-fg: #fafafa;
--apr-muted: #a1a1aa;
--apr-radius: 22px;
--apr-btn-bg: #fafafa;
--apr-btn-fg: #18181b;
}
Layout And Typography Variables
--apr-bg--apr-fg--apr-muted--apr-radius--apr-sheet-radius--apr-shadow--apr-padding--apr-max-width--apr-gap--apr-font-family--apr-font-size--apr-line-height--apr-font-weight--apr-title-size--apr-title-weight--apr-z-index
Overlay Variables
--apr-overlay--apr-overlay-blur--apr-inplace-overlay
Button And Variant Variables
--apr-btn-radius--apr-btn-bg--apr-btn-fg--apr-danger-bg--apr-danger-fg--apr-success-bg--apr-success-fg--apr-warning-bg--apr-warning-fg
Alternatives
- Vanilla JavaScript Alert/Confirm Modal Dialog Library – Modal.js
- Easily Build Confirmation Popups with the confirmDialog.js Library
- Customizable Alert/Confirm Modal Library – modal-alert.js
- Accessible Modal Dialog Component In Vanilla JavaScript – a11y-dialog







