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.