滑动即可显示

通过滑动显示模式,用户可以水平滑动前景行或内容窗格,以显示隐藏的界面(例如关联操作按钮 [置顶、忽略、归档、删除] 或次级侧边菜单),而不会自动激活任何操作。容器不会在滑动期间执行操作,而是会弹开以显示互动控件,用户可以检查并激活这些控件,也可以向后滑动以关闭这些控件。

如需了解在超过阈值后立即提交并关闭项的全宽滑动手势,请参阅滑动移除。如需在垂直滚动容器顶部显示隐藏的标题,请参阅下拉显示。

如何实现

无论是显示列表项上的操作按钮,还是显示可滚动窗格旁边的侧边菜单,其机制都是相同的:将前景内容和隐藏面板封装在一个外部水平滚动容器中,该容器配置了 CSS 滚动贴靠 (scroll-snap-type: x mandatory) 和 overscroll-behavior: chain。浏览器的原生滚动引擎可处理惯性、贴靠、键盘焦点滚动和滚动链,而无需 JavaScript 手势监听器。

第 1 步:标记滑动封装容器、内容和显示的面板

在 .SwipeReveal 滚动容器中,先放置前景内容,再放置显示的控件(操作按钮或侧边菜单)。由于内容在 DOM 顺序中排在第一位,因此滚动容器自然会从滚动偏移量 0 开始,面板会隐藏在屏幕右侧。

<!-- Example A: List item with revealable action buttons -->
<div class="SwipeReveal">
  <div class="SwipeReveal-content">
    <strong>Design review notes</strong>
    <p>Updated component specs for the Q3 release.</p>
  </div>
  <div class="SwipeReveal-panel" role="group" aria-label="Item actions">
    <button type="button">Pin</button>
    <button type="button">Archive</button>
    <button type="button">Delete</button>
  </div>
</div>

<!-- Example B: Scrollable content pane with a revealable side menu -->
<div class="SwipeReveal" role="region" aria-label="Document preview with swipeable side menu">
  <div class="SwipeReveal-content is-scrollable" tabindex="0">
    <h2>Article Preview</h2>
    <p>Scroll vertically through this pane, or swipe left to reveal the side menu...</p>
  </div>
  <nav class="SwipeReveal-panel" aria-label="Contextual menu">
    <a href="#share">Share</a>
    <a href="#bookmark">Bookmark</a>
    <a href="#export">Export PDF</a>
  </nav>
</div>

第 2 步:配置水平贴靠容器和 overscroll-behavior: chain

如果您将某个元素封装在外部滚动容器中,只是为了创建滑动显示效果,那么该外部封装容器就会成为文档(或其他父级滚动容器)内的中间滚动容器。

overscroll-behavior 的四个关键字可独立控制多余的滚动是否会传播到祖先滚动容器(滚动链接),以及容器是否显示本地滚动回弹功能,例如弹性橡皮筋拉伸或边缘发光(本地边界效果):

值 滚动链(传播到祖先元素) 局部边界效果(橡皮筋/发光) 在“滑动即可查看”广告中的作用
auto 是 是 默认行为;在需要正常回弹反馈的内部内容滚动条上保留此行为,但在外部滑动封装容器上避免此行为,因为这会导致菜单回弹
chain 是 否 在外部 .SwipeReveal 滚动条上使用,以便多余的滚动操作链接到父级滚动条(例如文档),而不会导致菜单或操作条弹跳
contain 否 是 在容器内捕获滚动,同时仍在本地回弹
none 否 否 同时阻止本地回弹和滚动链接到祖先元素

将 .SwipeReveal 设为两列网格 (grid-template-columns: 100% max-content),其中 .SwipeReveal-content 贴靠到 start,.SwipeReveal-panel 贴靠到 end:

.SwipeReveal {
  /* Column 1: full-width content. Column 2: auto-sized actions or side menu. */
  display: grid;
  grid-template-columns: 100% max-content;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;

  /* Prevent the swipe wrapper and revealed panel from bouncing locally while
     still chaining excess scroll into the parent scroller (the document). */
  overscroll-behavior: chain;
}

@media (prefers-reduced-motion: no-preference) {
  .SwipeReveal {
    scroll-behavior: smooth;
  }
}

.SwipeReveal-content {
  scroll-snap-align: start;
  scroll-initial-target: nearest;
  background: Canvas;
}

.SwipeReveal-content:focus-visible {
  outline: auto;
  outline-offset: -2px;
}

.SwipeReveal-content.is-scrollable {
  block-size: 20rem;
  overflow: auto;
  /* Keeps default `overscroll-behavior: auto` so the content pane retains
     its normal bounce effect while chaining horizontal swipes to `.SwipeReveal`. */
}

