hcg-modal
v1.0.0
Published
Lightweight, dependency-free JavaScript popup modal in vanilla JS - sizes, positions, animations, focus trap, scroll lock, stacking, and bring-your-own-box support.
Maintainers
Readme
hcg-modal - JavaScript Popup Modal (Vanilla JS)
hcg-modal is a lightweight, dependency-free popup modal / dialog library written in plain JavaScript. It gives you a ready-made, accessible modal window - the kind you use for confirmations, alerts, forms, image lightboxes, cookie notices, or any "pop up over the page" content - without pulling in jQuery, a framework, or a build step.
Add one CSS file and one JS file, call hcgModal({ ... }), and you get a fully featured
dialog: size and position control, custom width, scroll handling for long content,
CSS-driven animations, an accessible focus trap, body scroll lock, layered stacking,
footer buttons, promise-based results, an auto-close timer, and the option to use your
own HTML as the dialog. It works on desktop and mobile (including back-button close), is
roughly a few kilobytes, and is themeable through CSS custom properties. A thin React
wrapper is included for React projects.
Features
- Zero dependencies - plain JavaScript and CSS, no jQuery, no build step
- Size variants:
small,medium,large,fullscreen - Custom width - any pixel number or CSS length (
600,'70%','40rem'), capped to the viewport - Positions:
center,top,bottom - Scroll modes -
scrollBody: truepins the header/footer and scrolls only the body;falsescrolls the whole dialog - Opt-in animations via
classNameutility classes:hcg-modal--anim-fade,hcg-modal--anim-slide,hcg-modal--anim-pop(respectprefers-reduced-motion), or bring your own - Footer action buttons with
primary/secondary/dangerstyles andonClickcallbacks - Promise result -
opened()resolves to the clicked button'svalue beforeCloseguard - veto or confirm a close (sync or async)- Auto-close timer with an optional progress bar that pauses on hover
- Enter-to-confirm -
confirmOnEntertriggers the primary button - Close on backdrop click, Escape key, close (X) button, and the browser/mobile back button - each toggleable
- Layered stacking - open modals on top of each other; Esc and back close them top-down
- Focus trap with focus restore,
role="dialog"/aria-modal, andaria-label/aria-labelledbysupport - Bring your own box - use any element or HTML string as the entire dialog, with auto-wired
.closeelements - No layout shift - scroll lock compensates for the scrollbar width so the page never jumps
- Themeable via CSS custom properties
- React wrapper included (
hcg-modal/react)
Install
Drop in the two files:
<link rel="stylesheet" href="hcg-modal.css">
<script src="hcg-modal.js"></script>Or install from npm:
npm install hcg-modalUsage
const modal = hcgModal({
title: 'Delete item?',
content: '<p>This action cannot be undone.</p>',
size: 'small',
buttons: [
{ text: 'Cancel', type: 'secondary', onClick: (m) => m.close() },
{ text: 'Delete', type: 'danger', onClick: (m) => { /* ... */ m.close(); } }
]
});
modal.open();Options
| Option | Default | Description |
| --- | --- | --- |
| title | '' | Header title (HTML or text). Omit to hide the header. |
| content | '' | Body content: HTML string, text, or any DOM Node. A Node is moved into the modal (not copied) — clone it first if it must remain in the page. |
| size | 'medium' | small, medium, large, or fullscreen. |
| width | null | Custom dialog width, overriding the size preset. Number → px (600), or any CSS length string ('70%', '40rem'). Still capped to the viewport. |
| position | 'center' | center, top, or bottom. |
| scrollBody | true | How large content scrolls. true: dialog capped to the viewport, header and footer pinned, only the body scrolls. false: the whole dialog scrolls inside the overlay. |
| closeOnBackdrop | true | Close when the backdrop is clicked. |
| closeOnEsc | true | Close when the Escape key is pressed. |
| closeOnBackButton | false | Close when the browser/mobile back button is pressed (uses the History API). Each stacked layer is closed by one back press. |
| beforeClose | null | (reason) => boolean \| Promise<boolean> called before any close; return false to cancel. reason: button, backdrop, escape, close-button, back, timer, or api. |
| timer | 0 | Auto-close after N milliseconds. 0 disables. |
| timerProgressBar | false | Show a shrinking progress bar for the timer (pauses while hovered). |
| confirmOnEnter | false | Pressing Enter triggers the primary footer button (ignored inside a textarea). |
| showClose | true | Show the header close (X) button. |
| ariaLabel | '' | Accessible name for the dialog (sets aria-label). Useful for a modal with no title. |
| ariaLabelledBy | '' | Id of an element that labels the dialog (sets aria-labelledby). Takes precedence over ariaLabel. |
| box | null | Use your own element or HTML string as the entire dialog (your own heading / content / footer). When set, title, content, buttons and showClose are ignored. A display:none/hidden element is revealed automatically. |
| className | '' | Extra class(es) added to the overlay, for animation and theming. Built-in: hcg-modal--anim-fade, hcg-modal--anim-slide, hcg-modal--anim-pop, hcg-modal--glass. |
| buttons | null | Array of { text, type, value, onClick(instance) }. type: primary, secondary, danger. value is what opened() resolves to when that button is clicked. |
| onOpen | null | Callback fired after the modal opens. |
| onClose | null | Callback fired when the modal closes. |
Security note:
titleandcontentaccept HTML strings and are inserted as markup (contentalso accepts a DOMNode). Never pass unsanitized user input to them, or you risk XSS. Button labels (buttons[].text) are inserted as plain text and are safe. Sanitize any user-derived HTML before passing it in.
Back-button note: with
closeOnBackButton,close()callshistory.back()asynchronously. Opening and closing modals extremely fast (e.g. programmatic toggles in SPA route changes) can race the history pop/push. In normal use this is not an issue; avoid rapid back-to-back open/close on the same tick if you rely on the back-button integration.
Instance methods
| Method | Description |
| --- | --- |
| open() | Open the modal. Returns the instance (chainable). |
| opened() | Open the modal and return a Promise that resolves when it closes, to the clicked button's value (or undefined when dismissed). |
| close(reason) | Close the modal. reason is optional and passed to beforeClose. |
| setContent(html\|node) | Replace the body content (HTML string or any DOM Node). A Node is moved into the modal — clone it first if it must stay in the page. |
| setTitle(html) | Replace the header title (creates the header if it was omitted). |
| isOpen() | Returns whether the modal is currently open. |
| destroy() | Immediately remove the modal and detach listeners. Unlike close(), it does not run beforeClose, play the exit animation, or fire onClose. |
Promise result, beforeClose, timer, Enter
Await a modal with opened() and give buttons a value:
const ok = await hcgModal({
title: 'Delete item?',
buttons: [
{ text: 'Cancel', value: false },
{ text: 'Delete', type: 'danger', value: true }
]
}).opened();
if (ok) deleteItem(); // dismissing (Esc / backdrop / X) resolves to undefinedGuard closing with beforeClose (return false, or a Promise<boolean>, to cancel):
hcgModal({
content: form,
beforeClose: () => !hasUnsavedChanges() || window.confirm('Discard changes?')
}).open();Auto-close as a toast, with Enter confirming the primary button:
hcgModal({ content: 'Saved', timer: 3000, timerProgressBar: true, showClose: false }).open();
hcgModal({ confirmOnEnter: true, buttons: [
{ text: 'Cancel', value: false },
{ text: 'OK', type: 'primary', value: true }
]}).opened();Animations
The modal has no animation or duration options. Instead, pass an animation class
through className and let CSS drive it. The modal reads the CSS transition time to
know when to remove itself, so the duration is whatever your CSS sets.
hcgModal({ content: 'Hi', className: 'hcg-modal--anim-pop' }).open();Built-in classes:
hcg-modal--anim-fade- plain opacity fade.hcg-modal--anim-slide- slides in; direction followsposition(top / center / bottom). Withposition: 'bottom'it slides up from the bottom edge like a sheet.hcg-modal--anim-pop- combined rise, scale and blur for a richer entrance.
The dialog uses a spring easing curve (cubic-bezier(0.34, 1.56, 0.64, 1)) on open.
Tune the speed with the --hcg-modal-duration variable, or write your own class
entirely:
.my-anim .hcg-modal-dialog { transform: translateY(40px); opacity: 0; }
.my-anim.hcg-modal--visible .hcg-modal-dialog { transform: none; opacity: 1; }hcgModal({ content: 'Hi', className: 'my-anim' }).open();Bring your own box
Pass a ready-made element (or HTML string) as the whole dialog with the box option.
The modal supplies only the backdrop, Esc / back-button close, scroll lock, focus trap
and stacking - your box owns its layout and close buttons.
<div id="myBox" style="display:none">
<div class="head">Heading</div>
<div class="body">Content…</div>
<div class="foot"><button class="x">Close</button></div>
</div>var box = document.getElementById('myBox').cloneNode(true); // clone to reuse
var modal = hcgModal({ box: box });
box.querySelector('.x').addEventListener('click', function () { modal.close(); });
modal.open();The modal moves the node into itself and removes it on close, so clone the original
if you want to reopen it. A display:none or hidden box is shown automatically.
Auto-wired close: any element inside the box (or content) with class .close or a
[data-hcg-close] attribute is automatically wired to close the modal - no JS needed.
Your own click handlers still work alongside it (close() is safe to call more than once):
<div id="myBox" style="display:none">
<div class="body">Content…</div>
<div class="foot">
<button class="close">Cancel</button> <!-- auto-wired -->
<button class="save">Save</button> <!-- wire yourself -->
</div>
</div>Theming
Override the CSS custom properties on .hcg-modal to restyle without editing the
stylesheet:
.hcg-modal {
--hcg-modal-bg: #1e1e1e;
--hcg-modal-color: #eaeaea;
--hcg-modal-primary: #8b5cf6;
--hcg-modal-radius: 14px;
--hcg-modal-z: 99999;
}Other hooks include --hcg-modal-overlay, --hcg-modal-shadow, --hcg-modal-border,
--hcg-modal-duration, and the per-type button colors.
Browser support
Modern evergreen browsers. Uses CSS custom properties and standard DOM APIs only.
License
MIT - HTML Code Generator (https://www.html-code-generator.com/)
