Modals

Glass Modal

A frosted-glass confirmation dialog with overlay dismiss and Escape-to-close.

HTML/CSS/JSavailableIntermediate
modaldialogglassoverlay
Author: Prism Bits @saiusesgithub
Live iframe preview
<button class="glass-modal-trigger" type="button" data-modal-open>Open modal</button>

<div class="glass-modal-overlay" data-modal hidden>
  <div class="glass-modal" role="dialog" aria-modal="true" aria-labelledby="glass-modal-title">
    <h2 class="glass-modal__title" id="glass-modal-title">Enable notifications?</h2>
    <p class="glass-modal__body">
      We'll ping you when a new component lands. You can turn this off any time.
    </p>
    <div class="glass-modal__actions">
      <button class="glass-modal__button glass-modal__button--primary" type="button" data-modal-close>
        Enable
      </button>
      <button class="glass-modal__button" type="button" data-modal-close>Not now</button>
    </div>
  </div>
</div>


<style>
.glass-modal-trigger {
  padding: 12px 22px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.08);
  color: #f4f5fb;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s ease, transform 0.2s ease;
}

.glass-modal-trigger:hover {
  background: rgba(255, 255, 255, 0.14);
  transform: translateY(-1px);
}

.glass-modal-trigger:focus-visible,
.glass-modal__button:focus-visible {
  outline: 2px solid rgba(167, 139, 250, 0.9);
  outline-offset: 2px;
}

.glass-modal-overlay[hidden] {
  display: none;
}

.glass-modal-overlay {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(5, 6, 12, 0.6);
  backdrop-filter: blur(6px);
  animation: glass-modal-fade 0.2s ease;
}

.glass-modal {
  width: min(100%, 380px);
  padding: 26px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 20px;
  background: linear-gradient(160deg, rgba(30, 32, 48, 0.92), rgba(14, 15, 26, 0.92));
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);
  animation: glass-modal-rise 0.25s ease;
}

.glass-modal__title {
  margin: 0;
  font-size: 1.25rem;
  color: #f4f5fb;
}

.glass-modal__body {
  margin: 10px 0 0;
  font-size: 0.9rem;
  line-height: 1.55;
  color: rgba(244, 245, 251, 0.6);
}

.glass-modal__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 22px;
}

.glass-modal__button {
  padding: 10px 18px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.06);
  color: #f4f5fb;
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s ease;
}

.glass-modal__button:hover {
  background: rgba(255, 255, 255, 0.12);
}

.glass-modal__button--primary {
  border: 0;
  background: #f4f5fb;
  color: #101322;
}

.glass-modal__button--primary:hover {
  background: #ffffff;
}

@keyframes glass-modal-fade {
  from {
    opacity: 0;
  }
}

@keyframes glass-modal-rise {
  from {
    opacity: 0;
    transform: translateY(14px) scale(0.97);
  }
}

@media (prefers-reduced-motion: reduce) {
  .glass-modal-overlay,
  .glass-modal {
    animation: none;
  }
}

</style>

<script>
const overlay = document.querySelector("[data-modal]");
const openButton = document.querySelector("[data-modal-open]");
const dialog = overlay.querySelector(".glass-modal");

const FOCUSABLE =
  'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';

function getFocusable() {
  return Array.from(dialog.querySelectorAll(FOCUSABLE)).filter(
    (el) => el.offsetParent !== null,
  );
}

function openModal() {
  overlay.hidden = false;
  // aria-modal="true" promises the rest of the page is inert — make it so.
  openButton.setAttribute("inert", "");
  const focusable = getFocusable();
  (focusable[0] || dialog).focus();
}

function closeModal() {
  overlay.hidden = true;
  openButton.removeAttribute("inert");
  openButton.focus();
}

// Keep Tab / Shift+Tab cycling within the dialog instead of escaping to the
// (now inert) background.
function trapTab(event) {
  const focusable = getFocusable();
  if (focusable.length === 0) {
    event.preventDefault();
    return;
  }

  const first = focusable[0];
  const last = focusable[focusable.length - 1];
  const active = document.activeElement;

  if (!dialog.contains(active)) {
    event.preventDefault();
    first.focus();
    return;
  }

  if (event.shiftKey && active === first) {
    event.preventDefault();
    last.focus();
  } else if (!event.shiftKey && active === last) {
    event.preventDefault();
    first.focus();
  }
}

openButton.addEventListener("click", openModal);

overlay.addEventListener("click", (event) => {
  if (event.target === overlay || event.target.closest("[data-modal-close]")) {
    closeModal();
  }
});

document.addEventListener("keydown", (event) => {
  if (overlay.hidden) return;
  if (event.key === "Escape") {
    closeModal();
  } else if (event.key === "Tab") {
    trapTab(event);
  }
});

</script>