Building Your Own JavaScript Modal Plugin

Introduction

When starting a new project, there are two staple Javascript UI components that you will likely require. The first being a carousel, which I’ve already

taken care of, and the second being a modal. Today we are going to build out a flexible CSS3 modal plugin. Here’s a demo to see what we’ll be building:

See the Pen Fade and Drop Demo by Ken Wheeler (@kenwheeler) on CodePen.

Getting Started

The difference between building a plugin and a project component lies in

flexibility. The first thing we are going to is take a step back and think about the requirements. Our modal plugin should:

  • Launch different modals based upon option sets
  • Allow users to define custom transitions
  • Be responsive
  • Have max/min width points
  • Anchor to the top of the page if too tall
  • Be centered otherwise
  • Accept a HTML string for content OR a domNode
  • Have no dependencies

See that last line? Thats right folks, we’re doing this in plain old Javascript.

The Javascript

Architecture

Alright, lets get our hands dirty. Our first order of business is going to be deciding on our plugin architecture and picking a design pattern. Let’s create an

IIFE to create a closure we can work within. Closures can be leveraged to create a private scope, where you have control over what data you make available.


// Create an immediately invoked functional expression to wrap our code
(function() {
    var privateVar = "You can't access me in the console"
}());

We want to add a constructor method for our plugin, and expose it as public. Our IIFE is called globally, so our

this keyword is pointing at the window. Let’s attach our constructor to the global scope using this.


// Create an immediately invoked functional expression to wrap our code
(function() {
  // Define our constructor
  this.Modal = function() {

  }
}());

Pointing our

Modal variable at a function creates a functional object, which can now be instantiated with the new keyword like so:


var myModal = new Modal();

This creates a new instance of our object. Unfortunately, our object doesn’t do much at this point, so lets do something about that.

Options

Taking a look back at our requirements, our first order of business is to allow user defined options. The way we are going to achieve this is to create a set of default options, and then merge it with the object the user provides.


// Create an immediately invoked functional expression to wrap our code
(function() {

  // Define our constructor
  this.Modal = function() {

    // Create global element references
    this.closeButton = null;
    this.modal = null;
    this.overlay = null;

    // Define option defaults
    var defaults = {
      className: 'fade-and-drop',
      closeButton: true,
      content: "",
      maxWidth: 600,
      minWidth: 280,
      overlay: true
    }

    // Create options by extending defaults with the passed in arugments
    if (arguments[0] && typeof arguments[0] === "object") {
      this.options = extendDefaults(defaults, arguments[0]);
    }

  }

  // Utility method to extend defaults with user options
  function extendDefaults(source, properties) {
    var property;
    for (property in properties) {
      if (properties.hasOwnProperty(property)) {
        source[property] = properties[property];
      }
    }
    return source;
  }

}());

Pause. What’s going on here? First we create global element references. These are important so that we can reference pieces of the Modal from anywhere in our plugin. Next up, we add a default options object. If a user doesn’t provide options, we use these. If they do, we override them. So how do we know if they have provided options? The key here is in the

arguments object. This is a magical object inside of every function that contains an array of everything passed to it via arguments. Because we are only expecting one argument, an object containing plugin settings, we check to make sure arguments[0] exists, and that it is indeed an object. If that condition passes, we then merge the two objects using a privately scoped utility method called extendDefaults. extendDefaults takes an object, loops through its properties, and if it isn’t an internal property(hasOwnProperty), assigns it to the source object. We can now configure our plugin with an options object:


var myModal = new Modal({
    content: 'Howdy',
    maxWidth: 600
});

Public Methods

Now that we have our

Modal object, and its configurable, how about adding a public method? The first thing a developer is going to want to do with a modal is open it up, so let’s make it happen.


// Create an immediately invoked functional expression to wrap our code
(function() {

  // Define our constructor
  this.Modal = function() {
    ...
  }

  // Public Methods

  Modal.prototype.open = function() {
    // open code goes here
  }

  // Private Methods

  // Utility method to extend defaults with user options
  function extendDefaults(source, properties) {
    ...
  }

}());

In order to expose a public method, we attach it to our

