How Our Dialogs Work

On our website, we use CSS keyframes animations to achieve our "dialog open" and "dialog close" animations (to see these animations in action, you can click the Contact Info button up at the top). The animations are triggered from JavaScript.

Here is how our animations are defined in CSS:

@keyframes dialog-open {
	0% {
		transform: scale(0.1);
		opacity: 0;
	}
	80% {
		transform: scale(1.1);
		opacity: 1;
	}
	100% {
		transform: scale(1);
		opacity: 1;
	}
}
@keyframes dialog-close {
	0% {
		transform: scale(1);
		opacity: 1;
	}
	100% {
		transform: scale(0.9);
		opacity: 0;
	}
}

Vendor-prefixed versions are omitted for clarity. Our "open" animation scales up to 110% of the proper size for the dialog, and then back down to 100%, to create a bouncing effect. The "close" animation just fades out and scales down to 90% to create a shrinking animation, somewhat similar to the animation that Windows has for closing a window.

Now that we've defined the animations, we just need to be able to trigger them when necessary from our JavaScript code. It's almost as simple as setting the animation property to the desired animation's name, along with duration and easing information. We also will want to be able to set a callback to run after the animation completes, so we need to provide that functionality as well. Finally, we want to un-apply the animation once it completes, so that it can be run again. In browsers that support the CSS animation property (which all modern browsers do, even IE 10+), there is an event that we can listen for in JavaScript to find out when the animation ends.

Here's our generic function for triggering CSS animations, heavily commented, showing how this works:

function animate(element, animation, callback) {
	// This timeout is a hack that's necessary in some browsers to make the animation run
	setTimeout(function() {
		// We have to set several vendor-prefixed properties to support each browser, but that's OK because browsers will ignore unknown properties
		element.style.WebkitAnimation = element.style.MozAnimation = element.style.animation = animation;
	}, 1);
	// DomUtilities contains DOM utilities (no, really!) and it has a "listen" method that can attach multiple event handlers with one callback
	// Again, we have several vendor-prefixed event names, plus the standard event name with two different variations in capitalization, to support all targeted browsers
	DomUtilities.listen(element, "webkitAnimationEnd mozAnimationEnd animationend animationEnd", function(e) {
		// Un-apply the animation, so that it can be run again later if necessary
		element.style.WebkitAnimation = element.style.MozAnimation = element.style.animation = "";

		// stopListening removes an event handler.  In any browser, when removing an event handler, we have to pass the exact same callback function as we passed
		// when we attached the handler in the first place.  Thus, we use arguments.callee, because it refers to the currently executing function.  In this case,
		// the currently executing function IS the event handler, so arguments.callee is just what we need
		// We detach the event handler so that it won't be attached more than once later
		DomUtilities.stopListening(element, "webkitAnimationEnd mozAnimationEnd animationend animationEnd", arguments.callee);

		// If there's a callback provided to fire after the animation is over, we call it now, and pass it the event object just in case the callback wants it
		if (callback) callback(e);
	});
}

In our real code, this function is actually defined as a method on the DomUtilities object. That was just omitted for clarity.

Ultimately, this function just triggers a CSS animation (defined with an @keyframes rule), and then calls a callback function when the animation is over. This function is used in our dialog code to animate the dialog in and out of view, like so:

// Expose a Dialog object globally, so that we can open a dialog from anywhere in our code
window.Dialog = {
	// The "dialog" argument is the dialog <div>
	open: function(dialog) {
		// "Show" the dialog, but keep it invisible with 0 opacity
		dialog.style.display = "block";
		dialog.style.opacity = 0;

		// In the actual code there is a bunch of math to position the dialog properly on the screen, omitted for brevity

		// Using the same animation function defined above, animate using the "dialog-open" animation with a duration of a quarter second and using the "ease-in-out"
		// easing function
		DomUtilities.animate(dialog, "dialog-open 0.25s ease-in-out", function() {
			// After the animation completes, set the dialog's opacity to 1 to keep it visible
			dialog.style.opacity = 1;
		});
	},
	close: function(dialog) {
		DomUtilities.animate(dialog, "dialog-close 0.25s ease-in-out", function() {
			// After the dialog animates out of view, we have to set the display to "none," or else the dialog will still be there
			dialog.style.display = "none";
		});
	}
};

This works just fine in browsers that support CSS animations. However, there are still people out there who use old versions of IE that don't support CSS animations. According to caniuse.com's browser usage table, there is still a substantial percentage (5.66% as of February 2014) of users using IE 8, but not (0.21%) using IE 7. Thus, supporting IE 8+ is a good goal for our site. Since IE 8 and 9 don't support animations, there will be a problem with our code.

Obviously, we can't do the animation in old IE versions (at least not in an efficient or worthwhile way), so it's time for some graceful degradation. Unfortunately, the code as it exists now will degrade ungracefully.

In IE 8, dialogs will probably open just fine (although potentially with some JavaScript errors), because the display will be set to block, and because IE 8 doesn't support opacity or animations, so it will ignore them. However, IE 9 does support opacity, so on IE 9 the dialog will be completely transparent, and therefore invisible. Also, on both IE 8 and 9, the dialog will not be able to close at all, because the callback for the animation, which is where the display property is set, will never fire.

To fix that, we need to be able to detect whether the current browser supports CSS animations or not, and if not, to show or hide the dialog appropriately for those browsers. Here's a function that can detect whether a browser supports a particular CSS property:

function supports(property) {
	// Create a div without actually adding it to the page, and define an array of vendor prefixes (.split(" ") splits the string into an array every place there's a space)
	var div = document.createElement("div"), vendors = "Webkit Moz O Ms".split(" ");
	// Remember if the property works or not
	var works = false;
	// Loop through the vendor prefixes...
	for (var i=0; i<vendors.length; i++) {
		var prefix = vendors[i];
		// ...and construct a property name based on the prefix.  For example, "animation" with the prefix "Webkit" becomes "WebkitAnimation"
		// This is done by concatenating the prefix, plus the first letter of the property name converted to uppercase, plus the rest of the property name
		var propName = prefix + property.substring(0, 1).toUpperCase() + property.substring(1, property.length);
		// If the div's style property has the propName in it, then the property is supported
		if (propName in div.style) works = true;
	}
	// Also check to see if the property is supported without a prefix
	if (property in div.style) works = true;
	// If any of the combinations worked, then works will be true.  Otherwise, it will have stayed false
	return works;
}

And here's how we can incorporate this function into our earlier code (changes in bold):

window.Dialog = {
	open: function(dialog) {
		dialog.style.display = "block";

		if (! supports("animation")) {
			// We've shown the dialog with the display property.  Now we want to stop before we hide it again with opacity, or try to animate it and potentially
			// run into errors.  So we just return
			return;
		}

		dialog.style.opacity = 0;

		// In the actual code there is a bunch of math to position the dialog properly on the screen, omitted for brevity

		DomUtilities.animate(dialog, "dialog-open 0.25s ease-in-out", function() {
			dialog.style.opacity = 1;
		});
	},
	close: function(dialog) {

		if (! supports("animation")) {
			// If we don't support animation, we make sure to hide the box without waiting for an animation that will never happen, and then return
			dialog.style.display = "none";
			return;
		}

		DomUtilities.animate(dialog, "dialog-close 0.25s ease-in-out", function() {
			// For browsers that DO support animation, we hide the dialog here.
			dialog.style.display = "none";
		});
	}
};

Aside from some additional CSS giving the dialogs their styling (background color, padding, rounded corners, positioning, etc.), that's it.