shadowfacts.net/site/posts/2019-11-11-js-free-hamburge...

12 KiB

metadata.title = "Building a JavaScript-Free Slide-Over Menu"
metadata.tags = ["web"]
metadata.date = "2019-11-11 21:08:42 -0400"
metadata.shortDesc = "Building a slide-over hamburger menu without using JavaScript."
metadata.oldPermalink = "/web/2019/js-free-hamburger-menu/"
metadata.slug = "js-free-hamburger-menu"
metadata.useOldPermalinkForComments = true

Slide-over menus on the web are a pretty common design pattern, especially on mobile. Unfortunately, they seem to generally be accompanied by massive, bloated web apps pulling in megabytes of JavaScript for the simplest of functionality. But fear not, even if you're building a JavaScript-free web app, or simply prefer to fail gracefully in the event the user has disabled JavaScript, it's still possible to use this technique by (ab)using HTML form and label elements.

Now, I could just spew a bunch of code onto the page and give you cursory explanation of what it's doing, but that would be boring, so instead I'm going to walk through the progression of how I built it and the reasons for the changes I made along the way. If that's all you want, you can take a look at the final version and View Source to see all the code.

We'll start off with a tiny bit of markup in the body (I'm assuming you can set up an HTML page yourself):

<div id="sidebar-content">
	<p>Some sidebar content</p>
</div>
<main>
	<p>
		<!-- lorem ipsum text, just so we don't have an entirely blank page -->
	</p>
</main>

We'll also have some basic CSS to start, so looking at our page isn't looking quite so painful.

body {
	font-family: sans-serif;
}

main {
	position: relative;
	max-width: 980px;
	margin: 0 auto;
}

Then, we'll need the element this whole thing hinges on: a checkbox input. Because of the CSS trick we're using to implement the visibility toggling, the checkbox element needs to be at the same level in the DOM as our #sidebar-container element. We're going to use the adjacent sibling selector (+), which means that the checkbox input needs to come directly before the sidebar container, but you could also use the general sibling selector (~) which would let you put the checkbox anywhere in the DOM given it has the same parent element as the sidebar container.

<input type="checkbox" id="sidebar-visible">
<div id="sidebar-content">
<!-- ... -->

The other half of the HTML behavior that we're relying on to make this work without JavaScript is that clicking <label> tags that are associated with a checkbox toggles the checkbox, and that the label tag can be anywhere in the document in relation to their associated element. We can also have several label elements controlling the same checkbox, which will let us provide a couple different options to the user for how to close the slide-over menu.

We'll need a couple of labels to start with: one shown next to the content and one that will be inside the menu to dismiss it.

<div id="sidebar-content">
	<p>Some sidebar content</p>
	<label for="sidebar-visible" class="sidebar-toggle">Close</label>
</div>
<main>
	<label for="sidebar-visible" class="sidebar-toggle">Open Sidebar</label>
	<!-- ... -->
</main>

Now, all we need to start toggling our sidebar is just a few CSS rules:

#sidebar-visible {
	display: none;
}
.sidebar-toggle {
	cursor: pointer;
}
#sidebar-content {
	display: none;
}
#sidebar-visible:checked + #sidebar-content {
	display: block;
}

The user never needs to see the checkbox itself, since they'll always interact with it through the label elements, so we can always hide it. For a good measure, we'll have our labels use the pointer cursor when they're hovered over, to hint to the user that they can be clicked on. Then we'll hide the sidebar content element by default, since we want it to start out hidden.

The most important rule, and what this whole thing hinges on, is that last selector. We're looking for an element with the ID sidebar-visible that matches the :checked pseudo-selector (which only applies to checked checkboxes or radio inputs) that has a sibling whose ID is sidebar-content. The key is that the element we're actually selecting here is the #sidebar-content, not the checkbox itself. We're essentially using the :checked pseudo-selector as a predicate, telling the browser that we only want to select the sidebar content element when our checkbox is checked.

If we take a look at our web page now, we can see we've got the building blocks in place for our slide-over menu. The page starts off not showing our sidebar content, but we can click the Open Sidebar label to show it, and then click the Close label to hide it once more.

Next, we'll need a bit of CSS to get it looking more like an actual sidebar. To start off, we'll give it a fixed position with all of its edges pinned to the edges of the viewport. We'll also give it a nice high z-index, to make sure it's shown above all of the regular content on our page.

#sidebar-content {
	display: none;
	position: fixed;
	top: 0;
	bottom: 0;
	left: 0;
	right: 0;
	z-index: 100;
}

This will get our sidebar element positioned correctly, but it's not so pretty. To clean it up a bit, we'll move the sidebar content element inside a new container element. Giving both elements background colors will also provide a visual cue of where the sidebar is in relation to the main content of our page.

<div id="sidebar-container">
	<div id="sidebar-content">
		<label for="sidebar-visible" class="sidebar-toggle">Close</label>
		<p>Some sidebar content</p>
	</div>