Modal object’s prototype. When you add methods to the object’s prototype, each new instance shares the same methods, rather than creating new methods for each instance. This is super performant, unless you have multi level subclassing, in which traversing the prototype chain negates your performance boost. We have also added comments and structured our component so that we have three sections: constructor, public methods & private methods. This doesn’t do anything functionally, but it keeps everything organized and readable.

Core Functionality

How about we take a step back? We now have a nice plugin architecture, with a constructor, options & a public method. It is time for the bread and butter, so let’s revisit what this plugin is supposed to do. Our plugin should:

  • Build a modal element and add it to the page
  • Add any classes specified in the `className` option to the modal
  • If the `closeButton` option is true, add a close button
  • If the `content` option is a HTML string, set it as the modal’s content
  • If the `content` option is a domNode, set it’s interior content as the modal’s content
  • Set the modal’s `maxWidth` and `minWidth` respectively
  • Add an overlay if the `overlay` option is true
  • When opened, add a `scotch-open` class that we can use with our CSS to define an open state.
  • When closed, remove the `scotch-open` class.
  • If the modal’s height exceeds the viewport’s height, also add a `scotch-anchored` class so that we can handle that scenario

Building Our Modal

We can’t have a modal plugin without building a modal, so let’s create a private method that constructs a modal using our defined options:


function buildOut() {

    var content, contentHolder, docFrag;

    /*
     * If content is an HTML string, append the HTML string.
     * If content is a domNode, append its content.
     */

    if (typeof this.options.content === "string") {
      content = this.options.content;
    } else {
      content = this.options.content.innerHTML;
    }

    // Create a DocumentFragment to build with
    docFrag = document.createDocumentFragment();

    // Create modal element
    this.modal = document.createElement("div");
    this.modal.className = "scotch-modal " + this.options.className;
    this.modal.style.minWidth = this.options.minWidth + "px";
    this.modal.style.maxWidth = this.options.maxWidth + "px";

    // If closeButton option is true, add a close button
    if (this.options.closeButton === true) {
      this.closeButton = document.createElement("button");
      this.closeButton.className = "scotch-close close-button";
      this.closeButton.innerHTML = "×";
      this.modal.appendChild(this.closeButton);
    }

    // If overlay is true, add one
    if (this.options.overlay === true) {
      this.overlay = document.createElement("div");
      this.overlay.className = "scotch-overlay " + this.options.classname;
      docFrag.appendChild(this.overlay);
    }

    // Create content area and append to modal
    contentHolder = document.createElement("div");
    contentHolder.className = "scotch-content";
    contentHolder.innerHTML = content;
    this.modal.appendChild(contentHolder);

    // Append modal to DocumentFragment
    docFrag.appendChild(this.modal);

    // Append DocumentFragment to body
    document.body.appendChild(docFrag);

  }

We start by getting our target content and creating a Document Fragment. A Document Fragment is used to construct collections of DOM elements outside of the DOM, and is used to cumulatively add what we have built to the DOM. If our

content is a string, we set our content variable to the option value. If our content is a domNode, we set our content variable to it’s interior HTML via innerHTML. Next up, we create our actual modal element, and add our className and minWidth/maxWidth properties to it. We create it with a default scotch-modal class for initial styling. Then, based upon option values, conditionally create a close button and an overlay in the same fashion. Finally, we add our content to a content holder div, and append it to our modal element. After appending our modal to the Document Fragment and appending our Document Fragment to the body, we now have a built modal on the page!

Events

This modal (hopefully) isn’t going to close itself, so providing we have a close button and/or an overlay, we need to bind events to them to make the magic happen. Below, we create a method to attach these events:


function initializeEvents() {

    if (this.closeButton) {
      this.closeButton.addEventListener('click', this.close.bind(this));
    }

    if (this.overlay) {
      this.overlay.addEventListener('click', this.close.bind(this));
    }

}

We attach our events using the

addEventListener method, passing a callback to a method we haven’t created yet called close. Notice we dont just call close, but we use the bind method and pass our reference to this, which references our Modal object. This makes sure that our method has the right context when using the this keyword.

