NanoModal is a tiny library for creating modal dialogs using
the native <dialog>
element. It consists of
around 100 lines of TypeScript and weighs about 850 bytes minified
and gzipped.
Features:
- Fully accessible
- Customizable CSS animations
- Maintains body scroll position
- Prevents content jumps due to the scrollbar width
- Overlay click and
Escape
press close the modal - Scrollbar on full viewport and a sticky close button
-
Standardizes cross-browser
<dialog>
implementation quirks
It also inherits native dialog features:
- Focus trap
- Scroll restoration when a modal is opened again
- Focusing the first focusable element
- Restoring the focus on close
Examples:
Unstyled modal
Nano includes minimal styling with a fade animation. It is meant to be used with the provided base theme or a custom theme.
Base theme
The base theme is provided. It is simple and includes basic responsive styles for positioning, animation and border radius. Everything is easy to customize or override.
Padding and typography in this example are added separately.
Scrollable content
Nano uses the scrollbar on the full viewport for a nicer user experience. The optional close button is sticky by default.
Custom styles
You can customize the modal looks and animation using CSS. Check out the styling section below.
Nested modals
Nano allows you to open a modal from another modal. The current modal will closes automatically. Like Highlander, there can be only one.
Please note that when opening a modal from another, focus will not be restored to the original trigger once the last are closed. This is outside the scope of the library. However, you can use returned promises to implement it yourself.
Usage
Add the required HTML
<!-- Single backdrop element, all Nano modals share it -->
<div class="nano-modal__backdrop"></div>
<!-- Modal -->
<dialog
id="modal"
class="nano-modal nano-modal--base"
aria-label="NanoModal example"
>
<div class="nano-modal__wrapper">
<div class="nano-modal__content">
<!-- Optional close button -->
<button data-nm-close class="nano-modal__close-button">
Close
</button>
<!-- Content goes here -->
</div>
</div>
</dialog>
Add data attributes
To create an open button, add
data-nm-open="my-modal"
attribute to an element. The
value should be the id
of the modal you want to open
(my-modal
in the example below).
<button data-nm-open="my-modal">Open Modal</button>
To create a close button, add
data-nm-close
attribute to an element. As only one
modal can be opened at a time, no need to specify the modal id.
<button data-nm-close>Close Modal</button>
Initialize it
Then initialize Nano. It will attach all necessary event listeners to the elements with the data attributes.
// Import the library
import { init } from '@stanko/nano-modal';
// Add mandatory CSS
import "@stanko/nano-modal/style.css";
// Add the base theme (optional)
import "@stanko/nano-modal/base-theme.css";
// Initialize NanoModal
init();
If you dynamically add more elements with data attributes, call the
init()
function again.
API
If you want to control modals manually, the library provides three functions:
-
init()
: Attaches event listeners to open and close buttons with data attributes. -
open(modal: HTMLDialogElement)
: Opens the modal. Returns a promise that resolves when the modal is open. -
close()
: Closes the open modal, if any. Returns a promise that resolves when the modal is closed.
Here is a dummy example:
import { init, open, close } from "@stanko/nano-modal";
// Attaches events to all elements with data-nm-open/close attributes
init();
// Opens the contact modal
const openModal = document.querySelector("#contact-open");
const modal = document.querySelector("#contact-modal");
openModal.addEventListener("click", () => {
open(modal);
});
// Closes the modal on form submit
const form = document.querySelector("#contact-form");
form.addEventListener("submit", () => {
// Submit logic would go here
close();
});
Accessibility
As dialog element takes care of things like focus trapping, you only
need to provide accessible name. Either by using
aria-labelledby
:
aria-labelledby="id of visible HTML element"
or inline with aria-label
aria-label="..."
Providing a description is optional. You can use
aria-describedby
for a short description. For long
content, it is better to let the user navigate it instead.
Styling
Nano exposes a few CSS variables for tweaking the base styles.
:root {
/* Backdrop (global, shared between all modals) */
--nm-backdrop-color: rgb(0 0 0 / 0.7);
--nm-backdrop-filter: blur(10px);
--nm-backdrop-z-index: 1;
/* Scrollbar */
--nm-scrollbar-track-color: transparent;
--nm-scrollbar-thumb-color: rgb(255 255 255 / 0.5);
/* Content */
--nm-radius: 20px; /* Border radius used for the base theme */
--nm-max-width: 50rem;
--nm-background: white;
/* Animation */
--nm-duration: 350ms;
/* Close button */
--nm-close-top: 0; /* Sticky position */
--nm-close-margin: 0;
}
These are very handy in combination with the base theme.
Custom theme
If you want to create a custom theme, start from the template below and refer to the base theme in the source code.
/* A custom theme */
.custom-modal[open] .nano-modal__content {
@starting-style {
/* Enter animation start */
}
}
.custom-modal[open].nano-modal--closing[open] .nano-modal__content {
/* Exit animation end */
}
.custom-modal .nano-modal__wrapper {
/* Content wrapper */
}
.custom-modal .nano-modal__content {
/* Content */
}
.custom-modal .nano-modal__close-button {
/* Close button */
}
/*
Because the backdrop is shared between all modals,
you'll have to target it like this in order to customize it only for a specific modal
*/
:root:has(.custom-backdrop-modal[open]) {
/* Backdrop */
}