Yeti Scroller 布局:用不换行的横向滚动轨道组织照片条、卡片组与步骤胶片
【免费下载链接】yetiA CSS-first, native, zero-build layout and styling framework for web designers.项目地址: https://gitcode.com/gh_mirrors/fo/yeti
导读
Scroller 是 Yeti 中专门负责"横向滚动单行"的布局:当一组同质内容必须保持在同一行、不能换行时,它让容器自身横向滚动,而不是挤压或折断子项。本文从 src/layouts/scroller/docs.md 出发,结合 scroller.css、manifest.json 与浏览器测试,完整讲解它的适用场景、实现原理、data-snap/data-width/data-gap三个数据属性、无障碍要求(tabindex+ 可访问名称),并给出可直接复制的示例与测试验证。读完你可以独立在 Yeti 项目中落地一个可键盘访问、可吸附对齐的横向滚动条。
何时使用 Scroller:一行必须是"一行"的内容
Scroller 解决的是一类特定的布局需求:一行内容应该始终是一行。典型场景包括:
- 一溜照片(photo strip);
- 一组相关的卡片(related cards);
- 一组步骤的"胶片条"(filmstrip of steps);
- 横向溢出的大元素,例如一张宽表格或一张超宽图片。
关键判断标准是与 cluster 的对比:cluster 是可换行的 flex 行,空间不足时子项会折行;而 scroller 恰恰相反——空间不足时子项保持原宽,容器开启横向滚动,让访客沿着这一行移动查看。因此,凡是"内容多到塞不下也不许换行"的横向条带,就是 scroller 的领地。
从 docs/guides/layouts.md 的 Foundation 6 对照表还能看到它的历史定位:Foundation 6 时代的 Orbit 轮播组件,在 Yeti 中就由scroller承担——一个不依赖 JavaScript 的原生横向滚动布局。
工作原理:不换行的 flex 行 + overflow-x
Scroller 的全部实现只有一份零依赖的 CSS,位于 src/layouts/scroller/scroller.css:
@layer yeti.layouts { .scroller { display: flex; gap: var(--_yeti-gap); overflow-x: auto; overscroll-behavior-x: contain; } .scroller:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); } .scroller > * { margin: 0; flex: 0 0 auto; } .scroller[data-width] > * { inline-size: var(--_yeti-width); } .scroller[data-snap] { scroll-snap-type: x mandatory; } .scroller[data-snap] > * { scroll-snap-align: start; } }逐行拆解其机制:
display: flex+ 不换行:flex 容器默认不换行,这是"保持一行"的基础。- 子项
flex: 0 0 auto:这是核心——子项既不收缩也不伸展,保留自然宽度。当整行宽度超过容器时,行溢出,容器因此可以滚动。这正是文档中"Children are told not to shrink, so the row overflows and the container scrolls"的源码对应。 overflow-x: auto:只在横向溢出时出现滚动条,纵向不干预。overscroll-behavior-x: contain:当滚动到轨道尽头时,把滚动"关在"轨道内部,避免连锁滚动触发外层页面(或相邻滚动区)的滚动,这是横向嵌套滚动场景中容易被忽略的细节。- 子项
margin: 0:间距完全交给gap决定,子项不携带任何外边距——这符合 Yeti 的总体原则:间距由持有子项的布局负责,测试中也有expectNoChildMargins专门校验这一点。
浏览器测试 test/browser/layouts/scroller.spec.js 精确验证了这套机制:在 400px 视口下断言scrollWidth > clientWidth(即确实溢出并可滚动),同时断言相邻子项top对齐、水平间距等于--yeti-space-md。
支持性方面,manifest.json 的
support字段标注了三个"无降级风险"的依赖:flexbox gap、scroll snap、overscroll-behavior,并且js: null——整个布局不需要任何脚本。
三个数据属性:data-gap />[data-gap="xs"] { --_yeti-gap: var(--yeti-space-xs); } [data-gap="lg"] { --_yeti-gap: var(--yeti-space-lg); }
值得注意的两点:
- 默认值用
:not([data-gap])选择器提供(.scroller:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); })。这是 Yeti 的既定惯例:只有属性缺席时布局才提供默认,因此嵌套布局永远不会继承父级的间距值。 - 支持流体间距对:
data-gap="sm-lg"不会在sm与lg之间跳变,而是从窄视口下的sm端点平滑流动到宽视口下的lg端点,与字体尺度本身的流式逻辑一致(见 src/tokens/scale.css 的插值机制)。
data-snap:滚动吸附
data-snap开启两条规则:
.scroller[data-snap] { scroll-snap-type: x mandatory; } .scroller[data-snap] > * { scroll-snap-align: start; }scroll-snap-type: x mandatory让横向滚动强制吸附,scroll-snap-align: start让每个子项的起始边缘成为吸附点,滚动停下时子项会"干净地"对齐到轨道起始边,不会卡在两张卡片中间。对应测试断言:未加data-snap的轨道scroll-snap-type为none,加了之后以x开头(scroll-snap-type: x mandatory)。
data-width:等宽子项
data-width让轨道从"自然宽度各异"变为"每格等宽"的胶片感序列:
.scroller[data-width] > * { inline-size: var(--_yeti-width); }取值同样是令牌后缀:data-width="xs"读取--yeti-width-xs(16rem),sm为 24rem、md为 32rem、lg为 48rem、xl为 64rem、2xl为 80rem(见 space.css 的宽度令牌定义)。测试对此的断言是:加了data-width="xs"后,子项宽度精确等于--yeti-width-xs的令牌值。
直接可用的最小示例
来自组件自带示例 src/layouts/scroller/example.html:
<div class="scroller"><div class="scroller" contenteditable="false">【免费下载链接】yetiA CSS-first, native, zero-build layout and styling framework for web designers.
项目地址: https://gitcode.com/gh_mirrors/fo/yeti创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考