Opening the Modal

Let’s head back to the public `open` method we created earlier. Time to make it shine:


Modal.prototype.open = function() {
    // Build out our Modal
    buildOut.call(this);

    // Initialize our event listeners
    initializeEvents.call(this);

    /*
     * After adding elements to the DOM, use getComputedStyle
     * to force the browser to recalc and recognize the elements
     * that we just added. This is so that CSS animation has a start point
     */
    window.getComputedStyle(this.modal).height;

    /*
     * Add our open class and check if the modal is taller than the window
     * If so, our anchored class is also applied
     */
    this.modal.className = this.modal.className +
      (this.modal.offsetHeight > window.innerHeight ?
        " scotch-open scotch-anchored" : " scotch-open");
    this.overlay.className = this.overlay.className + " scotch-open";

}

When opening our modal, we first have to build it. We call our

buildOut method using the call method, similarly to the way we did in our event binding with bind. We are simply passing the proper value of this to the method. We then call initializeEvents to make sure any applicable events get bound. Now, I know you are saying to yourself, what is going on with getComputedStyle? Check this out: We are using CSS3 for our transitions. The modal hides and shows based upon applied class names. When you add an element to the dom, and then add a class, the browser might not have interpereted the initial style, so you never see a transition from its initial state. Thats where window.getComputedStyle comes into play. Calling this forces the browser to recognize our initial state, and keeps our modal transition looking mint. Lastly, we add the scotch-open class name. But that’s not all. We currently have our modal centered, but if its height exceeds the viewport, that’s gonna look silly. We use a ternary operator to check the heights, and if our modal is too tall, we also add the scotch-anchored class name, to handle this situation.

Closing the Modal

Like anything else that is completely amazing in this world, at some point our modal must come to an end. So let’s build a method to send it to the other side:


Modal.prototype.close = function() {
    // Store the value of this
    var _ = this;

    // Remove the open class name
    this.modal.className = this.modal.className.replace(" scotch-open", "");
    this.overlay.className = this.overlay.className.replace(" scotch-open",
      "");

    /*
     * Listen for CSS transitionend event and then
     * Remove the nodes from the DOM
     */
    this.modal.addEventListener(this.transitionEnd, function() {
      _.modal.parentNode.removeChild(_.modal);
    });
    this.overlay.addEventListener(this.transitionEnd, function() {
      if(_.overlay.parentNode) _.overlay.parentNode.removeChild(_.overlay);
    });

}

In order to have our modal transition out, we remove its

scotch-open class name. The same applies to our overlay. But we aren’t finished yet. We have to remove our modal from the DOM, but its going to look ridiculous if we don’t wait until our animation has completed. We accomplish this by attaching an event listener to detect when our transition is complete, and when it is, its “Peace out, cub scout”. You may be wondering where this.transitionEnd came from. I’ll tell you. Browsers have different event names for transitions ending, so I wrote a method to detect which one to use, and called it in the constructor. See below:


// Utility method to determine which transistionend event is supported
function transitionSelect() {
    var el = document.createElement("div");
    if (el.style.WebkitTransition) return "webkitTransitionEnd";
    if (el.style.OTransition) return "oTransitionEnd";
    return 'transitionend';
}

this.Modal = function() {
    ...
    // Determine proper prefix
    this.transitionEnd = transitionSelect();
    ....
}

Wrap Up

And there you have it. We have built out our modal javascript plugin. Comments and spacing aside,

we did it in 100 lines of pure, sweet Vanilla Javascript. Check out our finished product below, and then get ready to talk CSS:


