PAYATHON 2026

如何检测横向 div 列表是否会溢出,并自动切换为下拉列表?

数据小许

可以用 ResizeObserver 同时观察可用宽度和列表完整展开时的宽度。发生溢出时,隐藏横向列表并显示下拉列表,同时保留横向列表的布局尺寸,以便继续判断何时可以恢复。

浏览器没有专门用于通知元素“即将溢出”的通用 DOM 事件,因此仍需比较尺寸。不过,不必持续轮询,也不必逐个累加 div 的宽度。

为什么只监听父容器不够

原问题通过 requestAnimationFrame 累加 $div.getBoundingClientRect().width,再与父容器宽度比较。这个方法可以工作,但要额外考虑 gap、margin 等占用的空间。如果每帧都执行,还会反复读取布局。

只观察父容器也不够:父容器宽度不变时,项目文字、字体或内容变化仍可能让列表变宽。

可以这样组织结构:

  • 外层 viewport 提供当前可用宽度,裁剪溢出内容。
  • 内层 row 保持单行,使用 width: max-content 保留完整内容所需的宽度。
  • 同时观察 viewport 和 row,任一方尺寸变化时重新判断。

这样可以响应容器宽度和列表自然宽度的变化。

一个完整示例

示例将整个列表切换为原生 select。项目来自静态 HTML,下拉选项在初始化时生成。

<div class="control" id="items-control">
  <div class="viewport">
    <div class="row">
      <div>项目 Alpha</div>
      <div>项目 Beta</div>
      <div>名称较长的项目 Gamma</div>
    </div>
  </div>

  <select class="menu" aria-label="选择项目" hidden></select>
</div>
.control {
  position: relative;
  min-width: 0;
}

.viewport {
  overflow-x: hidden;
}

.row {
  display: flex;
  flex-wrap: nowrap;
  width: max-content;
  gap: 8px;
  white-space: nowrap;
}

.row > div {
  flex: 0 0 auto;
}

.control.is-overflowing .viewport {
  visibility: hidden;
}

.menu {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  width: 100%;
  height: 100%;
}
const control = document.querySelector('#items-control');
const viewport = control.querySelector('.viewport');
const row = control.querySelector('.row');
const menu = control.querySelector('.menu');

for (const [index, item] of Array.from(row.children).entries()) {
  menu.add(new Option(item.textContent.trim(), String(index)));
}

let frameId = null;

function update() {
  frameId = null;

  // 本例 viewport 没有 padding、border,测得宽度即可用于比较。
  const availableWidth = viewport.getBoundingClientRect().width;
  const requiredWidth = row.getBoundingClientRect().width;
  const overflowing = requiredWidth > availableWidth;

  control.classList.toggle('is-overflowing', overflowing);
  viewport.setAttribute('aria-hidden', String(overflowing));
  menu.hidden = !overflowing;
}

function scheduleUpdate() {
  if (frameId !== null) return;
  frameId = requestAnimationFrame(update);
}

const observer = new ResizeObserver(scheduleUpdate);
observer.observe(viewport);
observer.observe(row);
scheduleUpdate();

// 在组件卸载时调用。
function destroy() {
  observer.disconnect();
  if (frameId !== null) {
    cancelAnimationFrame(frameId);
    frameId = null;
  }
}

这里的 requestAnimationFrame 在收到尺寸变化通知时安排一次更新,并合并同一帧内的重复通知,不会持续运行动画循环。

为什么隐藏后仍然可以检测

visibility: hidden 会保留元素的布局尺寸。即使当前显示的是下拉列表,row 仍具有完整展开时的宽度,ResizeObserver 也可以继续观察它。

如果对测量用的 row 设置 display: none,它就不再生成布局盒,无法直接测出完整列表所需的宽度。

示例中的下拉列表使用绝对定位,不参与正常布局;切换时只改变横向列表的可见性。这可以减少展示方式改变尺寸、尺寸变化又触发切换所造成的来回抖动。

父子组件之间的处理也可以放在组件内部:由组件持有 viewport 和 row 的引用,不需要让每个子 div 自行查找应该监听的祖先元素。

能否直接比较 scrollWidth 和 clientWidth

如果内容始终保留在容器中,下面的判断通常更简单:

const overflowing = viewport.scrollWidth > viewport.clientWidth;

overflow-x: hidden 只负责裁剪显示,不会让溢出的内容从布局中消失,因此仍可读取滚动区域的宽度。

不过,scrollWidth 和 clientWidth 是整数值,临界位置可能受取整影响。示例使用 getBoundingClientRect() 比较两个独立盒子的宽度,可以保留小数精度。实际布局若包含 padding、border 或 transform,需要先统一比较口径,不能直接照搬这个宽度比较。

使用时需要注意什么

ResizeObserver 观察的是元素尺寸,不是通用的内容变更通知器。示例通过 width: max-content 让 row 的宽度随内容变化;如果把 row 固定为容器宽度,即使内部溢出增加,它自身的宽度也可能不变。

如果项目会动态增删,应同步更新下拉选项。最好从同一份数据生成两种展示;只有无法控制 DOM 更新来源时,才考虑用 MutationObserver 补充监听。尺寸检测不会自动维护选项数据。

如果需求变成保留能放下的项目,将剩余项目放进“更多”菜单,还需要计算菜单按钮本身占用的宽度,并判断每个项目的边界。当前示例是整组切换,比较列表总宽度与容器可用宽度即可。

备注:内容仅供参考。