</div>
#sidebar-container {
	display: none;
	position: fixed;
	top: 0;
	bottom: 0;
	left: 0;
	right: 0;
	z-index: 100;
	background-color: rgba(0, 0, 0, 0.3);
}
#sidebar-visible:checked + #sidebar-container {
	display: block;
}
#sidebar-content {
	background-color: #eee;
}

Note that we've change the rules that we previously applied to #sidebar-content to now target the #sidebar-container element, since that's now the root element of our sidebar. If we take a look at our page again, now we'll see that displaying the content works correctly and the backgrounds for the various parts of our page are good. But, the sidebar looks more like a topbar. Let's fix that by giving it an explicit size, instead of letting it size itself:

#sidebar-content {
	width: 25%;
	height: 100vh;
}

If you haven't encountered it before the vh unit in CSS represents a percentage of the viewport's height, so 100vh is the height of the viewport and 50vh would be half the height of the viewport (likewise, there's a vw unit representing the viewport's width).

Now we're making good progress. Trying out our slide-over menu, one thing that would be nice is the ability to click anywhere outside of the menu to dismiss it, as if we had clicked close instead. We can accomplish that by adding yet another label that's hooked up to our checkbox:

<div id="sidebar-container">
	<!-- ... -->
	<label for="sidebar-visible" id="sidebar-dismiss"></label>
</div>

We'll need to position and size this label so that it covers the entire rest of the page. We could do this manually, specifying the position and sizes of each label, or we could be a bit clever and use Flexbox. First, we'll need to go back and change our sidebar container to be in flexbox mode when it's shown:

#sidebar-visible:checked + #sidebar-container {
	display: flex;
	flex-direction: row;
}

We set the flex direction to row because our sidebar content and the label will layout horizontally across the page, with our content on the left and the dismissal label on the right. We can also go back to our sidebar content styles and remove the height rule. Since we don't specify otherwise, the flex items will expand to fill the container along the axis perpendicular to the flex direction (in this case, that will be the vertical axis), and since the flex container fills the viewport height, so too will the flex items.

#sidebar-content {
	background-color: #eee;
	width: 25%;
}

Making our dismissal label fill the remaining space is then as simple as setting its flex-grow priority to 1 (any number greater than the default of 0, which our content has, will do).

#sidebar-dismiss {
	flex-grow: 1;
}

On our updated page, after opening the slide-over menu, we can click anywhere outside it (in the translucent, darkened area) to dismiss the menu.

The last thing that would be nice to add is a simple transition for when the menu opens or closes. Before we can start adding transitions, we'll need to make a change to our existing CSS. Currently, we're hiding and showing the menu using the display property, switching between none and flex. But that won't work with transitions. Since the browser has no way of knowing how we want to interpolate between the two values, none of the transitions we specify will have any effect because the container will still be shown/hidden instantaneously. Luckily, there's another solution to hiding and showing the container element: the visibility property, which is interpolatable between visible and hidden. So, we'll change our container to always be in flexbox mode, but start out being hidden and then become visible when the checkbox is toggled on.

#sidebar-container {
	visibility: hidden;
	display: flex;
	flex-direction: row;
	/* ... */
}
#sidebar-visible:checked + #sidebar-container {
	visibility: visible;
}

Now we've got the exact same behavior as before, but we have the ability to add transitions. Let's start with making the partially opaque background to fade in and out as the menu is shown and hidden. We can accomplish this by moving the background color rule to only apply when the checkbox is checked, and have the container start out with a fully transparent background color. We'll also instruct it to transition both the visibility and background-color properties.

#sidebar-container {
    /* ... */
    background-color: transparent;
    transition:
          visibility 0.35s ease-in-out,
          background-color 0.35s ease-in-out;
}
#sidebar-visible:checked + #sidebar-container {
	visibility: visible;
	background-color: rgba(0, 0, 0, 0.3);
}

Now if we try showing and hiding the menu, we can see the semi-translucent gray background fades in and out properly, but the sidebar content itself is still shwoing up immediately, without any transition. Let's actually provide a transition for it now. We'll have it slide on and off the side of the page. To do this, we'll initially set the sidebar's left position to -100%, which will put the right edge of the sidebar at the left edge of the screen, leaving the content off-screen. We also need to specify that the content element is positioned relative to itself.

#sidebar-content {
	/* ... */
	position: relative;
	left: -100%;
}

Then, when the checkbox is checked, we can reset the left property to 0, bringing it back on-screen:

#sidebar-visible:checked + #sidebar-container > #sidebar-content {
	left: 0;
}

Lastly, we'll tell the sidebar content element to transition its left property with the same parameters as the background transition:

#sidebar-content {
	/* ... */
	transition: left 0.35s ease-in-out;
}

Now our menu has a nice transition so it's not quite so jarring when it's shown/hidden.

I've polished it up a little bit more for the final version, but the core of the menu is done! And all without a single line of JavaScript.