// Create an immediately invoked functional expression to wrap our code
(function() {

  // Define our constructor
  this.Modal = function() {

    // Create global element references
    this.closeButton = null;
    this.modal = null;
    this.overlay = null;

    // Determine proper prefix
    this.transitionEnd = transitionSelect();

    // Define option defaults
    var defaults = {
      className: 'fade-and-drop',
      closeButton: true,
      content: "",
      maxWidth: 600,
      minWidth: 280,
      overlay: true
    }

    // Create options by extending defaults with the passed in arugments
    if (arguments[0] && typeof arguments[0] === "object") {
      this.options = extendDefaults(defaults, arguments[0]);
    }

  }

  // Public Methods

  Modal.prototype.close = function() {
    var _ = this;
    this.modal.className = this.modal.className.replace(" scotch-open", "");
    this.overlay.className = this.overlay.className.replace(" scotch-open",
      "");
    this.modal.addEventListener(this.transitionEnd, function() {
      _.modal.parentNode.removeChild(_.modal);
    });
    this.overlay.addEventListener(this.transitionEnd, function() {
      if(_.overlay.parentNode) _.overlay.parentNode.removeChild(_.overlay);
    });
  }

  Modal.prototype.open = function() {
    buildOut.call(this);
    initializeEvents.call(this);
    window.getComputedStyle(this.modal).height;
    this.modal.className = this.modal.className +
      (this.modal.offsetHeight > window.innerHeight ?
        " scotch-open scotch-anchored" : " scotch-open");
    this.overlay.className = this.overlay.className + " scotch-open";
  }

  // Private Methods

  function buildOut() {

    var content, contentHolder, docFrag;

    /*
     * If content is an HTML string, append the HTML string.
     * If content is a domNode, append its content.
     */

    if (typeof this.options.content === "string") {
      content = this.options.content;
    } else {
      content = this.options.content.innerHTML;
    }

    // Create a DocumentFragment to build with
    docFrag = document.createDocumentFragment();

    // Create modal element
    this.modal = document.createElement("div");
    this.modal.className = "scotch-modal " + this.options.className;
    this.modal.style.minWidth = this.options.minWidth + "px";
    this.modal.style.maxWidth = this.options.maxWidth + "px";

    // If closeButton option is true, add a close button
    if (this.options.closeButton === true) {
      this.closeButton = document.createElement("button");
      this.closeButton.className = "scotch-close close-button";
      this.closeButton.innerHTML = "×";
      this.modal.appendChild(this.closeButton);
    }

    // If overlay is true, add one
    if (this.options.overlay === true) {
      this.overlay = document.createElement("div");
      this.overlay.className = "scotch-overlay " + this.options.className;
      docFrag.appendChild(this.overlay);
    }

    // Create content area and append to modal
    contentHolder = document.createElement("div");
    contentHolder.className = "scotch-content";
    contentHolder.innerHTML = content;
    this.modal.appendChild(contentHolder);

    // Append modal to DocumentFragment
    docFrag.appendChild(this.modal);

    // Append DocumentFragment to body
    document.body.appendChild(docFrag);

  }

  function extendDefaults(source, properties) {
    var property;
    for (property in properties) {
      if (properties.hasOwnProperty(property)) {
        source[property] = properties[property];
      }
    }
    return source;
  }

  function initializeEvents() {

    if (this.closeButton) {
      this.closeButton.addEventListener('click', this.close.bind(this));
    }

    if (this.overlay) {
      this.overlay.addEventListener('click', this.close.bind(this));
    }

  }

  function transitionSelect() {
    var el = document.createElement("div");
    if (el.style.WebkitTransition) return "webkitTransitionEnd";
    if (el.style.OTransition) return "oTransitionEnd";
    return 'transitionend';
  }

}());

The CSS

This

is a CSS3 modal, so Javascript is only half the battle. To recap, we have a base class on our modal of scotch-modal, and an open class of scotch-open. Modals that exceed the viewport height have a class of scotch-anchored, and potentially have an overlay(scotch-overlay) and a close button(scotch-close). Let’s apply some base styles:


/* Modal Base CSS */
.scotch-overlay
{
    position: fixed;
    z-index: 9998;
    top: 0;
    left: 0;

    opacity: 0;

    width: 100%;
    height: 100%;

    -webkit-transition: 1ms opacity ease;
       -moz-transition: 1ms opacity ease;
        -ms-transition: 1ms opacity ease;
         -o-transition: 1ms opacity ease;
            transition: 1ms opacity ease;

    background: rgba(0,0,0,.6);
}

