No-JS Mobile Menu

Almost every site needs a menu, and the pattern is pretty consistent. On large displays, you'll display some full version of the menu on the page at all times. On small displays, you'll have a button that opens the menu. It's pretty clear how to do that with JavaScript. onClick = moveMenu(). Maybe trigger a fancy animation for your open/close button. What might be less clear is how to do that without relying on JS at all.

Here I'm going to focus mostly on making the small-screen version, since the large-screen version has nothing fancy going on. The menu we'll be developing is the example above, which is also the one I use on this site.

The base case

How might we create a mobile menu that does use JS? It might look something like this: (Note this is in Svelte, so I can show you the result easily. If you're not familiar with it that's ok, it should still be easy to follow)

<script>
  let show = false;

  let toggleMenu = () => {
    show = !show;
  };
</script>

<div id="menu_bar">
  <div id="menu" class={show ? "show" : ""}>Contents of the menu</div>
  <button on:click={toggleMenu}>Toggle</button>
</div>

<style>
  div#menu {
    transform: translateY(-100%);
  }
  div#menu.show {
    transform: translateY(0px);
  }
</style>

there it is, just toggle the position using transform when the "toggle" button is clicked. You can see it in action there. In order to get something nice, it'll have to be more complicated than that - a taller menu would be cut off, for example - but that's the general idea.

The trick

How do we accomplish that same idea with only CSS? We need a way to keep track of whether or not the menu is open, and use that information to style our menu. A good way to store user input in CSS is the "checkbox" input type. Using CSS, you can check if a checkbox is checked using :checked, and even style elements based on that information. It makes creating custom checkboxes very straightforwad, and is what we'll use to make this menu.

<div id="menu_bar">
  <label>
    <input type="checkbox" />
    <div id="menu">Contents of the menu</div>
    <p>Toggle</p>
  </label>
</div>

<style>
  input {
    display: none;
  }
  input:checked ~ div#menu {
    transform: translateY(0px);
  }
  div#menu {
    transform: translateY(-100%);
  }
</style>

That's the basic idea! You just store the state of the menu in the checkbox, and it's perfectly interactible automatically.

Complications

There are some complications with the menu as it stands.

  1. The menu bar height always takes up the height of the full menu, which isn't very useful
  2. The dropdown doesn't span the entire menu width
  3. You should probably assign an id to the label-input pair. That is obviously easy to fix, I left it out here for simplicity. You could also use the :has selector instead if you want to try implementing this a slightly different way, that would allow you to put the menu outside the label
  4. If you're actually going to implement this, I wouldn't recommend specifying all of the styles with ids. Again, I did that for simplicity/clarity
See here:

Complications

Menu Size

We can fix the menu size by using another div

<script lang="ts">
</script>

<div id="menu_bar">
  <label>
    <input type="checkbox" />
    <div id="clip">
      <div id="menu">
        <p>Here is a menu</p>
        <p>That is tall</p>
        <p>And useful now</p>
      </div>
    </div>
    <div id="toggle">Toggle</div>
  </label>
</div>

<style>
  input {
    display: none;
  }
  input:checked ~ div div#menu {
    transform: translateY(0px);
  }
  label {
    position: relative;
  }
  div#menu {
    transform: translateY(-100%);
  }
  div#clip {
    position: absolute;
    left: 0px;
    top: 0px;
    width: 100%;
    overflow: hidden;
  }
  div#toggle {
    z-index: 10;
  }
</style>

Fixed

And that's it! The extra div automatically takes the size of its contents, so it's the perfect size to crop our menu. And, all this works without any JS! Try disabling JavaScript on this page (you can use a special extension for that, or if you have uBlock Origin click "More" and </>). The initial JS example won't work, but all the others do! And, so does the menu on this page. It's also easy to add optional functionality that does use JS if you want, like closing the menu if you click outside for example.

Animation

You'll notice the example menu even has a nice animation for the hamburger button. That is also done entirely in CSS. It works similarly to how the menu itself does, except it animates SVG lines. I won't go into detail here, but if you're interested check out the GitHub repo for this site here.

Why bother?

Well, for fun, mostly. But also, because not everyone has JS! Some people are waiting for JS to load because your site is bloated, or they're on a micro-browser, or they have it disabled for security reasons.

Notes

  • This obviously isn't everything involved in the exact menu on this site, but it's enough to get you close. There's a ton of customization you could do to make it behave just the way you like.
  • Firefox can be a bit finnicky with absolute positioning/sizing, so make sure whatever you do works there.
  • If you want to put other form elements in the menu, you'll want to use a different approach. Using :has like I allude to above is likely what you would want.