DEV Community

Cover image for Mobile-to-Desktop Menu in 2 Lines of CSS
Mads Stoumann
Mads Stoumann

Posted on Edited on

Mobile-to-Desktop Menu in 2 Lines of CSS

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:

<header>
  <a href="/">LOGO</a>
  <label>
    <input type="checkbox">
  </label>
  <div class="menu-flyout">
    <nav class="menu-main">
      <a href="#">...</a>
      <a href="#">...</a>
    </nav>
    <nav class="menu-cta">
      <a href="#">CTA</a>
      <a href="#">CTA</a>
    </nav>
  </div>
</header>
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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, <header> 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;
}
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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

flex-direction: var(--menu-flyout-dir, column);
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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

Flyout


The toggler is just a styled <input type="checkbox">. We can use this to reveal the flyout when clicked:

header:has(input:checked) .menu-flyout {
  translate: 0;
}
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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; }
}
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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 <label> with the toggler is hidden as expected, the <header> 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;
  }
}
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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;
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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%
);
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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 <body>-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;
  }
}
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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;
}
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

... 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));
}
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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 <label> with:

<button
  class="menu-toggle"
  aria-label="Toggle flyout menu"
  aria-expanded="false"
  aria-controls="flyout-menu">
</button>
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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')
    );
})
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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; }
šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung

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

Top comments (11)

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
alexpgmr profile image
Alex • • Edited

Great menu!
Is it possible to fix the effect of increasing the distance between items when you enlarge the window horizontally in mobile?

Image description

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann • • Edited

Yes! There are multiple gap's in use, though.

In .menu-main (the main items), the gap is using clamp, so it changes dynamically, depending on the viewport-width:

gap: clamp(0.5rem, -3.875rem + 14vw, 4rem);

You can adjust these, or make your own here, or simply enter a static value in a "mobile-only" media-query.

The gap between the CTA-items is using the same method, but with smaller values.

Desktop:
On the wrapper, the space between the logo, the main items and the CTA is spaced with justify-content: space-between, but you can add a gap-property, if you want.

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
alexpgmr profile image
Alex •

Thank you!

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
avikapoor_ssojet profile image
Andrew •

This is a very clever and elegant solution to create a responsive menu in CSS. I love how you have used the :hover and :focus-within pseudo-classes to toggle the menu visibility. I also appreciate how you have explained the code and provided a demo link. This is a very useful tip for web developers. Thank you for sharing!

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Thank you!

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
mxglt profile image
Maxime Guilbert •

Great post! Really useful!

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Thanks!

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
scottwright_dev profile image
Scott •

This is super helpful, I’d like to try this out on my next project. Appreciate you sharing it.

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Thank you!

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
miracool profile image
Makanju Oluwafemi •

Great content!

šŸ† Live Soccer TV: madsstoumann/mobile to desktop menu in lines of css ikn - Jadwal Siaran Langsung
 
madsstoumann profile image
Mads Stoumann •

Thank you!