Cadenza
EN

覆盖式滚动条的滚动容器 —— 滚动条渲染在视口之外,与 scroll-fade 渐隐天然兼容

基于 Base UI 的滚动容器:真正滚动的是视口,滚动条渲染成视口的 兄弟节点(覆盖式,不占布局宽度)。这个结构是它存在的理由 —— 库里的 scroll-fade 渐隐是挂在滚动元素上的 mask,原生滚动条长在滚动元素 内部会被 mask 一起压暗,兄弟滚动条则始终清晰。

库内的 DataTableInfiniteSelect 以及本站 demo 卡片的代码区,滚动都走同一个 ScrollArea。

使用

import { ScrollArea } from '@gedatou/cadenza-ui'
<ScrollArea className="rounded-xl border block-64 inline-72">
  {/* 会溢出的内容 */}
</ScrollArea>

给根元素定尺寸即可,视口自动撑满。默认纵向、滚动条 hover 显示 —— 两件事各有一个 prop:方向显隐

组成

一个 ScrollArea 展开是四个部件,只有滚动条另外对外导出:

ScrollArea                 ← 根:尺寸约束定在这里,它自己不滚动
├── viewport               ← 真正滚动的元素(viewportClassName / viewportRef 指的就是它)
├── ScrollAreaScrollbar    ← 视口的兄弟,覆盖式;按 orientation 渲染 1–2 条
│   └── thumb
└── corner                 ← 两条滚动条交汇处的小方块

viewport / thumb / corner 由封装层按 orientationscrollbars 自动渲染, 外部通过 viewportClassNamedata-slot 触达。

单独导出的 ScrollAreaScrollbar 供自定义组合场景使用,一般用不到 —— 名字随 家族(Base UI 的部件就叫 ScrollArea.Scrollbar),不是 shadcn 的裸名 ScrollBar。 它自己不带悬停淡出:那身类名是 ScrollAreascrollbars 传进去的。

方向

orientation 决定渲染哪些滚动条:vertical(默认)只出纵向、horizontal 只出横向、both 两条都要。横向滚动时内容要用 inline-max 之类按内容撑开:

滚动条显隐

scrollbars 三档:always 常显、hover(默认)悬停或滚动进行中显示、 hidden 不渲染:

无溢出时 Base UI 会直接把滚动条从 DOM 卸载(keepMounted 默认 false), 不用自己判断。hover 档不靠卸载,而是把滚动条压成 opacity-0,由 data-hovering / data-scrolling 提回不透明 —— 透明的条也不会挡住点击, 因为 data-hovering 只在指针进到区域内时存在,那正是它不透明的时候。

滚动渐隐

把 scroll-fade 工具类挂到视口上(viewportClassName,不要挂根元素):

scroll-fade-y 纵向、scroll-fade-x 横向、scroll-fade-inset 双轴并支持 固定表头 / 固定列偏移。渐隐随滚动位置显隐 —— 滚到头对应边的 fade 自动消失, 这一步靠 animation-timeline: scroll(self)只有元素自己在滚动时才有进度: 挂到根元素上既拿不到进度,又会把兄弟滚动条一起压暗,等于退回原生滚动条的处境。 不支持 scroll-driven animation 的浏览器退化成两端常显渐隐。

尺寸用 --scroll-fade-size(或 scroll-fade-8 这类工具类)调, 详见 @gedatou/cadenza-ui/styles.css 里的 scroll-fade 一族。

状态与 className

className 一律双形态:字符串,或 (state) => string 的函数。下表左列是挂在 DOM 上的 data-*,Tailwind 直接当变体写(data-scrolling:opacity-100);右列是同一个状态在 函数 className 里的名字。

部件data-*出现时机函数 className 里的名字
根 / 视口 / 滚动条data-scrolling滚动进行中(停手 500ms 后消失)scrolling
根 / 视口 / 滚动条data-has-overflow-x / -y该轴内容超出视口hasOverflowX / hasOverflowY
根 / 视口 / 滚动条data-overflow-x-start / -x-end / -y-start / -y-end对应那条边还有没滚到的内容overflowXStart / overflowXEnd / overflowYStart / overflowYEnd
滚动条 / 拇指data-orientation始终有值:vertical / horizontalorientation(只在滚动条的 state 里)
滚动条data-hovering指针在整个滚动区域内hovering(同样只在滚动条的 state 里)
data-orientation始终有值:vertical / horizontal / both。封装层回显 orientation 的配置,不是 Base UI 的状态

className(根)与 viewportClassName 收到的是完全同一组字段——视口的 state 就是 根的 state(ScrollAreaViewportState extends ScrollAreaRootState),两边都含 cornerHidden(角落方块是否隐藏,没有对应的 data-*)。hoveringorientation 只在滚动条元素上:想按悬停改根元素的样子,用 hover: 伪类。

需要从外部定位时用 data-slot

data-slot是什么
scroll-area根元素
scroll-area-viewport真正滚动的视口
scroll-area-scrollbar单条滚动条
scroll-area-thumb滚动条里的拇指
scroll-area-corner两条滚动条交汇处

键盘交互

按键行为
Tab内容有溢出时视口进入 Tab 序(tabIndex=0,无溢出时是 -1),落上去有一圈 focus ring
/ / / 逐行 / 逐列滚动
PageUp / PageDown整屏翻动
Home / End滚到顶 / 底

滚动本身是浏览器给可聚焦滚动容器的原生行为。这里只有两件事是组件做的: Base UI 让有溢出的视口可聚焦(否则纯键盘用户到不了滚动区域),封装层给它 focus-visible 的 ring。

Props

顺序规则:必填 → 非受控默认值 → 受控值 → 回调 → 行为开关 → 外观 → className

Prop类型默认值说明
orientation'vertical' | 'horizontal' | 'both''vertical'渲染哪些滚动条,见方向
scrollbars'always' | 'hover' | 'hidden''hover'滚动条显隐策略(导出类型 ScrollAreaScrollbars),见滚动条显隐
viewportRefRef<HTMLDivElement>视口引用(滚动位置、虚拟化都要它)
viewportClassNamestring | (state) => string视口类名;scroll-fade 挂这里。函数形式收到视口状态(scrollinghasOverflowX/Y 与四个溢出边缘标志——正是 scroll-fade 要读的那些)
viewportStyleCSSProperties | (state) => CSSProperties视口内联样式(如 maxHeight),同样支持函数形式
classNamestring | (state) => string根元素类名;尺寸约束定在这里。函数形式收到 Base UI Root 的状态(scrollinghasOverflowX/YoverflowXStart 等溢出方向、cornerHidden——hovering 不在其中,它只挂在滚动条元素的 data-hovering 上)
其余Base UI ScrollArea.Root 的 props(元素原生属性 + ref透传

三个类型一并导出:ScrollAreaProps(根组件完整 props:Base UI Root 的 props + 上表的 seam 扩展)/ ScrollAreaScrollbarProps(滚动条部件的 props,即 ScrollArea.Scrollbar.Props)/ ScrollAreaScrollbarsscrollbars 的取值联合)。