This week I’ll explain how you can write a navigation drawer (with jQuery) and why you might need one.
The need for a navigation drawer
So my original site design required the use of a horizontal row of button in a sticky header. The problem with this is that mobile devices have very limited room especially horizontal wise. Because of this and because modern mobile users have already understood the meaning of the famous three line button, the navigation drawer make for the perfect navigation system on mobile.
Original design:
This is what happens when the browser window is too small (ie on mobile)
Making a navigation drawer
So now that we have identified a reason to use the navigation drawer, how do we actually make one? Well it turns out to be relatively simple using a bit of Javascript. To start let’s write out the basic skeleton for our plugin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
(function($){ var makeNavDrawer = function(element, options) { $elem = $(element); // Merge config settings var config = $.extend({ // nav is an element that hold a list of 'a' elements for each page nav: undefined, dividerColor: 'rgba(0, 0, 0, 0.2)', drawerWidth: '200px', drawerHeight: '100%', }, options); // do all of the nav drawer work here... }; $.fn.makeNavDrawer = function(options) { return makeNavDrawer($(this), options); }; })(jQuery); |
Next we need to set up our functions to show or hide the drawer. Since css animations is preferred over Javascript animation due to performance reasons, we will be using the ‘transform’ feature as opposed to using jQuery’s .animate() function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var Drawer = (function() { var _isOpen = false; return { isOpen: function() { return _isOpen; }, open: function() { $drawer.css('transform', 'translate(-' + config.drawerWidth + ', 0)'); _isOpen = true; }, close: function() { $drawer.css('transform', 'translate(0px, 0)'); _isOpen = false; } }; })(); |
After this we would need to set up the elements that will actually hold our drawer and add it to the window. The basic idea to do this is first, duplicate our navigation system (in this case it is a <div> containing a list of <a> which define links to other parts of our site) then strip out what we need (the list of <a> elements) and remove the previous container. We can use jQuery to do this dynamically:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
// declare a few variable to minimize function calls. var $body = $("body"); // clone the original navigation element... var $nav = config.nav.clone(); // create a div which will be our drawer... var $drawer = $("<div></div>"); // append the navigation element to the drawer div // and remove the previous container $drawer.append($nav); $nav.children().unwrap(); // create the style that we will use for each of the navigation items var navElementStyle = { display: 'block', 'border-width': '1px', 'border-color': config.dividerColor, 'border-style': 'solid', 'border-bottom-style': 'none', 'box-sizing': 'border-box', height: '50px', 'padding-left': '20px', 'padding-right': '20px', 'line-height': '50px', }; // style each navigation item $drawer.find('a').css(navElementStyle); // create the style for the navigation drawer itself and apply it $drawer.css({ position: 'fixed', width: config.drawerWidth, 'background-color': '#444', height: config.drawerHeight, top: '0px', right: '-' + config.drawerWidth, 'z-index': 4, transition: 'all 0.5s ease-in-out', cursor: 'pointer', '-webkit-tap-highlight-color': 'rgba(0, 0, 0, 0)', }); $body.append($drawer); $drawer.click(function() { // if the user clicks on any of the items inside the drawer, close it Drawer.close(); }); // make the drawer button open and close the drawer... $elem.click(function() { if(Drawer.isOpen()) { Drawer.close(); } else { Drawer.open(); } }); |
Finally, for design reasons, when the user brings up the navigation drawer we want the focus of the site to be the navigation drawer. To achieve this effect, we can dim the rest of the site when the drawer is open and we can animate the dim as well for a nice user experience.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var $overlay = $("<div></div>"); $overlay.css({ position: 'fixed', width: '100%', height: config.drawerHeight, transition: 'all 0.5s ease-in-out', background: '#000', 'z-index': 3, top: '0px', left: '0px', opacity: '0', // This line is very important. Since the overlay will be on top // of everything. Even though it's opacity is set to 0, it can still // handle pointer events (such as mouse clicks) which is not what we // want. Instead we want to allow users to click through the overlay // whenever the overlay is invisible so we need to disable pointer // events. 'pointer-events': 'none', }); $body.append($overlay); |
We will also need to modify our Drawer.open() and Drawer.close() functions so that it will toggle our overlay on and off. So our Drawer object will now look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var Drawer = (function() { var _isOpen = false; return { isOpen: function() { return _isOpen; }, open: function() { $drawer.css('transform', 'translate(-' + config.drawerWidth + ', 0)'); // allow the overlay div to catch click events... $overlay.css('pointer-events', 'auto'); $overlay.css('opacity', 0.6); _isOpen = true; }, close: function() { $drawer.css('transform', 'translate(0px, 0)'); $overlay.css('opacity', 0); // allow pointer events to go through the div $overlay.css('pointer-events', 'none'); _isOpen = false; } }; })(); |
Last but not least we need a few more lines of code to enable users to close the drawer by tapping outside of it.
1 2 3 4 5 6 7 8 9 |
$('html').click(function() { if (Drawer.isOpen()) { Drawer.close(); } }); $elem.click(function(event){ event.stopPropagation(); }); |
And there you have it. A fully functional navigation drawer.
Full source:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
(function ($) { var makeNavDrawer = function (element, options) { $elem = $(element); // Merge config settings var config = $.extend({ // nav is an element that hold a list of 'a' elements for each page nav: undefined, dividerColor: 'rgba(0, 0, 0, 0.2)', drawerWidth: '200px', drawerHeight: '100%', }, options); var Drawer = (function () { var _isOpen = false; return { isOpen: function () { return _isOpen; }, open: function () { $drawer.css('transform', 'translate(-' + config.drawerWidth + ', 0)'); // allow the overlay div to catch click events... $overlay.css('pointer-events', 'auto'); $overlay.css('opacity', 0.6); _isOpen = true; }, close: function () { $drawer.css('transform', 'translate(0px, 0)'); $overlay.css('opacity', 0); // allow pointer events to go through the div $overlay.css('pointer-events', 'none'); _isOpen = false; } }; })(); // declare a few variable to minimize function calls. var $body = $("body"); // clone the original navigation element... var $nav = config.nav.clone(); // create a div which will be our drawer... var $drawer = $("<div></div>"); // the overlay var $overlay = $("<div></div>"); // append the navigation element to the drawer div $drawer.append($nav); // append the navigation element to the drawer div // and remove the previous container $drawer.append($nav); $nav.children().unwrap(); // create the style that we will use for each of the navigation items var navElementStyle = { display: 'block', 'border-width': '1px', 'border-color': config.dividerColor, 'border-style': 'solid', 'border-bottom-style': 'none', 'box-sizing': 'border-box', height: '50px', 'padding-left': '20px', 'padding-right': '20px', 'line-height': '50px', }; // style each navigation item $drawer.find('a').css(navElementStyle); // create the style for the navigation drawer itself and apply it $drawer.css({ position: 'fixed', width: config.drawerWidth, 'background-color': '#444', height: config.drawerHeight, top: '0px', right: '-' + config.drawerWidth, 'z-index': 4, transition: 'all 0.5s ease-in-out', cursor: 'pointer', '-webkit-tap-highlight-color': 'rgba(0, 0, 0, 0)', }); $overlay.css({ position: 'fixed', width: '100%', height: config.drawerHeight, transition: 'all 0.5s ease-in-out', background: '#000', 'z-index': 3, top: '0px', left: '0px', opacity: '0', // This line is very important. Since the overlay will be on top // of everything. Even though it's opacity is set to 0, it can still // handle pointer events (such as mouse clicks) which is not what we // want. Instead we want to allow users to click through the overlay // whenever the overlay is invisible so we need to disable pointer // events. 'pointer-events': 'none', }); $body.append($drawer); $body.append($overlay); $drawer.click(function () { // if the user clicks on any of the items inside the drawer, close it Drawer.close(); }); // make the drawer button open and close the drawer... $elem.click(function () { if (Drawer.isOpen()) { Drawer.close(); } else { Drawer.open(); } }); $('html').click(function () { if (Drawer.isOpen()) { Drawer.close(); } }); $elem.click(function (event) { event.stopPropagation(); }); }; $.fn.makeNavDrawer = function (options) { return makeNavDrawer($(this), options); }; })(jQuery); |
Example use: