skip to Main Content

I’m trying to create a simple page with a menu on one side of the main content. Now I want to make the menu collapsible.

As a proof of concept, consider the following example:

#container {
  display: flex;
  position: fixed;
  height: 100%;
}

#menu {
  background: yellow;
}

#toggle {
  cursor: pointer;
  user-select: none
}

#menu.collapsed .content {
  display: none;
}
<div id="container">
  <div id="menu">
    <div id="toggle" onclick="document.getElementById('menu').classList.toggle('collapsed')">X</div>
    <div class="content">Menu content</div>
  </div>
  <div class="content">Main content goes here</div>
</div>

You can click the X to toggle the menu, which works as intended, but the transition is instant which feels jarring. I would like to make it feel smoother by slowly closing the menu.

One way I found to make it work is by setting an explicit width on the menu and using a transition: width property. The stylesheet becomes:

This sort of works, as you can see here:

#container {
  display: flex;
  position: fixed;
  height: 100%;
}

#menu {
  background: yellow;
}

#toggle {
  cursor: pointer;
  user-select: none
}

#menu.collapsed .content {
  overflow: hidden;
}

#menu .content {
  transition: width 1s;
  width: 100px;
}

#menu.collapsed .content {
  width: 0px;
}
<div id="container">
  <div id="menu">
    <div id="toggle" onclick="document.getElementById('menu').classList.toggle('collapsed')">X</div>
    <div class="content">Menu content</div>
  </div>
  <div class="content">Main content goes here</div>
</div>

I don’t really like this solution for the following reasons (in decreasing importance):

  1. I have to guess the initial width (100px in this example). I would rather that the initial width is derived from the natural width of the content, as happened before. But I can’t just remove width: 100px because then the transition no longer works. And width: 100% doesn’t seem to work correctly either.

  2. While the menu is closing, the text reflows, which looks a little ugly. I’d like to prevent this if possible.

  3. Keeping the menu in the DOM tree with width: 0px might be less efficient than display: none because the browser still has to render it (maybe???)

Is there a better way to animate the closing/opening of the menu?

(I found a related question here: Collapsible flexible-width sidebar using only CSS. That one uses max-width instead of width, which sort of solves issue 1, but it introduces a new problem, namely that if max-width is high, it effectively adds a delay to the transition animation.)

2

Answers


  1. To animate the sidebar, you can use a combination of transition: width;, position: fixed, and white-space: nowrap; overflow: hidden; to get a pretty convincing result:

    function openNav() {
      document.getElementById("mySidenav").style.width = "250px";
    }
    
    function closeNav() {
      document.getElementById("mySidenav").style.width = "0";
    }
    .sidenav {
      height: 100%;
      width: 0;
      position: fixed;
      z-index: 1;
      top: 0;
      left: 0;
      background-color: #111;
      overflow-x: hidden;
      transition: 0.5s;
      padding-top: 60px;
    }
    
    .sidenav a {
      padding: 8px 8px 8px 32px;
      text-decoration: none;
      font-size: 25px;
      color: #818181;
      display: block;
      transition: 0.3s;
    }
    
    .sidenav a:hover {
      color: #f1f1f1;
    }
    
    .sidenav .closebtn {
      position: absolute;
      top: 0;
      right: 25px;
      font-size: 36px;
      margin-left: 50px;
    }
    <div id="mySidenav" class="sidenav">
      <a href="javascript:void(0)" class="closebtn" onclick="closeNav()">&times;</a>
      <a href="#">About</a>
      <a href="#">Services</a>
      <a href="#">Clients</a>
      <a href="#">Contact</a>
    </div>
    
    <h2>Animated Sidenav Example</h2>
    <p>Click on the element below to open the side navigation menu.</p>
    <span style="font-size:30px;cursor:pointer" onclick="openNav()">&#9776; open</span>
    Login or Signup to reply.
  2. If you don’t want to set a fixed width for the menu for some reason, then you need to do a lot of manipulation in js. And they will be directed specifically to set the menu width )) Here is an example that does not take into account adaptive resizing, but can use you as a start point:

    document.querySelectorAll('.menu .toggle').forEach(el => {
      el.addEventListener('click', () => {
        const container = el.closest('.container');
        const menuContent = container.querySelector('.menu-content');
        const content = menuContent.querySelector('.content');
        if ( container.classList.contains('collapsed') ) {
          menuContent.style.width = content.getBoundingClientRect().width + 'px';
        } else {
          menuContent.style.width = 0;
        }
        container.classList.toggle('collapsed');
      });
    })
    
    function setMenuWidth() {
      document.querySelectorAll('.menu-content').forEach(el => {
        const container = el.closest('.container');
        if ( container.classList.contains('collapsed') ) {
          return;
        }
        const menuContent = container.querySelector('.menu-content');
        const content = menuContent.querySelector('.content');
        const width = content.getBoundingClientRect().width;
        content.style.removeProperty('width');
        el.style.removeProperty('width');
        content.style.width = width + 'px';
        el.style.width = width + 'px';
      })
    }
    
    setMenuWidth();
    .container {
      display: flex;
      position: fixed;
      height: 100%;
    }
    
    .menu {
      background: yellow;
      flex:none;
    }
    
    .menu .toggle {
      cursor: pointer;
      user-select: none
    }
    
    .menu-content {
      transition: width .4s;
      overflow: hidden;
    }
    
    .menu-content .content {
      transition: opacity .4s;
    }
    
    .container.collapsed .menu-content .content {
      opacity: 0;
    }
    <div class="container">
      <div class="menu">
        <div class="toggle">X</div>
        <div class="menu-content">
          <div class="content">Menu content</div>
        </div>
      </div>
       <div class="content">Main content goes here</div>
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search