对可折叠侧边栏进行动画处理

Animating a collapsible side bar

提问人:Maks Verver 提问时间:11/18/2023 最后编辑:Rory McCrossanMaks Verver 更新时间:11/18/2023 访问量:42

问:

我正在尝试创建一个简单的页面,在主要内容的一侧有一个菜单。现在我想使菜单可折叠。

作为概念证明,请考虑以下示例:

#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>

您可以单击 X 来切换菜单,该菜单按预期工作,但过渡是即时的,感觉很不和谐。我想通过慢慢关闭菜单来让它感觉更流畅。

我发现使它工作的一种方法是在菜单上设置显式并使用属性。样式表变为:widthtransition: width

这种工作,正如你在这里看到的:

#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>

我不太喜欢这个解决方案,原因如下(重要性递减):

  1. 我必须猜测初始宽度(在本例中)。我宁愿初始宽度是从内容的自然宽度派生而来的,就像以前发生的那样。但我不能只是删除,因为那样过渡就不再起作用了。而且似乎也无法正常工作。100pxwidth: 100pxwidth: 100%

  2. 当菜单关闭时,文本会重排,看起来有点难看。如果可能的话,我想防止这种情况发生。

  3. 将菜单保留在 DOM 树中可能比浏览器仍然必须渲染它(也许???)效率低width: 0pxdisplay: none

有没有更好的方法来动画菜单的关闭/打开?

(我在这里找到了一个相关的问题:仅使用 CSS 的可折叠灵活宽度侧边栏。它使用 instead 而不是 ,这在某种程度上解决了问题 1,但它引入了一个新问题,即如果为高,它实际上会为过渡动画添加延迟。max-widthwidthmax-width

JavaScript HTML CSS

评论


答:

0赞 aarvinr 11/18/2023 #1

要对侧边栏进行动画处理,您可以使用 、 和 的组合来获得非常令人信服的结果:transition: width;position: fixedwhite-space: nowrap; overflow: hidden;

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>

评论

0赞 Maks Verver 11/18/2023
这基本上是我得出的解决方案。它有我在问题中提到的所有问题,最值得注意的是我需要修复 css 中的宽度,而不是使用内容宽度。
1赞 aarvinr 11/18/2023
不幸的是,如果不使用 JS 显式测量宽度,就没有办法做到这一点,这是一种浪费。你能做的最好的事情就是将宽度设置为100%,这将填满整个页面。这将确保内容适合,但它可能看起来不是最好的。您还可以使用媒体断点为不同的屏幕分配不同的宽度。
1赞 aarvinr 11/18/2023
至于占用DOM的菜单,这应该不会影响性能。我也曾经摆脱过文本换行。overflow: hidden;
1赞 imhvost 11/18/2023 #2

如果出于某种原因不想为菜单设置固定宽度,那么您需要在 js 中进行大量操作。它们将被专门用于设置菜单宽度 )) 下面是一个不考虑自适应调整大小的示例,但可以使用您作为起点:

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>