.scotch-modal
{
    position: absolute;
    z-index: 9999;
    top: 50%;
    left: 50%;

    opacity: 0;

    width: 94%;
    padding: 24px 20px;

    -webkit-transition: 1ms opacity ease;
       -moz-transition: 1ms opacity ease;
        -ms-transition: 1ms opacity ease;
         -o-transition: 1ms opacity ease;
            transition: 1ms opacity ease;

    -webkit-transform: translate(-50%, -50%);
       -moz-transform: translate(-50%, -50%);
        -ms-transform: translate(-50%, -50%);
         -o-transform: translate(-50%, -50%);
            transform: translate(-50%, -50%);

    border-radius: 2px;
    background: #fff;
}

.scotch-modal.scotch-open.scotch-anchored
{
    top: 20px;

    -webkit-transform: translate(-50%, 0);
       -moz-transform: translate(-50%, 0);
        -ms-transform: translate(-50%, 0);
         -o-transform: translate(-50%, 0);
            transform: translate(-50%, 0);
}

.scotch-modal.scotch-open
{
    opacity: 1;
}

.scotch-overlay.scotch-open
{
    opacity: 1;

}

/* Close Button */
.scotch-close
{
    font-family: Helvetica,Arial,sans-serif;
    font-size: 24px;
    font-weight: 700;
    line-height: 12px;

    position: absolute;
    top: 5px;
    right: 5px;

    padding: 5px 7px 7px;

    cursor: pointer;

    color: #fff;
    border: 0;
    outline: none;
    background: #e74c3c;
}

.scotch-close:hover
{
    background: #c0392b;
}

In a nutshell, we are making our modal and overlay just appear by default. We leave a 1ms transition on by default, so that we can be sure that our

transitionend event actually fires. We use the translate centering method to vertically and horizontally center our modal in the window. If scotch-anchored is applied, we center horizontally, and anchor our modal 20px from the top of the window. This is a great starting base for adding custom animations via the className option, so why don’t we go ahead create a custom animation for the fade-and-drop default className of our plugin:


/* Default Animation */

.scotch-overlay.fade-and-drop
{
    display: block;

    opacity: 0;
}

.scotch-modal.fade-and-drop
{
    top: -300%;

    opacity: 1;

    display: block;
}

.scotch-modal.fade-and-drop.scotch-open
{
    top: 50%;

    -webkit-transition: 500ms top 500ms ease;
       -moz-transition: 500ms top 500ms ease;
        -ms-transition: 500ms top 500ms ease;
         -o-transition: 500ms top 500ms ease;
            transition: 500ms top 500ms ease;
}

.scotch-modal.fade-and-drop.scotch-open.scotch-anchored
{

    -webkit-transition: 500ms top 500ms ease;
       -moz-transition: 500ms top 500ms ease;
        -ms-transition: 500ms top 500ms ease;
         -o-transition: 500ms top 500ms ease;
            transition: 500ms top 500ms ease;
}

.scotch-overlay.fade-and-drop.scotch-open
{
    top: 0;

    -webkit-transition: 500ms opacity ease;
       -moz-transition: 500ms opacity ease;
        -ms-transition: 500ms opacity ease;
         -o-transition: 500ms opacity ease;
            transition: 500ms opacity ease;

    opacity: 1;
}

.scotch-modal.fade-and-drop
{
    -webkit-transition: 500ms top ease;
       -moz-transition: 500ms top ease;
        -ms-transition: 500ms top ease;
         -o-transition: 500ms top ease;
            transition: 500ms top ease;
}

.scotch-overlay.fade-and-drop
{
    -webkit-transition: 500ms opacity 500ms ease;
       -moz-transition: 500ms opacity 500ms ease;
        -ms-transition: 500ms opacity 500ms ease;
         -o-transition: 500ms opacity 500ms ease;
            transition: 500ms opacity 500ms ease;
}

For our

fade-and-drop transition, we want the overlay to fade in, and the modal to drop in. We utilize the delay argument of the transition property shorthand to wait 500ms until the overlay has faded in. For our outro transition, we want the modal to fly back up out of sight and then fade the overlay out. Again, we use the delay property to wait for the modal animation to complete.

Using Our Fancy New Modal

