NanoModal

A sane, vanilla modal using the native <dialog> element

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 */
}

Unstyled Modal

Not much to see here, this is the minimal version without additional styling.

NanoModal

The base theme is simple and includes responsive styles for positioning, animation and border radius

Padding and typography in this example are added separately.

Long dummy content

NanoModal uses the scrollbar on the full viewport for a nicer user experience. The optional close button is sticky by default.

Open another modal from this modal

Nano allows you to open another modal from within a modal, but the current modal will be closed automatically. Like Highlander, there can be only one.

Another modal

This modal is opened from another modal. You can close it or open the previous modal.

Custom styling

You can use CSS to customize Nano and it's animations. Check the styling section for more details.