Build a Responsive Slideout Drawer
Introduction
A slideout drawer is a menu that lives outside your application’s viewport and slides in and out of view based upon some user interaction (e.g. a user clicking or tapping a hamburger menu button). Slideout drawers are fantastic patterns to follow when screen real estate is limited and you need to provide additional features and functionality to your end user. This tutorial will walk you through how to setup a simple slideout drawer primarily using CSS3 and a little JavaScript. We’ll then take it a step further and show you how you can easily make your slideout drawer responsively adjust to changing viewports.
Take a look at this CodePen Demo for a preview of what we’ll be building.
Before We Begin
To get the most of out this tutorial you should be familiar with basic web development practices. Specifically, you should have some experience with HTML5, LESS for CSS development and the latest versions of the JavaScriptlanguage (e.g., ES6, ES2015, etc.). If your not comfortable setting up a development workspace you can easily setup a quick environment using codepen, plunkr or jsfiddle to follow along with this tutorial.
The HTML
For our slideout drawer we are going to create a navigation menu that slides out from the left-hand side of the page after a hamburger menu button is clicked from a navbarcomponent. To make our setup a little easier, faster and nicer to look at we’ll be using Bootstrap 4, jQuery, and Font Awesome. So, let’s get started.
Setup Your Page
The first thing we need to do is setup a simple page and include all of our previously mentioned dependencies. The structure listed below pretty much comes right out of the Bootstrap 4 Getting Started documentation with the exception of Font Awesome.
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css">
<!-- Font Awesome CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<!-- Bootstrap JS Dependencies -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js"></script>
</body>
</html>
We then need to add a #siteWrapper
div container just inside the body
tag. The #siteWrapper
will house our navbar and our slideout drawer.
<body>
<div id="siteWrapper" class="site-wrapper"></div>
</body>
Adding the Navbar
Just inside the #siteWrapper
container add the nav
element with the .navbar navbar-dark bg-dark
class names. This snippet of code, again, comes right from Bootstrap 4's navbar component and it will create and position for us a navbar with a hamburger menu button at the top of our page. The hamburger menu button will be used to open our slideout drawer.
<body>
<div id="siteWrapper" class="site-wrapper">
<!--Navbar-->
<nav class="navbar navbar-dark bg-dark">
<a id="hamburgerMenuButton" class="navbar-brand" href="#" role="button">
<i class="fa fa-bars" aria-hidden="true">
<span class="sr-only">Open sidebar menu.</span>
</i>
</a>
</nav>
</div>
</body>
Adding the Slideout Drawer
Underneath our nav
element we'll now add the #slideout
and #slideoutMask
containers. Our #slideout
container consists of both a header (.slideout-header
) and content (.slideout-content
) areas. The .slideout-header
contains a close button for our slideout drawer and the .slideout-content
section contains our site's navigation links. Lastly, our #slideoutMask
, which sits at the bottom of the page, will cover or mask our entire site when the slideout drawer is enabled. This will direct the user's attention to the open slideout drawer and reduce any noise or distraction from the rest of the page.
<body>
<nav>
...
</nav>
<div id="slideout" class="slideout">
<div class="slideout-header">
<button id="slideoutCloseButton" href="#" class="btn btn-link slideout-close" title="Close">
<i class="fa fa-times fa-lg">
<span class="sr-only">Close slideout menu.</span>
</i>
</button>
</div>
<div class="slideout-content">
<section class="slideout-section">
<h2 class="section-title">Explore</h2>
<nav>
<ul class="list-unstyled section-list">
<li class="section-list-item" role="presentation">
<a href="#">Home</a>
</li>
<li class="section-list-item" role="presentation">
<a href="#">About</a>
</li>
<li class="section-list-item" role="presentation">
<a href="#">Contact</a>
</li>
</ul>
</nav>
</section>
</div>
</div>
<div id="slideoutMask" class="slideout-mask"></div>
</body>
Now that we have our markup in place let’s move forward and style our slideout drawer.
The CSS
As mentioned above we’ll be using the LESS preprocessor for our CSS code development. Before we start coding we’ll define a color palette for our slideout drawer and define some global variables and mixins that will be used throughout our implementation.
Add a Color Palette
In using any CSS preprocessor, whether it’s LESS or SASS, one of the first things we should do is setup a basic color palette for the feature. We’ll keep our color palette for the slideout drawer really simple and just define colors for white, black, red and three tones of gray.
/* Color Palette
----------------------------------------------*/
@white: #FFFFFF;
@black: #000000;
@red: #DB0900;
@gray: #868E96;
@gray-light: lighten(@gray, 10%);
@gray-lighter: lighten(@gray-light, 10%);
Add Variables
In addition to a color palette, it’s also a good idea to create a variables section and list out any global values for properties that we’ll need to reference throughout our feature. For our slideout drawer we’ll need to manage and adjust the z-index
property of both our slideout drawer and the drawer's mask, which covers and darkens the screen when our slideout drawer is opened.
/* Variables
----------------------------------------*/
// Base for z-index property.
@zindex: 1000;
Add Mixins
We’ll also add three lightweight mixins for convenience. The first two mixins, .drop-shadow
and .opacity
should be somewhat self-explanatory. We'll be using the .drop-shadow
mixin to add a shadow to our slideout drawer and the .opacity
mixin to control the opacity of our slideout drawer's mask when the slideout drawer is opened and closed. The last mixin, .hover
, will be used to apply a subtle color fade on hovered link elements and anchor tags.
/* Mixins
-----------------------------------------------*/
.drop-shadow (@x: 0, @y: 1px, @blur: 2px, @spread: 0, @alpha: 0.25) {
box-shadow: @x @y @blur @spread rgba(0, 0, 0, @alpha);
}.opacity (@opacity: 0.5) {
opacity: @opacity;
}.hover (@color: @black, @hover-color: @red, @text-decoration: none) {
color: @color;
text-decoration: none;
cursor: pointer;
transition: color 0.3s ease; &:hover, &:focus {
color: @hover-color;
text-decoration: @text-decoration;
}
}
Style the Slideout Container
We want our .slideout
drawer to stretch from the top of the page to the bottom of the page and we only want it to be about 560px or (35em)
wide for desktop users. To make this happen we'll make use of the position
, top
, left
, bottom
and width
properties defined in the below code snippet. We'll adjust the width to be a bit more responsive later on.
.slideout {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 35em;
}
We also want to style our slideout drawer with a white background and a gray border with a slight drop shadow on the right-hand side of drawer to make it really look like the drawer is hovering above the main page. To achieve this, take a look at the below code, where we’ve added background-color
and border-right
properties as well as added our drop shadow effect using our .drop-shadow
mixin that we defined earlier.
.slideout {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 35em;
background-color: @white;
border-right: 0.063em solid lighten(@gray-light, 30%);
.drop-shadow(@x: 1px, @y: 0, @blur: 8px, @spread: 0, @alpha: 0.50);
}
We want our .slideout
container to explicitly sit above other positioned elements. To make this happen we've added the z-index
property and set it to a 1000
with our previously defined @zindex
variable. This will force our .slideout
container above other positioned elements as long as those elements also receive a z-index
property smaller then a 1000
. This is something we'll do later on when styling the slideout mask.
.slideout {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 35em;
background-color: @white;
border-right: 0.063em solid lighten(@gray-light, 30%);
.drop-shadow(@x: 1px, @y: 0, @blur: 8px, @spread: 0, @alpha: 0.50);
z-index: @zindex;
}
Setup the Slideout Container for Animation
Adding to the .slideout
style definition we also want the ability to move our slideout drawer on and off screen. We can accomplish this with the transform
property, which applies a 2D or 3D transformation to an element. Transformations include rotating, scaling, moving or skewing an element and this is exactly what we'll need to move our slideout drawer on and off the screen. To make transformations easier the transform
property provides several transform-functions
such as translateX(x)
, translateY(y)
, scale(x,y)
and many others (check out the MDN web docs on transform-functions). As a starting point for our slideout drawer we'll give the transform
property a value oftranslateX(-100%)
. This will position our slideout drawer completely off-screen. Later on in the tutorial we'll update this value to translateX(0)
to move the slideout drawer into view. For a nice explanation of the transform
property checkout this CSS-Tricks article on CSS transforms.
.slideout {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 35em;
background-color: @white;
border-right: 0.063em solid lighten(@gray-light, 30%);
.drop-shadow(@x: 1px, @y: 0, @blur: 8px, @spread: 0, @alpha: 0.50);
z-index: @zindex;
transform: translateX(-100%);
}
The transform
property will help us move our .slideout
container on and off the screen, however, our goal is to animate our .slideout
container from one position to another. The transform
property alone is not enough to wire up a smooth animation. We need to tell the browser that we want a smooth transition or animation from one position to the next. To do that we'll use the transition
property, which allows you the ability to smoothly change or animate property values. The below code example highlights the syntax for the transition
property. For a full explanation checkout this CSS-Tricks article on CSS transitions.
transition: <property> <duration> <timing-function>
To animate our slideout drawer along the x-axis, we'll define the transition
property as transition: transform 0.3s ease
. This means that any update to the transform
property value will result in a smooth animation that takes .3 seconds where the speed of the animation is calculated by the ease timing-function (checkout the MDN web docs on transition-timing-functions for more information). For example, if we were to change transform: translateX(-100%)
to transform: translateX(0)
our .slideout
container would smoothly slide into view. Likewise, changing the values back again from transform: translateX(0)
to transform: translateX(-100%)
would animate the .slideout
container off screen.
In addition to animating the transform
property, you've probably noticed that we have also added width 0.3s ease
to our transition
. You can define multiple transitions, separated by a comma, to the transition
property. In our case, extending the transition
property with width 0.3s ease
states that we'd also like to animate the width
property with a duration
of .3 seconds using the ease timing function. We'll explain why we're animating the width
property later on in the tutorial.
.slideout {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 35em;
background-color: @white;
border-right: 0.063em solid lighten(@gray-light, 30%);
.drop-shadow(@x: 1px, @y: 0, @blur: 8px, @spread: 0, @alpha: 0.50);
z-index: @zindex;
transform: translateX(-100%);
transition: transform 0.3s ease, width 0.3s ease;
}
Style the Slideout Header
When our slideout drawer is open we’ll want the ability to close it. For that reason, we have a .slideout-header
container with a nested .slideout-close
button. The .slideout-header
container is given some padding
to increase the visual space around the close button. We'll also be using flexbox
to position the .slideout-close
button. To do so, we'll first set the display
property of the .slideout-header
to flex
, which defines the .slideout-header
as a flex container. Next we'll set the justify-content
property on .slideout-header
to flex-end
. This will position the nested .slideout-close
button toward the far right-hand side of the .slideout-header
container. For more on flexbox checkout Chris Coyier's Complete Guide to Flexbox.
.slideout-header {
padding: 1.2em 1.0em 1.2em 1.7em;
display: flex;
justify-content: flex-end;
.slideout-close {
display: inline-block;
.hover(@color: lighten(@gray-light, 10%), @hover-color: @black);
&:focus {
outline: auto 5px -webkit-focus-ring-color;
}
}
}
Style the Slideout Content
Along with a .slideout-header
our slideout drawer has a .slideout-content
container, which, as you may have already guessed, contains all of our slideout drawer's contents. Similar to the .slideout-header
we'll add some padding
to the .slideout-content
container for visual space and add a border-top
definition to the container to add some visual separation between the header and content area of our slideout drawer. Lastly, for a little added polish, we'll apply our .hover
mixin to all anchor tags within the .slideout-content
container. The .hover
mixin adds a smooth color transition when hovering or focusing site links. In our case, the .hover
mixin will fade from black to a dark red when hovered or focused.
.slideout-content {
border-top: 0.063em solid lighten(@gray-light, 10%);
padding: 3.750em;
a {
.hover(@hover-color: darken(@red, 10%));
text-transform: uppercase;
font-size: 0.875em;
}
}
Style the Slideout Sections
Our slideout drawer also contains a navigation menu for our site, which consists of a menu title and three links (e.g., home, about and contact). We want the .section-title
to be a bit larger then our navigation links so we set the font-size
to 1.125em
or (18px)
and we set the text-transform
property to uppercase
. We also wanted to have a little separation between the .section-title
and our .section-list
so, first, we reset the margin
and padding
definitions and then we give the .section-title
a heavy margin-bottom
of 1.563em
or (25px)
. Lastly, the .section-list
and .section-list-item
containers, which make up our actual menu, is mostly relying on Bootstrap 4's .list-unstyled
class definition along with some minor tweaks to margin
and padding
definitions for its over all visual appearance.
.slideout-section {
.section-title {
font-size: 1.125em;
text-transform: uppercase;
margin: 0 0 1.563em 0;
padding: 0;
}
.section-list {
margin: 0;
.section-list-item {
padding: 0 0 0.625em 0;
}
}
}
Style the Slideout Mask
Whenever our slideout drawer is opened we want to cover or mask everything that lives underneath it. We’ll do this by setting the position
, top
, left
, bottom
and right
properties for the .slideout-mask
as shown below. To picture what we're doing think of the .slideout-mask
container as a rectangle. We are simply forcing each of the sides of our rectangle to stick to the four corners of your browser window.
.slideout-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
While unlikely, let's also add overflow: hidden
to our .slideout-mask
, which will explicitly prevent any scrolling behavior.
.slideout-mask {
position: fixed;
overflow: hidden;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
Now that our mask is covering our screen we want to give it a slightly transparent background color. We'll accomplish this by setting the background-color
property using our @black
variable and then we'll adjust the opacity of the background with our .opacity
mixin that we defined earlier.
.slideout-mask {
position: fixed;
overflow: hidden;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: @black;
.opacity(@opacity: .5);
}
Next we'll add the z-index
property to our .slideout-mask
and we'll set it to have a value that is less then our .slideout
container. If your remember from earlier on in the tutorial we set the z-index
on our .slideout
container to be a 1000
using the @zindex
variable. Setting the z-index
property on the .slideout-mask
to be less then a 1000
will explicitly position our .slideout-mask
above the main page but not above our .slideout
container, which houses our navigation menu.
.slideout-mask {
position: fixed;
overflow: hidden;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: @black;
.opacity(@opacity: .5);
z-index: (@zindex - 1);
}
When we open and close our slideout drawer we'd like our mask to fade in and fade out, respectively. To start we'll use our .opacity
mixin and set it to 0
to make our .slideout-mask
fully transparent. For this tutorial our .slideout-mask
container is always present, always lives on the main page and is stacked on top of our main page regardless of it's opacity. Without adding anything more to our .slideout-mask
definition we've created an invisible sheet that is covering our main page, which prevents the user from interacting with any of the underlying links or buttons. To fix this issue, we need to also add the visibility
property and initially set it to hidden
. The visibility
property, unlike the display
property can be animated to show or hide an element without removing that element from the layout of our document. Lastly, we configure the transition
property to animate any changes to opacity
or visibility
. For more information on what CSS properties can be animated checkout the MDN web docs on animatable css properties.
.slideout-mask {
position: fixed;
overflow: hidden;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: @black;
z-index: (@zindex - 1);
.opacity(@opacity: 0);
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
Add a Style to Open the Slideout Drawer
The last piece of CSS that needs to be added is the .slideout-open
definition. This class, using JavaScript, will be dynamically added and removed from our #siteWrapper
container. If you remember from when we set up our markup, the #siteWrapper
wraps our entire slideout drawer feature and represents the top-most level of our template. We'll append the .slideout-open
class to the #siteWrapper
container to override the opacity
and visibility
settings on our .slideout-mask
as well as position our .slideout
drawer into view with the transform
property and since these properties also have a transition
associated with them changes in their values will result in a smooth animation.
.slideout-open {
.slideout-mask {
.opacity(@opacity: 0.7);
visibility: visible;
}
.slideout {
transform: translateX(0);
}
}
Add Responsive Styling
To make our slideout drawer more responsive we’ll write a media query that will set the width
of our .slideout
container to 100vw
any time our slideout drawer is open and the browser width is less then or equal to 768px
. So, when our slideout drawer is open and the browser width is reduced our slideout drawer's width will grow until it reaches the full width of the available viewport.
@media (max-width : 768px) {
.slideout-open {
.slideout {
width: 100vw;
}
}
}
Now that our CSS is complete let’s bring it all together with some JavaScript.
The JavaScript
We want our slideout drawer to open when the user selects the hamburger menu in the navigation header as well as close with the slideout drawer’s close button. We’ll wire up this functionality with Native JavaScript. The below code is relatively lightweight and could easily be translated to work within your framework of choice (i.e., AngularJS, React, VueJS, etc.)
To get started we’ll first cache references to the #siteWrapper
, #hamburgerMenuButton
and the #slideoutCloseButton
using the const
declaration. We have chosen to use the const
declaration instead of the let
keyword because we assume that the chosen variable identifiers won't or shouldn't change. For more information on the const
declaration checkout the MDN documentation on using the const keyword.
// Cache references to DOM nodes.
const siteWrapper = document.querySelector('#siteWrapper');
const hamburgerMenuButton = document.querySelector('#hamburgerMenuButton');
const slideoutCloseButton = document.querySelector('#slideoutCloseButton');
We’ll then add the openSlideoutDrawer
function which will open our slideout drawer by appending the .slideout-open
class name onto the #siteWrapper
container.
// Cache references to DOM nodes.
const siteWrapper = document.querySelector('#siteWrapper');
const hamburgerMenuButton = document.querySelector('#hamburgerMenuButton');
const slideoutCloseButton = document.querySelector('#slideoutCloseButton');
function openSlideoutDrawer () {
siteWrapper.classList.add('slideout-open');
slideoutCloseButton.focus();
}
We’ll also need to add the closeSlideoutDrawer
function which will close our slideout drawer by removing the .slideout-open
class name from the #siteWrapper
container.
// Cache references to DOM nodes.
const siteWrapper = document.querySelector('#siteWrapper');
const hamburgerMenuButton = document.querySelector('#hamburgerMenuButton');
const slideoutCloseButton = document.querySelector('#slideoutCloseButton');function openSlideoutDrawer () {
siteWrapper.classList.add('slideout-open');
slideoutCloseButton.focus();
}function closeSlideoutDrawer () {
siteWrapper.classList.remove('slideout-open');
hamburgerMenuButton.focus();
}
Finally, we’ll use the addEventListener
method to call the openSlideoutDrawer
function anytime the hamburgerMenuButton
receives a click
event. Using the same approach, we'll also wire up the slideoutCloseButton
to trigger the closeSlideoutDrawer
function.
// Cache references to DOM nodes.
const siteWrapper = document.querySelector('#siteWrapper');
const hamburgerMenuButton = document.querySelector('#hamburgerMenuButton');
const slideoutCloseButton = document.querySelector('#slideoutCloseButton');
function openSlideoutDrawer () {
siteWrapper.classList.add('slideout-open');
slideoutCloseButton.focus();
}
function closeSlideoutDrawer () {
siteWrapper.classList.remove('slideout-open');
hamburgerMenuButton.focus();
}
// Bind event listeners.
hamburgerMenuButton.addEventListener('click', openSlideoutDrawer, false);
slideoutCloseButton.addEventListener('click', closeSlideoutDrawer, false);
Conclusion
This tutorial covered one way of creating a slideout drawer with transitions. I’m positive there are other approaches and techniques but this should give a nice starting point for you to play around with different implementations.
I hope you enjoyed this tutorial and thanks for checking it out! Please leave me a comment with any feedback, improvements or suggestions.