Now we have a fully working modal plugin. Woo! So how do we actually use it? Using the

new keyword, we can create a new modal and assign it to a variable:


var myModal = new Modal();

myModal.open();

Without the

content option set, it is going to be a pretty lame modal, so lets go ahead and pass in some options:


var myModal = new Modal({
    content: '<p>Ken Wheeler is strikingly handsome.</p>',
  maxWidth: 600
});

myModal.open();

What about if we want to set up a custom animation? We should add a class to the className option that we can style with:


var myModal = new Modal({
  className: 'custom-animation',
    content: '<p>Ken Wheeler is strikingly handsome.</p>',
  maxWidth: 600
});

myModal.open();

and then in our CSS, reference it and do your thing:


.scotch-modal.custom-animation {
  -webkit-transition: 1ms -webkit-transform ease;
       -moz-transition: 1ms    -moz-transform ease;
        -ms-transition: 1ms     -ms-transform ease;
         -o-transition: 1ms      -o-transform ease;
            transition: 1ms         transform ease;
  -webkit-transform: scale(0);
       -moz-transform: scale(0);
        -ms-transform: scale(0);
         -o-transform: scale(0);
            transform: scale(0);
}

.scotch-modal-custom-animation.scotch-open {
  -webkit-transform: scale(1);
       -moz-transform: scale(1);
        -ms-transform: scale(1);
         -o-transform: scale(1);
            transform: scale(1);
}

I know what you’re thinking:

“Ken, what if we want to add new features to our plugin?”

By now if you haven’t realized it, I’ll spill the beans. This article isn’t about writing a modal, it’s about writing a plugin. If you have been following along you should have the tools required to do just this. Say you want to make the plugin open automatically when instantiated. Let’s add an option for that. First, we add the option to our defaults in our constructor method.


// Define our constructor
  this.Modal = function() {

    // Create global element references
    this.closeButton = null;
    this.modal = null;
    this.overlay = null;

    // Define option defaults
    var defaults = {
      autoOpen: false,  <------------------
      className: 'fade-and-drop',
      closeButton: true,
      content: "",
      maxWidth: 600,
      minWidth: 280,
      overlay: true
    }

    // Create options by extending defaults with the passed in arugments
    if (arguments[0] && typeof arguments[0] === "object") {
      this.options = extendDefaults(defaults, arguments[0]);
    }

  }

Next, we check if the option is

true, and if so, fire our open method.


// Define our constructor
  this.Modal = function() {

    // Create global element references
    this.closeButton = null;
    this.modal = null;
    this.overlay = null;

    // Define option defaults
    var defaults = {
      autoOpen: false,
      className: 'fade-and-drop',
      closeButton: true,
      content: "",
      maxWidth: 600,
      minWidth: 280,
      overlay: true
    }

    // Create options by extending defaults with the passed in arugments
    if (arguments[0] && typeof arguments[0] === "object") {
      this.options = extendDefaults(defaults, arguments[0]);
    }

    if(this.options.autoOpen === true) this.open(); <------------------

  }

It is as simple as that.

Conclusion

I sincerely hope that after reading this, everyone learned something they didn’t know before. I personally learned a number of things during the course of writing it! We now have a fully functioning CSS3 modal plugin, but don’t stop here. Make it yours. Add features that you think would be helpful, craft some custom transitions, go absolutely bananas. You don’t even need to make a better modal. Take the plugin building skills you acquired here today, and go build a brand new plugin. Who knows, it could be the next big thing! If you have made it this far, I appreciate your time and diligence and I look forward to writing more fun tutorials here in the future. In the meantime, checkout some cool demos below!

See the Pen Fade and Drop Demo by Ken Wheeler (@kenwheeler) on CodePen.

See the Pen Zoom Demo by Ken Wheeler (@kenwheeler) on CodePen.

See the Pen Zoom and Spin Demo by Ken Wheeler (@kenwheeler) on CodePen.

Ken Wheeler

Lead Front End Engineer @ Media Hive. Creator of Slick Carousel, Guff Sass Library & the Cash library. Dad, Gentleman & Scholar. Startlingly Handsome