Building a modal sounds like a five-minute job. Drop a div over the page, add a backdrop, toggle a class, done. Then someone tries to use it with a keyboard and tabs straight past the dialog into the page behind it. A screen reader announces all the hidden content underneath. The Escape key does nothing. Closing the modal leaves focus stranded in the void. Suddenly that five-minute job is a week of edge cases.
Micromodal.js exists to make all of that someone else's problem. It is a tiny (~1.8kb minified and gzipped), dependency-free, vanilla-JavaScript library that wires up the accessibility and interaction behavior of modal dialogs for you. It traps focus, toggles the right ARIA attributes, restores focus when the modal closes, handles Escape and overlay clicks, and supports nested modals. What it does not do is impose any styling on you. You write semantic markup, point Micromodal at it, and keep complete control over how everything looks.
Because it operates on plain DOM nodes, micromodal is framework-agnostic. It works in a plain HTML page, and it slots in alongside React, Vue, Svelte, or Angular without any special integration layer.
Why Reach For It
The headline feature is accessibility, and that is genuinely the reason to use it instead of hand-rolling a div. When a modal opens, Micromodal:
- Traps tab focus inside the dialog, so Tab and Shift+Tab cycle through the modal's controls instead of escaping to the page behind it.
- Moves focus to the first focusable element in the modal automatically.
- Restores focus to whatever element triggered the modal once it closes.
- Toggles ARIA attributes like
aria-hiddenas the modal opens and closes. - Closes on the Escape key and on a click of the background overlay.
- Supports nested modals without you tracking focus stacks by hand.
On top of that, it is tiny and has zero dependencies, so the bundle-size cost is negligible. It ships with no CSS whatsoever, which means you are never fighting someone else's styles. And you can drive it two ways: declaratively through data attributes, or programmatically through a show/close API.
Getting It Into Your Project
Install it from npm or yarn:
npm install micromodal --save
# or
yarn add micromodal
If you would rather skip the build step, there is a CDN build:
<script src="https://cdn.jsdelivr.net/npm/micromodal/dist/micromodal.min.js"></script>
Writing Markup the Library Can Trust
Micromodal does not generate any HTML for you. Instead, you write an accessible modal structure once and let the library wire up the behavior. The structure matters: the container needs a unique id and must start hidden, the dialog needs the correct ARIA roles, and the close targets need a data attribute.
<!-- Trigger: the value must match the modal's id -->
<button data-micromodal-trigger="modal-1">Open Modal</button>
<!-- Container: unique id, hidden by default -->
<div id="modal-1" aria-hidden="true">
<!-- Overlay: data-micromodal-close makes background clicks dismiss the modal -->
<div tabindex="-1" data-micromodal-close>
<!-- Dialog: ARIA role plus a label pointing at the title -->
<div role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
<header>
<h2 id="modal-1-title">Modal Title</h2>
<button aria-label="Close modal" data-micromodal-close></button>
</header>
<div id="modal-1-content">
Modal Content
</div>
</div>
</div>
</div>
A few rules keep things working: the container must have a unique id and start with aria-hidden="true". Any element carrying data-micromodal-trigger="<id>" opens the matching modal, and any element carrying data-micromodal-close (the overlay, the X button) dismisses it. The aria-labelledby on the dialog should point at the id of your heading so assistive tech announces the modal by its title.
Bringing It To Life
With the markup in place, initialization is a single call:
import MicroModal from 'micromodal';
MicroModal.init();
That one line scans the page for trigger and close attributes and wires everything up. Click the trigger button, and the matching modal opens with focus trapped inside it. Press Escape or click the overlay, and it closes, returning focus to the trigger. You did not write a single line of focus-management code.
The only styling you strictly need is something to actually show and hide the modal. Micromodal adds a class (default is-open) when a modal opens, so the minimum stylesheet is:
.modal { display: none; }
.modal.is-open { display: block; }
Everything else — the backdrop dimming, the card shadow, the layout — is yours to design however you like.
Tuning the Behavior
The interesting part is the configuration object you can pass to init(). It lets you hook into the modal lifecycle and adjust how the library behaves:
import MicroModal from 'micromodal';
MicroModal.init({
onShow: (modal) => console.info(`${modal.id} is shown`),
onClose: (modal) => console.info(`${modal.id} is hidden`),
openClass: 'is-open',
disableScroll: true,
disableFocus: false,
awaitOpenAnimation: false,
awaitCloseAnimation: false,
debugMode: true,
});
A few of these earn their keep quickly. disableScroll: true locks the page behind the modal so the background does not scroll while a dialog is open. The onShow and onClose callbacks receive the modal element (and the trigger and event), which is the natural place to load data, reset a form, or fire analytics. If you use custom data attributes, openTrigger and closeTrigger let you rename data-micromodal-trigger and data-micromodal-close to whatever you prefer. And disableFocus: true opts out of the automatic focus-on-open when you have a reason to manage focus yourself.
Opening Modals From Code
The declarative trigger flow is convenient, but sometimes a modal needs to open in response to application logic — a timer, a failed network request, a route change, or a confirmation prompt that has no button behind it. For that, Micromodal exposes a programmatic API:
MicroModal.show('modal-1', {
onShow: (modal) => {
// fetch fresh data, reset state, etc.
},
onClose: (modal) => {
// clean up
},
disableScroll: true,
awaitCloseAnimation: true,
});
// Later, close it from anywhere:
MicroModal.close('modal-1');
MicroModal.show() accepts the same configuration object as init(), but scoped to that single invocation, so you can give one particular modal its own behavior without touching the global setup. This imperative style is especially handy in component frameworks, where you typically prefer calling show('id') and close('id') from event handlers rather than relying on DOM data attributes.
Animating Without the Flicker
Because Micromodal toggles a class, you can drive open and close animations purely in CSS. The catch is the close animation: by default the library removes the modal from view immediately, which cuts your fade-out short. The awaitCloseAnimation: true option fixes this by waiting for your CSS animation to finish before hiding the modal.
@keyframes mmfadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes mmfadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.modal__overlay {
animation: mmfadeIn 0.3s ease;
}
.modal[aria-hidden="false"] .modal__overlay {
animation: mmfadeIn 0.3s ease;
}
.modal[aria-hidden="true"] .modal__overlay {
animation: mmfadeOut 0.3s ease;
}
Pair those keyframes with MicroModal.init({ awaitCloseAnimation: true }) and the modal fades out gracefully instead of snapping shut. The companion awaitOpenAnimation: true does the same on the way in, delaying focus until the open animation has played so the focus ring does not appear mid-transition.
Using It With React
Since Micromodal works on plain DOM nodes, you can use it directly inside a React component. Render the modal markup as JSX, then call MicroModal.init() — or MicroModal.show(id) — inside a useEffect:
import { useEffect } from 'react';
import MicroModal from 'micromodal';
function Dialog() {
useEffect(() => {
MicroModal.init();
}, []);
const open = () => MicroModal.show('modal-1');
return (
<>
<button onClick={open}>Open Modal</button>
<div id="modal-1" aria-hidden="true">
{/* ...accessible modal markup... */}
</div>
</>
);
}
The main thing to watch for in React is re-renders that detach or recreate the modal node, and making sure your aria-hidden reflects component state. In practice many React users skip the declarative data-micromodal-trigger flow entirely and call show/close imperatively, since React already owns the DOM. If you would rather have an idiomatic React component with render props, there is a separate community package called react-micro-modal that wraps the same accessibility ideas — just note that it is a distinct project from the vanilla micromodal covered here.
Where It Fits Today
It is worth being honest about the landscape. Modern browsers now ship a native <dialog> element with showModal(), which gives you focus trapping, a backdrop, and Escape handling with no JavaScript library at all. That has genuinely eroded part of Micromodal's original niche. Another close relative is a11y-dialog, which shares the same tiny, unstyled, accessibility-first philosophy and is actively maintained.
So where does Micromodal still shine? Its sweet spot is "I want a correct, accessible modal in any stack, with almost no bundle cost and total styling freedom." The consistent ARIA wiring, the careful focus restoration, the overlay-click-to-close behavior, the animation hooks, and the cross-framework portability all add up to something dependable. If you want a modal that just works for keyboard and screen-reader users, looks exactly the way you designed it, and barely registers on your bundle analyzer, Micromodal.js remains a very easy library to recommend.