Morphing Menu Button
Codrops is a favorite resource for front-end demos, and one of their latest articles Morphing Button Concept was recently useful to a project I was working on.
However, while looking through the source code I was wondering if it weren’t at all possible to do some of these interactions without JavaScript.
Experimenting with CSS and input
hacks
As the concept between all of these demos is relatively the same — on click/press of an element, have another element come into view for user interaction — I’m going to just stick with showing how to convert over the Sidebar Settings demo to pure CSS.
Let’s break down the HTML first:
<input type="checkbox" id="nav-expand" class="invis">
<nav class="nav-side">
<label for="nav-expand" class="btn-label">
Click plz
</label>
<ul class="menu-list">
<li>
...
</li>
</ul>
</nav>
<div class="main-base">
...
</div>
I’ve cut out anything that isn’t directly necessary to understanding how this demo works. Within what I’ve named the main-base you can place all the necessary content.
As you can see it’s pretty straight forward markup. The key here is the input type="checkbox"
that be visually hidden with the invis
class, and the <label for="nav-expand">
that will serve as our false “button” and means of controlling the checked / unchecked state of the checkbox with a mouse.
The order of the markup here is also important as the checkbox needs to appear before the navigation in the DOM, so that CSS sibling selectors can be used to show/hide.
The CSS
As with the HTML, I’m going to omit some of the CSS that I’ve used for presentational purposes, so we can focus in on just what’s needed to achieve the functionality of this demo. (You can dive into all of the CSS here though).
The following blocks of CSS will get the main foundation of the initial state of our demo setup.
/*
This hides the checkbox from browsers
but not from screen readers
*/
.invis {
height: 1px;
position: absolute;
overflow: hidden;
opacity: 0;
width: 1px;
}
.invis:focus + .nav-side label {
outline: 4px solid #000;
outline-offset: -4px;
}
Now for the main content area and navigation:
/*
Where the content of the page goes
Notice the left: 0px; We need the 'px' here
for our transition animation to work properly
*/
.main-base {
left: 0px;
position: relative;
transition: all .3s;
}
/*
The initial state of the nav
*/
.nav-side {
background: #E85657;
color: #fff;
bottom: 80px;
height: 80px;
left: 40px;
overflow: hidden;
position: fixed;
transition: all .3s;
width: 80px;
z-index: 2;
}
/*
Initial state & global styling of the nav elements
inside of our sidebar menu
As with the note about the left: 0px for .main-base,
notice the height: 0%; here. We need the % to effectively
animate our transition later.
*/
.menu-list {
height: 0%;
overflow: hidden;
visibility: hidden; /* keep the nav hidden to all users */
transition: all .3s;
}
.menu-list a {
display: block;
padding: 12px 8px;
text-decoration: none;
}
/*
Make the cursor look like a pointer, so on hover,
people know they can press the label
*/
.btn-label {
cursor: pointer;
}
That covers the ground work to get our page setup in the inactive state.
Now here’s the CSS that’s needed to morph our nav ‘btn’ into a sidebar, and push the content of our page over to the right.
/*
The :checked pseudo class and + selector
here are the keys to all of this.
Here we move our .nav-side is morphing from
it's original height, width and position to these
new values.
*/
#nav-expand:checked + .nav-side {
bottom: 0;
height: 100%;
left: 0;
width: 200px;
}
/*
Expands our .menu-list from 0% to 100% height
and re-exposes the list to be visible / accessible
*/
#nav-expand:checked + .nav-side .menu-list {
height: 100%;
visibility: visible;
}
/*
Here we're changing our .btn-label from a text label
into a close button. We do this by making our text
transparent (to hide the original 'click plz' text,
changing the label to an inline-block,
and giving it a height/width to reduce it's size
*/
#nav-expand:checked + .nav-side .btn-label {
color: transparent;
display: inline-block;
height: 20px;
line-height: 20px;
text-align: center;
width: 20px;
}
/*
Here we introduce our 'X' using the ::before pseudo class.
This will announce as an "X" to screen readers. This is weird.
*/
#nav-expand:checked + .nav-side .btn-label::before {
color: #000;
content: "X";
line-height: 20px;
}
/*
This final bit moves our page over to the right
and is a further example of why the order of the
markup is important, as we specifically need to
chain our elements together with the + selector
*/
#nav-expand:checked + .nav-side + .main-base {
left: 200px;
}
And that’ll do it!
As mentioned before, the importance of the markup ordering here is key. By having the checkbox as the first element in our DOM, and then having the other elements we need to interact with be siblings of that checkbox, we can use the + sibling selector to chain all of those elements together.
So, how useful is all of this?
Not very.
It’s nice to be able to play around CSS to see what’s achievable without JavaScript. But purposefully ignoring and misusing semantic markup can have detrimental affects on the accessibility of your interface.
While the Codrops demo doesn’t have much in the way of necessary focus management or ARIA to better indicate what’s actually happening here for people using screen readers, this CSS only demo isn’t much better.
Since originally posting this, back in 2014, I’ve made some efforts to clean up the code to make it a bit more accessible… but really something like this would better be achieved with some very light JavaScript, a button
element with aria-expanded="true/false"
to indicate state, and possibly some focus management depending on if the revealed element was modal or not.
If you’re still interested in the “technically accessible” original demo, here’s the CodePen with the full source code.