Multi-Use Modal Popup

Accessible and usable for just about any type of content you want to pop up gracefully on your posts in Ghost...

Launch it from a Link...

Wine bar location (interactive map)

From a Button...

From an Image Link...

Tesco finest* wine bar location

Launch From Any HTML Element...

This is a div :-)

For Opening an Uploaded PDF...

List of Nursing Homes for Mom (PDF)

For Popping Up a Web Page...

Denver Geeks Website (Web Page)

Popping Up a Youtube Video or Other Embeddable Media...

Pop-up YouTube video...

TO USE:

  • Add attribute data-modal to any link, or button, or tag, or div, etc. to indicate "Launch in modal".
  • Give data-modal the url of the iframe src if it's different than the link href.
  • The modal title may be taken from the optional data attribute data-title="", or the link text, or the image alt text.
  • The modal description may also be overriden by the data attribute data-desc.

Here is an example of a simple link to a website:

<a class="lnk_modal-open" href="https://denverit.com/" data-modal>Denver Geeks Website (Web Page)</a>

The snippet above creates this...

Denver Geeks Website (Web Page)

To enjoy, put this CSS code into your Header Injection:

<style>
    
/* Modal opening object (link, button, or pretend button) */

button,
[aria-role="button"] {
  cursor: pointer;
}
button[data-modal][aria-controls],
[aria-role="button"][data-modal][aria-controls] {
  transition: all .3s ease-out;
}

/* Hover & focus indication. */

/* Reads as: if button, or aria-role=button, has data-model attribute and JavaScript has added aria-controls then on hover or focus */ 

button[data-modal][aria-controls]:hover,
button[data-modal][aria-controls]:focus,
[aria-role="button"][data-modal][aria-controls]:hover,
[aria-role="button"][data-modal][aria-controls]:focus {
  -webkit-filter: contrast(120%);
  filter: contrast(120%);
  box-shadow: 0 0 0 4px rgba(255, 0, 0, .6); /* Red so you can tell */
  outline: 0 solid;
}


/* Modal opening link cosmetics */

.lnk_modal-open {
  font-size: larger;
  background-color: #fff;
  padding:.25rem .5rem;
  display: inline-block;
  text-decoration: none;
  border: 0 solid;
  margin: 0 auto;
}

.lnk_modal-img {
  padding: 0;
}

.lnk_modal-open:active {
  -webkit-filter: brightness(85%);
  filter: brightness(85%);
}

.img_modal-open {
  display: block;
  border: 0 solid;
}


/* The modal section is added via JS */

.modal {
  max-width: 80vw;
  max-height: 90vh;
  background-color: #F7F0E8;
  margin: 0 auto;
  position: absolute;
  left: 10%;
  right: 10%;
  top: 5%;
  bottom: 5%;
  z-index: 99999;
  border: 1px solid #000;
  box-shadow: 0 .25em .5em #000;
  transition: opacity .5s ease-out, visibility 0s ease-out 1s, transform .5s ease-out .5s;
  backface-visibility: hidden;
  opacity: 0;
  visibility: hidden;
  transform: scale(.8) translate3d(0,0,0);
  pointer-events: none;
}

.modal[aria-hidden="false"] {
  position: fixed;
  transition-delay: 0s,0s, 0s;
  opacity: 1;
  visibility: visible;
  transform: scale(1) translate3d(0,0,0);
  pointer-events: auto;
}


/* Light box properties */

