侧边栏展开逻辑设计思路总结
最近,在为 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;