NETFRAME_DOCS // MODAL
GITHUB

Modal

Fixed dialog with backdrop overlay. Focus should be trapped inside while open.

Structure

MODAL PANEL

<!-- Overlay -->
<div class="nf-modal-overlay" id="modal-overlay" role="presentation">

<!-- Dialog -->
<div class="nf-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" tabindex="-1">
  <div class="nf-modal-header">
    <h2 class="nf-modal-title" id="modal-title">CONFIRM_ACTION</h2>
    <button class="nf-modal-close" aria-label="Close">✕</button>
  </div>
  <div class="nf-modal-body">
    <p>This action will purge all cached node data. Proceed?</p>
  </div>
  <div class="nf-modal-footer">
    <button class="nf-button nf-button-ghost">CANCEL</button>
    <button class="nf-button nf-button-danger">PURGE</button>
  </div>
</div>

</div>

Sizes

SIZE VARIANTS

nf-modal-sm 20rem
default 32rem
nf-modal-lg 48rem
nf-modal-xl 64rem
nf-modal-full 100vw × 100svh
<div class="nf-modal nf-modal-sm">...</div>  <!-- 20rem -->
<div class="nf-modal">...</div>              <!-- default 32rem -->
<div class="nf-modal nf-modal-lg">...</div>  <!-- 48rem -->
<div class="nf-modal nf-modal-xl">...</div>  <!-- 64rem -->
<div class="nf-modal nf-modal-full">...</div> <!-- 100vw × 100svh -->

JavaScript

const overlay = document.getElementById('modal-overlay');
const modal   = overlay.querySelector('.nf-modal');
const trigger = document.getElementById('open-modal');
const close   = modal.querySelector('.nf-modal-close');
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
let isOpen = false;

function openModal() {
  isOpen = true;
  overlay.classList.remove('nf-hidden');
  overlay.removeAttribute('aria-hidden');
  // Keep keyboard and screen-reader focus out of the page behind the dialog.
  overlay.parentElement.querySelectorAll(':scope > :not(#modal-overlay)').forEach((el) => (el.inert = true));
  modal.focus();
}

function closeModal() {
  if (!isOpen) return; // Escape on a closed modal must not steal focus.
  isOpen = false;
  overlay.classList.add('nf-hidden');
  overlay.setAttribute('aria-hidden', 'true');
  overlay.parentElement.querySelectorAll(':scope > [inert]').forEach((el) => (el.inert = false));
  trigger.focus();
}

trigger.addEventListener('click', openModal);
close.addEventListener('click', closeModal);
overlay.addEventListener('click', (e) => {
  if (e.target === overlay) closeModal();
});
document.addEventListener('keydown', (e) => {
  if (!isOpen) return;
  if (e.key === 'Escape') closeModal();
  if (e.key === 'Tab') {
    // Wrap focus inside the dialog.
    const items = [...modal.querySelectorAll(FOCUSABLE)];
    const first = items[0], last = items[items.length - 1];
    if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
    else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
  }
});

Place the overlay as a direct child of <body> so the inert step covers the rest of the page.

Class reference

Class Description
nf-modal-overlay Fixed full-screen backdrop — hide with nf-hidden or aria-hidden="true"
nf-modal Dialog panel — default max-width 32rem
nf-modal-sm 20rem
nf-modal-lg 48rem
nf-modal-xl 64rem
nf-modal-full Full viewport
nf-modal-header Top section with title and close button
nf-modal-title Title text
nf-modal-close Close × button
nf-modal-body Scrollable content area
nf-modal-footer Bottom action row