.SwipeReveal-panel {
  /* Snapping a trailing panel to `end` (or a leading panel to `start`) brings
     all controls fully into view while keeping the adjacent edge of the content visible. */
  scroll-snap-align: end;
  display: flex;

  &:first-child {
    scroll-snap-align: start;
  }
}

如果您还想使用三列 (grid-template-columns: max-content 100% max-content) 或两列 (max-content 100%) 在左侧 (inline-start) 显示控件,请将前导 .SwipeReveal-panel 放在 .SwipeReveal-content 之前。由于 .SwipeReveal-content 具有 scroll-initial-target: nearest,因此滚动容器最初位于内容列(而非前导面板)上。

第 3 步:在键盘焦点上将可聚焦的内容窗格重新贴靠到视图中

由于 .SwipeReveal-panel 内的按钮或链接在静止状态下位于屏幕外,因此使用键盘通过 Tab 键进入这些按钮或链接会自动滚动 .SwipeReveal 以显示获得焦点的控件。不过,由于 .SwipeReveal-content 跨越了外部容器宽度的 100%,因此即使侧边栏处于打开状态,它的大部分内容仍会留在滚动窗口内。因此,按 Shift+Tab 将焦点移回 .SwipeReveal-content(或其内部的可聚焦元素)不会自动将轨道滚动回 0。

监听 .SwipeReveal-content 上的 focusin 并检查 event.target.matches(':focus-visible'),以便窗格或任何后代上的键盘焦点贴靠 .SwipeReveal-content 回到开头,而不会干扰指针点击或触摸拖动:

for (const content of document.querySelectorAll('.SwipeReveal-content')) {
  content.addEventListener('focusin', (event) => {
    if (event.target.matches(':focus-visible')) {
      content.scrollIntoView({
        block: 'nearest',
        inline: 'start',
      });
    }
  });
}

最佳实践和陷阱

  • 务必使用 scroll-snap-type: x mandatory,而不是 proximity。使用 proximity 时,轻轻滑动可能会导致操作按钮或侧边菜单在静止状态下部分露出。
  • 务必在外层水平滑动显示滚动条 (.SwipeReveal) 上设置 overscroll-behavior: chain,以便滚动操作可以链接到父级滚动条,而不会导致显示的菜单或操作条发生弹跳。
  • 务必在内部可滚动内容窗格 (.SwipeReveal-content.is-scrollable) 上保留 overscroll-behavior: auto(默认值),以便在这些窗格上提供正常的回弹反馈,同时仍将水平滑动链接到外部滑动滚动条。
  • 务必将显示的操作和侧边菜单项实现为可聚焦的真实 DOM 控件(<button>、<a>)。当 .SwipeReveal-content 可聚焦或包含可聚焦控件时,请将 focusin 监听器与 event.target.matches(':focus-visible') 配对以调用 scrollIntoView({ block: 'nearest', inline: 'start' }),这样,当用户通过 Tab 键返回内容窗格时,侧边栏会关闭,而不会影响指针互动。
  • 请勿在 JavaScript 中拦截 wheel、touchmove 或 pointermove 事件,以手动转换内容或模拟滚动链。主线程手势拦截会阻止合成器驱动的滚动,破坏原生滚动惯性,并降低响应速度。

回退

当 .SwipeReveal 容器将 .SwipeReveal-panel 放置在 .SwipeReveal-content 左侧 (inline-start) 时,不支持 scroll-initial-target: nearest 的浏览器最初会在滚动偏移量 0 处呈现,并显示前导面板。

如果您的基准目标不支持 scroll-initial-target,请使用 CSS.supports('scroll-initial-target', 'nearest') 检测支持情况,并在 load 上使用 behavior: 'instant' 调用 scrollTo(),以便具有前导面板的容器直接跳转到 .SwipeReveal-content,而无需滚动页面:

采用渐进增强的方式

将 overscroll-behavior: chain 和 scrollbar-width: none 视为渐进增强,无需 JavaScript 后备:

  • 自动 CSS 级联回退:overscroll-behavior 的初始值为 auto。尚无法识别 chain 关键字的浏览器会在解析时忽略 overscroll-behavior: chain,并保留 auto。由于 auto 已允许滚动链接到祖先滚动容器,因此滑动显示手势和滚动链接到父级容器都可以在所有浏览器中开箱即用;支持的浏览器只需抑制外部滑动封装容器上不必要的橡皮筋回弹。
  • 替换 contain 或 none:如果基础样式表或实用程序类设置了 overscroll-behavior: contain 或 none,请在 overscroll-behavior: chain 之前立即声明 overscroll-behavior: auto,以便尚不支持 chain 的浏览器仍将滚动链接到父级,而不是将其捕获。
  • 隐藏滚动条 (scrollbar-width: none):隐藏水平滚动条轨道纯粹是为了美观;如果旧版引擎忽略 scrollbar-width: none,滑动显示容器仍可使用标准滚动条正常运行。