.modal_lightbox {
  text-indent: -200em;
  background-color: rgba(0, 0, 0, 0.8);
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  /* places the modal overlay between the main page (0) and the modal dialog (10) */
  z-index: 5;
  cursor: pointer;
  transition: opacity .5s ease-out, visibility 0s ease-out .5s;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.modal_lightbox-on {
  transition-delay: 0s, 0s;
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}
.modal_lightbox-on:hover {
  /* Stolen from trickle.js. Under consideration. SVGs will not work here */
  cursor: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAANpJREFUOBGNkz0KAjEQhYPYWXgCKws7LcXWyt7WG4i9jegBvIJ4AvEyXsRCsBDiN0sGkiE/O/A2O9n3viS7rPPeb9ETrVzPwrtHDzR1XF5I6oPWLQaeq5hDXQRw0I6xCuF5HH7Tz7oFuTkhrSyEhza8THaLoQhphpWUg/QOFyA/AFpy5nTbGrIjxvg4AiiGBzYc+rGZH9KPzFy+ZbX4bX9l+VDZr5NQMMbhbtvMxccpQ3JhpTchtXATQviItIpvW0CY7HHm8c9UDRd2chbABt3RQk2tEe8O3dDkD4JQ4iOR7BMpAAAAAElFTkSuQmCC"), pointer;
}


/* Modal title and description */

.modal_title,
.modal_desc {
  position: absolute;
  top: 5px;
  left: -200em;
  background-color: #fff;
  color: #000;
  text-shadow: 0 0 0 #fff;
  font-size: 20px;
  padding: 0.125em .25em;
  /* Tesco requirement
  font-family: Tesco_W_Rg, sans-serif; */
  margin: 0;
}

.modal_title:focus,
.modal_desc:focus {
  left: 5px;
}

[aria-hidden="false"] .modal_title {
  transition: opacity .5s ease-out 3s;
  opacity: 0;
}

.modal_title,
.modal_title:focus {
  opacity: 1;
  transition: opacity .5s ease-out;
}


/* The iframe */

.modal_iframe {
  transition: opacity .5s ease-out, visibility 0s ease-out 1s;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.modal_iframe-on {
  width: 100%;
  transition: opacity .8s ease-out, visibility 0s ease-out 0s;
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

[aria-hidden="true"] .modal_iframe {
  display: none;
}

[aria-hidden="false"] .modal_iframe {
  display: block;
}


/* The modal pop-ups close button, appears last in the modal, but is moved visually to the top right of the pop-up */

.modal_lnk-close {
  cursor: pointer;
  position: absolute;
  top: -20px;
  right: -20px;
  border: 0 solid;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  background-color: #f00;
  box-shadow: 0 .25em .5em rgba(0, 0, 0, .25);
  overflow: hidden;
  transition: background-color .3s ease-out;
}

.modal_lnk-close:hover,
.modal_lnk-close:active,
.modal_lnk-close:focus {
  background-color: #c00;
  outline: 0 solid;
}

.svg-close {
  pointer-events: none;
  width: 40px;
  height: 40px;
  stroke: #fff;
  stroke-width: 2;
}


/* Modal SVG (Tesco) loading animation version 2 (overlaid on itself and out of phase) */

[class*="svg-loading"] {
  position: absolute;
  width: 80px;
  height: 80px;
  top: calc(50% - 40px);
  left: calc(50% - 40px);
  z-index: -1;
  transition: opacity .3s ease-out;
  backface-visibility: hidden;
}

.svg-loading {
  fill: #00539f;
  -webkit-animation: rotate 4s linear 0s infinite;
  animation: rotate 4s linear 0s infinite;
}

.svg-loading2 {
  /* Match to .modal background colour for full effect */
  fill: #F7F0E8;
  -webkit-animation: rotate 5s linear 1s infinite;
  animation: rotate 5s linear 1s infinite;
}

@-webkit-keyframes rotate {
  to {
    -webkit-transform: rotate(360deg) translate3d(0,0,0);
    transform: rotate(360deg) translate3d(0,0,0);
  }
}

@keyframes rotate {
  to {
    -webkit-transform: rotate(360deg) translate3d(0,0,0);
    transform: rotate(360deg) translate3d(0,0,0);
  }
}


/* While modal is open */

/* Class added to body tag to prevent scroll
   Note the body does not require class "-modal" */
body.-modal-open {
  overflow: hidden;
}


/* Any tags classed with "-modal", when open, get "-modal-open" added */
/* Both of these are equivalent, best practice to use the attribute version which enforces accessibility */

.-modal[aria-hidden="true"] {
  -webkit-filter: blur(4px);
  filter: blur(4px);
}
.-modal.-modal-open {
  -webkit-filter: blur(4px);
  filter: blur(4px);
}


/* Generic helper style */
.u-margin2 {
  margin: 2rem 0;
}

</style>

. . . and put this Javascript into your Footer Injection:

<script>
// https://john-dugan.com/javascript-debounce/
var debounce=function(e,t,n){var a;return function(){var r=this,i=arguments,o=function(){a=null,n||e.apply(r,i)},s=n&&!a;clearTimeout(a),a=setTimeout(o,t||200),s&&e.apply(r,i)}};


(function (window, d, debounce) {

  "use strict";

  // Modal pop-up window iframe version 4.1 22-08-2016

  // v4.1 - Launch from an anchor link, button or any other not recommended object
  // modal title text set by data-modalTitle, image alt or link text.
  // Object with .-modal (modalName) with have .-modal-open appended === "-" + modalName + contentClass

  // To do - under consideration:
  // make lightbox the modal section and code around it?
  // perhaps <section class=modal = lightbox
  //            <div =modal_inner = modal?
  // lightbox doesnt need to be in keychain just add onclick to close

  // To do:
  //    Option to set an ideal pop-up size eg an image
  //      - maintain the aspect ratio
  //      - centre it

  // Requires:
  //    SVG definitions for: #icon-cross, #icon-loading
  //    External functions: debounce()

  // Assumptions:
  //    First object in modal is the modal title
  //    Last object is the modal close link

  
  // defaults
  var modalName = "modal";
  var lightboxClass = "lightbox";
  var openClass = "-" + modalName + "-open";
  //var modalDesc = "<kbd>tab</kbd> or <kbd>shift + tab</kbd> to move focus.";
  var modalDesc = "Tab or Shift + Tab to move focus.";


  var _setContentObjs = function (isModalOpen) {
    var objs = d.getElementsByClassName("-" +modalName);
    var i = objs.length;
    while (i--) {
      if (!!isModalOpen) {
        objs[i].classList.add(openClass);
        if (objs[i].tagName.toLowerCase !== "body") {
          objs[i].setAttribute("aria-hidden", "true");
        }
      } else {
        objs[i].classList.remove(openClass);
        objs[i].removeAttribute("aria-hidden");
      }
    }
    return !!isModalOpen;
  };


  var _closeModal = function (e) {
    var count = e.target.count; // = lightbox, modal (ESC key), close btn
    var modalSection = d.getElementById(modalName + "_" + count);
    var lightbox = d.getElementById(modalName + "_" + count + "_" + lightboxClass);
    var modalLink;
    if (modalSection) {
      modalSection.setAttribute("aria-hidden", "true");
      lightbox.className = lightbox.className.replace(lightboxClass + "-on", "");

      _setContentObjs(!modalSection.getAttribute("aria-hidden"));
      modalLink = d.getElementById(modalSection.returnId);
      d.body.classList.remove(openClass);
      modalLink.focus();
    }
  };


  var _getModalSize = function (modalSection) {
    var clone = modalSection.cloneNode(true);
    var size = {};
    clone.className = modalName;
    clone.setAttribute("style", "position:fixed;visibility:hidden;transform: none");
    modalSection.parentElement.appendChild(clone);
    size.width = clone.clientWidth; // more performant than getBoundingClientRect
    size.height = clone.clientHeight; // more performant than getBoundingClientRect
    modalSection.parentElement.removeChild(clone);
    return size;
  };


  var _resizeIframes = function () {

    var size;
    var iframes;
    var ii;

    var modals = d.getElementsByClassName(modalName);
    var i = modals.length;

    while (i--) {

      size = _getModalSize(modals[i]);

      iframes = modals[i].getElementsByClassName(modalName + "_iframe");
      ii = iframes.length;

      while (ii--) {
        iframes[ii].width = size.width;
        iframes[ii].height = size.height;
      }
    }
  };


  var _addIframe = function (modalSection) {

    var size;
    var close_lnk;
    var frames = modalSection.getElementsByClassName(modalName + "_iframe");
    var iframe;
    if (!frames[0]) {

      iframe = d.createElement("iframe");

      // Don't display iframe until it's content is ready
      iframe.addEventListener("load", function () {
        iframe.classList.add(modalName + "_iframe-on");
      }, false);

      iframe.src = modalSection.modalSrc;
      iframe.className = modalName + "_iframe";

      size = _getModalSize(modalSection);
      iframe.width = size.width;
      iframe.height = size.height;

      iframe.setAttribute("frameborder", 0);
      iframe.setAttribute("allowfullscreen", true);

      // Add iframe before the close button
      close_lnk = d.getElementById(modalName + "_" + modalSection.count + "_lnk_close");
      modalSection.insertBefore(iframe, close_lnk);

    }
  };
  
  
  var _getTarget = function (obj) {
    var target = obj;
    var isBodyTag = obj.tagName.toLowerCase() === "body";
    if (isBodyTag) {
      return false;
    }
    if (!obj.modalSrc) {
      target = _getTarget(obj.parentElement);
    }
    return target;
  }


  var _openModal = function (e) {

    e.preventDefault();
    
    var target = _getTarget(e.target);
    
    if (target) {

      var count = target.count;
      var tempId = modalName + "_" + count;
      var tempLightboxClass = modalName + "_" + lightboxClass;
      var modalSection = d.getElementById(tempId);
      var lightbox = d.getElementById(tempId + "_" + lightboxClass);

      if (modalSection && lightbox) {
        if (!lightbox.className.match(tempLightboxClass + "-on")) {
          lightbox.className += " " + tempLightboxClass + "-on";
        }
        modalSection.setAttribute("aria-hidden", "false");
        _addIframe(modalSection);

        _setContentObjs(!!modalSection.getAttribute("aria-hidden"));

        d.body.classList.add(openClass);
        d.getElementById(modalName + "_" + count + "_title").focus();
      }
    }
  };


  var _keydown_openerObj = function (e) {
    // enter or space from the opener object
    if (e.which === 13 || e.which === 32) {
        e.preventDefault(); 
        _openModal(e);
    }
  };


  var _addOpenModalLinkAttr = function (modalLink) {
    modalLink.id = modalLink.id || "modal_" + modalLink.count + "_lnk";
    modalLink.setAttribute("aria-controls", modalName + "_" + modalLink.count);
    
    // test if it's not a button
    var tag = modalLink.tagName.toLowerCase();
    if (tag !== "button") {
      modalLink.setAttribute("aria-role", "button");
      modalLink.addEventListener("keydown", _keydown_openerObj, false);
    }
    
    // click only requires space and enter activtion too
    if (tag !== "a" || "button") {
      modalLink.tabIndex = 0;
    }
    
    modalLink.addEventListener("click", _openModal, false);
  };


  var _keydown_modal = function (e) {

    var target = e.target;

    // ESC key on anything actionable
    if (e.which === 27) {
      _closeModal(e);
    }

    // tab key and shift on the h1
    if (e.which === 9 && e.shiftKey) {
      if (target.classList.contains(modalName + "_title")) {
        e.preventDefault();
        //focus on last object in modal (close btn)
        d.getElementById(modalName + "_" + e.target.count + "_lnk_close").focus();
      }
    }

    // tab key and not shift on the close link.
    if (e.which === 9 && !e.shiftKey) {
      if (target.classList.contains(modalName + "_lnk-close")) {
        e.preventDefault(); 
        //focus on first object in modal - or should it be the modal? Requires testing
        d.getElementById(modalName + "_" + e.target.count + "_title").focus();
      }
    }

    // enter or space on the close link - why again??
    if (e.which === 13 || e.which === 32) {
      if (target.classList.contains(modalName + "_lnk-close")) {
        e.preventDefault(); 
        _closeModal(e);
      }
    }
  };


  var _getTitleText = function (modalLink) {
    var alt = "";
    var imgs = modalLink.getElementsByTagName("img");
    if (imgs && imgs[0]) {
      alt = imgs[0].alt;
    }
    return modalLink.getAttribute("data-modalTitle") || alt || modalLink.textContent; 
  };


  var _getModalTitle = function (modalLink) {
    var title = d.createElement("h1");
    title.id = modalName + "_" + modalLink.count + "_title";
    title.className = modalName + "_title";
    title.tabIndex = 0;
    title.textContent = _getTitleText(modalLink);
    title.count = modalLink.count;
    title.addEventListener("keydown", _keydown_modal, false);
    return title;
  };


  var _getModalSVG = function (icon, clss, title) {
    var svg = d.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.classList.add(clss);
    if (title) {
      var t = d.createElementNS("http://www.w3.org/2000/svg", "title");
      t.textContent = title;
      svg.appendChild(t);
    }
    var use = d.createElementNS("http://www.w3.org/2000/svg", "use");
    use.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#" + icon);
    svg.appendChild(use);
    return svg;
  };


  var _getModalDesc = function (modalLink) {
    var desc = d.createElement("p");
    desc.id = modalName + "_" + modalLink.count + "_desc";
    desc.className = modalName + "_desc";
    desc.tabIndex = 0;
    desc.innerHTML = modalLink.getAttribute("data-modalDesc") || modalDesc;
    desc.count = modalLink.count;
    desc.addEventListener("keydown", _keydown_modal, false);
    return desc;
  };


  var _getModalCloseLink = function (modalLink) {
    var link = d.createElement("a");
    link.id = modalName + "_" + modalLink.count + "_lnk_close";
    link.className = modalName + "_lnk-close";
    link.tabIndex = 0;
    link.appendChild(_getModalSVG("icon-cross", "svg-close", "Close modal"));
    link.count = modalLink.count;
    link.addEventListener("click", _closeModal, false);
    link.addEventListener("keydown", _keydown_modal, false);
    return link;
  };


  var _addModalSection = function(modalLink) {
    var section = d.createElement("section");
    section.id = modalName + "_" + modalLink.count;
    section.count = modalLink.count;
    section.returnId = modalLink.id;
    section.className = modalName;
    section.setAttribute("aria-hidden", "true");

    // should be on the activating link?
    section.setAttribute("aria-labelledby", modalName +"_" + modalLink.count + "_title");
    section.setAttribute("aria-describedby", modalName +"_" + modalLink.count + "_desc");

    section.setAttribute("role", "dialog");
    section.modalSrc = modalLink.modalSrc;

    section.appendChild(_getModalTitle(modalLink));
    section.appendChild(_getModalSVG("icon-loading", "svg-loading", "Loading"));
    section.appendChild(_getModalSVG("icon-loading", "svg-loading2", ""));
    section.appendChild(_getModalDesc(modalLink));
    section.appendChild(_getModalCloseLink(modalLink));

    d.body.appendChild(section);
  };


  var _addLightbox = function (modalLink) {

    var count = modalLink.count;
    var lightboxDiv = d.createElement("div");

    lightboxDiv.id = modalName + "_" + count + "_" + lightboxClass;
    lightboxDiv.className = modalName + "_" + lightboxClass;
    lightboxDiv.count = count;
    lightboxDiv.returnId = modalLink.id;

    // mouse / touch only
    lightboxDiv.addEventListener("click", _closeModal, false);

    d.body.appendChild(lightboxDiv);
  };


  var configuration = function (cfg) {
    modalName = cfg.modalName || modalName;
    
    lightboxClass = cfg.lightboxClass || lightboxClass;
    
    // any object with a class -modal will have the class -modal-open added when the modal is open.
    //openClass = "-" + modalName + (cfg.openClass || "-open");
    openClass = cfg.openClass ? "-" + modalName + cfg.openClass : openClass;
  };


  var initialise = function (cfg) {

    configuration(cfg);

    var modalSrc;
    var dataModals = d.querySelectorAll("[data-" + modalName + "]");

    if (dataModals) {
      var i = dataModals.length;
      while (i--) {

        // Link href and iframe src are not always the same!
        modalSrc = false;

        // use the href
        if (dataModals[i].hasAttribute("href")) {
          modalSrc = dataModals[i].href;
        }

        // overwrite src with data-modal content when available
        if (dataModals[i].getAttribute("data-modal").length) {
          modalSrc = dataModals[i].getAttribute("data-modal");
        }

        if (modalSrc) {
          dataModals[i].modalSrc = modalSrc;
          dataModals[i].count = i;
          _addOpenModalLinkAttr(dataModals[i]);
          _addModalSection(dataModals[i]);
          _addLightbox(dataModals[i]);
        }

      }

      window.addEventListener("resize", debounce(_resizeIframes, 250, false));

    }

  };

  initialise({
    modalName : "modal",  // class name of modal, also used as the base for all classes used except on SVGs.
    openClass : "-open", // is default ("-" + modaName automatically prepended)
    lightboxClass : "lightbox" // is default (modaName + "_" automatically prepended)
  });

}(window, document, debounce));
</script>

From Accessible modal dialog pop-up iframe (v4)

Uses an anchor to launch a modal pop-up which is then populated with an iframe. This version removed the requirement for an image, also allows modal title and description to be user defined.

Production ready (encapuslated with external configuration and instantiation) version available: Modal Dialog demo. For minified JavaScript see source.

Alternatives...

Tingle - Tingle is a simple modal plugin written in pure JavaScript (Source code on GitHub).

Tingle.js, 2kB vanilla modal plugin
Tingle.js is a minimalist and easy-to-use modal plugin written in pure JavaScript created with UX in mind (2kB, no dependencies, easily customizable with CSS)
Loading