侧边栏展开逻辑设计思路总结

2026 年 2 月 8 日 星期日(已编辑)
18

侧边栏展开逻辑设计思路总结

最近,在为 Linums Chat 编写网页侧边栏 collapse 逻辑时,在交互体感上遇到了一些问题,故写下此总结,避免以后继续踩坑。

首先,本项目的侧边栏组件简略代码如下:

export function Sidebar({ children, className }: SidebarProps) {
  const collapsed = useUIStore((s) => s.sidebarCollapsed)
  const toggleSidebar = useUIStore((s) => s.toggleSidebar)
  
  return (
    <aside
      className={cn(
        'flex h-screen flex-col bg-[hsl(var(--sidebar-bg))] dark:bg-[hsl(var(--sidebar-bg))] transition-all duration-300 relative',
        'overflow-x-hidden',  
        collapsed ? 'w-16' : 'w-64',
        className
      )}
    >
      {/* 顶部:Logo和折叠按钮 */}
      <div className={cn(
        "p-3",
        collapsed ? "flex flex-col items-center gap-2" : "flex items-center justify-between px-4"
      )}>
        <div className={cn(
          "flex items-center gap-2",
          collapsed && "justify-center"
        )}>
          <Slack width={20} height={20}/>
          {!collapsed && (
            <span className="font-semibold text-lg whitespace-nowrap bg-gradient-to-r from-[#000000] to-[#2d2d2d] bg-clip-text text-transparent">
              Linums
            </span>
          )}
        </div>
        <Button
          variant="ghost"
          size="icon"
          onClick={toggleSidebar}
          className="h-8 w-8 hover:bg-[hsl(var(--sidebar-hover))] shrink-0"
          title={collapsed ? "展开侧边栏" : "折叠侧边栏"}
        >
          {collapsed ? <PanelLeft className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
        </Button>
      </div>

      {/* 中间:动态内容(会话列表等模块) */}
      <div className="flex-1 min-h-0 overflow-hidden">    
          <ScrollArea className="h-full px-3">{children}</ScrollArea>
      </div>

    </aside>
  )
}

1) 侧边栏展开时的体感问题

简略来说,它用一个zutand store的state来控制此时侧边栏的状态,当发生改变时,侧边栏用 width 递增做展开动画。当展开的时候,children中的聊天列表会即时的限时被溢出时的排列情况,因为宽度是递增的,刚开始的时候宽度并没有那么宽,所以这个列表从视觉上看是有一瞬间溢出列表的,虽然很快恢复,但是用户体感会非常不适应。

问题的根因很简单,根据浏览器的渲染原理,像width这种能够改变布局的属性会触发浏览器的reflow,这会让子元素在动画中不断重排(reflow)”导致的瞬时溢出和换行闪一下的问题,解决这个办法的根本方式是去改变渲染时机。

对于这个场景,我们可以避免让列表中children的内容参与关于width动画变化引发的reflow计算,也就是说,我们可以固定内容的width恒定为展开,然后利用translate来做揭开的效果,对于translate这类属性,在浏览器渲染顺序中,既不在布局阶段也不在绘制阶段,而在之后的合成阶段,所以不会引发重排和重绘。

具体的解决办法是:

采用的 transform 方式:

  • 外层仍然可以保留 w-16 <-> w-64 的过渡(负责“窗口变宽”);
  • 内层内容面板固定 w-64,通过 translateX 滑入/滑出(负责“内容揭开”);
  • 折叠时用 invisible + pointer-events-none,避免隐藏内容仍可被点击/聚焦。

解决方案代码如下:

{/* 中间:动态内容(会话列表等模块) */}
<div className="flex-1 min-h-0 overflow-hidden">
  <div
    className={cn(
      'h-full w-64 transition-transform duration-300 will-change-transform',
      collapsed
        ? 'invisible -translate-x-full pointer-events-none'
        : 'visible translate-x-0'
    )}
    aria-hidden={collapsed}
  >
    <ScrollArea className="h-full px-3">{children}</ScrollArea>
  </div>
</div>

在这里用了一个少见的tailwind工具 -translate-x-full

translate-x-full:向右移动自身宽度的 100%(+100%) -translate-x-full:向左移动自身宽度的 100%(-100%)

2) ScrollArea 高度不生效/不滚动

ScrollArea 或其父容器如果没有正确的高度约束,很容易出现:

  • 内容溢出但不滚动;
  • 或滚动区域高度为 0。

经验法则:

  • 侧边栏容器用 flex flex-col h-screen
  • 中间滚动区父级要有 flex-1

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...