Mobile-to-Desktop Menu in 2 Lines of CSS

Valentsea
Total
0
Shares

As a frontend developer, I’ve created numerous menus over the years. These menus have typically been the most intricate element of a website, incorporating mobile interactions, desktop transitions, item reordering, and more. In one instance, I built a mega-menu for a client that was so expansive, users mistook it for an entire webpage due to its full-page coverage.

However, in my most recent project, I aimed for simplicity by utilizing some of the latest and greatest CSS features available.

Let’s dive in!




Structure

For the markup, the simplest structure I could come up with, that still supported the flexibility needed to go from a mobile flyout to a desktop-menu, was this:

href="/">LOGO type="checkbox"> class="menu-flyout"> class="menu-main"> href="#">... href="#">... class="menu-cta"> href="#">CTA href="#">CTA
Enter fullscreen mode

Exit fullscreen mode

We’ll add more classes and stuff later, but this is the basic structure. This version will work without any JavaScript, but has an accessibility-issue, we’ll look into later as well.

The main element,

is a flex-container, using justify-content: space-between to place the logo left and the toggler right:

Mobile Menu Collapsed

.menu-flyout is the container for two navigation-blocks: one with the main menu-items, and one with CTA’s (call-to-actions).

The flyout will cover the whole screen when visible, but otherwise be placed off-screen:

.menu-flyout {
  inset: 0;
  position: fixed;
  translate: -100vw 0;
}
Enter fullscreen mode

Exit fullscreen mode

The navigation blocks are both flex-containers with flex-direction set to a custom property:

flex-direction: var(--menu-flyout-dir, column);
Enter fullscreen mode

Exit fullscreen mode

The second navigation block is placed at the bottom, using justify-content: end:

Flyout


The toggler is just a styled . We can use this to reveal the flyout when clicked:

header:has(input:checked) .menu-flyout {
  translate: 0;
}
Enter fullscreen mode

Exit fullscreen mode

Flyout

Since the toggler is wrapped in a label, we can use this to hide it on desktop:

@media (min-width: 768px) {
  label { display: none; }
}
Enter fullscreen mode

Exit fullscreen mode

NOTE: Don’t worry, everything will have classes in the final examples, it’s just to simplify the examples, that I use plain tags!


So far, so good. When we resize the screen to desktop, we get:

Desktop

OK, so the with the toggler is hidden as expected, the

is still a flex-container, the flyout is still placed off-screen, but is now gigantic, taking up the whole screen real-estate.

Let’s fix that with two lines of CSS:

@media (min-width: 768px) {
  .menu-flyout {
    --menu-flyout-dir: row;
    display: contents;
  }
}
Enter fullscreen mode

Exit fullscreen mode

Which results in:

Desktop menu

Yay! If we inspect that, we’ll see that the two navgation blocks are now “direct” items in our main flex-container, and .menu-flyout seems to have disappeared:

Flex container

So what’s going on?

display: contents is the same as saying to an element: “Forget your own box, just display your child-nodes”. Or in formal MDN-lingo:

These elements don’t produce a specific box by themselves. They are replaced by their pseudo-box and their child boxes.

We change the flex-direction for both navigation-blocks, by updating the custom property, we declared earlier, --menu-flyout-dir.


We now have a working, mobile-to-desktop menu using very little HTML and CSS and no JavaScript at all.

Let’s see what else we can do. I’ve set the desktop max-width to 1200px, but would like to “stretch” the background-color to the edge of the screen.

This used to require an extra element around the menu, but can now be done with a very large border-image:

border-image: conic-gradient(
  hsl(240, 10%, 20%) 0 0)
  fill 0//100vw;
Enter fullscreen mode

Exit fullscreen mode

OK … this will actually cover the whole screen.

Let’s add a clip-pathto fix that:

clip-path: polygon(
  -100vw 0,
  100vw 0,
  100vw 100%,
  -100vw 100%
);
Enter fullscreen mode

Exit fullscreen mode

Now, even on very large desktops, the background will stretch to the edge of the screen (you probably need to zoom-in to see it!):

Large Desktop




Mobile fixes

On mobile, we can use :has to detect when the toggler is checked, even from the -element. We can utilize this to prevent overflow/scrolling, when the flyout is visible:

@media (max-width: 767px) {
  body:has(.menu-toggle:checked) {
    overflow: hidden;
  }
}
Enter fullscreen mode

Exit fullscreen mode


If you rotate your (large) phone, the menu will switch to the desktop-version.

If you have an iPhone with a “notch”, the menu will “go into” that notch. We can fix this with the env()-function and safe-area-inset. First, we create two variables with block- and inline-padding:

header {
  --menu-pb: .75em;
  --menu-pi: 1.5em;
}
Enter fullscreen mode

Exit fullscreen mode

… and later on, we’ll define the padding:

header {
  padding:
    var(--menu-pb)
    calc(env(safe-area-inset-right) 
    + var(--menu-pi))
    var(--menu-pb)
    calc(env(safe-area-inset-left)
    + var(--menu-pi));
}
Enter fullscreen mode

Exit fullscreen mode

Now, when you rotate your phone, extra padding will be added inline, if the phone has a notch!



Demo




Accessibility Concerns

While the menu above works fine without any JavaScript, the toggle-button-checkbox is a hack, not working well with screen-readers.

Let’s add an id to the flyout, and replace the with:


  class="menu-toggle"
  aria-label="Toggle flyout menu"
  aria-expanded="false"
  aria-controls="flyout-menu">

Enter fullscreen mode

Exit fullscreen mode

Then add a small JS-snippet:

const toggle = document.querySelector(
  '.menu-toggle');
toggle.addEventListener('click', () => {
  toggle.setAttribute(
    'aria-expanded', document.body
    .classList.toggle('menu-open')
    );
})
Enter fullscreen mode

Exit fullscreen mode

This snippets toggles a class, menu-open on the body-element. We’ll use the status, whether the class is set or not, for the aria-pressed-attribute.

All the places we used :checked before, we should now use menu-open:

.menu-open .menu-flyout { translate: 0; }
Enter fullscreen mode

Exit fullscreen mode



Demo


The demos have a lot more stuff going on (transitions, clamped gaps, hover etc.) than I showcased in this article.

Open them on Codepen to see the full-width desktop-view — fork them — and play around with them.

Let me know what you think in the comments.


Photo by Brett Sayles

Total
0
Shares

JavaScript Array method .map()

Hello Everyone, in this post I will try to explain about JavaScript Array map() method with the help…

You May Also Like