返回博客

卡片最后一行「飘」到边框外——外层 min-height 与嵌套 grid button 的 Chromium 布局陷阱

侧边栏卡片列表,每张卡片底部都有一行「更新于 MM/DD HH:MM」的时间戳。某天用户反馈:这行文字漂浮在卡片边框下方的空白区域里,像是被甩出去了。

打开 F12,DOM 结构分明正确——时间戳 <span> 稳稳地在 <button> 里面。React 渲染没有问题。但视觉上,卡片的背景色和边框止步于徽章行,时间戳孤零零地悬在下一张卡片的上方。

现象

  • 卡片:标题行 + 徽章行 + 时间戳行,共三层子元素。
  • 标题和徽章显示在卡片边框内;时间戳行渲染在边框外面,飘在卡片间隙。
  • F12 检查计算高度:外层 .record-itemheight 约等于 min-height: 76px,远小于三行内容的自然高度(约 90–100px)。
  • 把外层的 min-height: 76px 在 DevTools 里临时删掉,时间戳立刻回到卡片内部。

根因

CSS 结构如下:

/* 外层卡片容器 */
.record-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  min-height: 76px;          /* ← 触发点 */
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.55);
  overflow: visible;
}
 
/* 内层可点击区域:唯一的 grid 子项,是 <button> */
.card-main {
  display: grid;
  min-height: 86px;
  /* 三个 block 子元素:title / badge / meta */
}

Chromium 在处理「外层 grid 容器 + 内部唯一子项是 <button> 且该 button 本身也是 grid」这一嵌套路径时,存在一个内禀尺寸计算的怪异行为:外层容器的 min-height 作为尺寸提示被传播进内层 <button> 的自布局过程,错误地封顶了 button 的渲染盒高度——尽管 button 本身既没有 height 也没有 max-height

button 的渲染盒被限制在约 76px,但它的三个子元素自然高度叠加超过了这个值。CSS 没有裁剪(overflow: visible),最后一个子元素就被绘制到了 button 渲染盒之外——而 backgroundborder 挂在外层的 .record-item 上,那里已经没有背景覆盖,时间戳因此「飘」了出去。

坑:外层 grid 的 min-height 会悄悄截断内层 button 的高度

当结构是「外层 grid 容器 → 唯一子项是自身也是 grid 的 <button>」时,外层的 min-height 在 Chromium 内禀尺寸路径中会错误地成为内层 button 的渲染高度上限,导致 button 最后一个子元素溢出到 button 渲染盒之外。

<div role="button"><a> 在相同结构下不会复现这个 bug——这是 <button> 元素特有的 Chromium intrinsic-sizing 路径问题。

排查要点

在 F12 里做三步快速确认:

  1. 选中外层 .record-item,查看 computed height:若接近 min-height 值而远小于内容自然高度,基本命中。
  2. 临时在 Styles 面板删除外层 min-height:时间戳行回位则确认是这个 bug。
  3. 查看 DOM:时间戳节点是否确实在 <button> 内部(而非被浏览器 HTML 解析提升到 button 之外)——若 DOM 深度正确,排除「block 元素被提升」的同类假阳性。

正解

删除外层 grid 容器的 min-height,将高度保底逻辑移到内层 button

/* AFTER — 修复后 */
.record-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  /* min-height 从这里移走 */
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.55);
  overflow: visible;
}
 
.card-main {
  display: grid;
  min-height: 86px;   /* 高度保底留在这里,空状态卡片不塌陷 */
}

对应的 diff 极简:

 .record-item {
   display: grid;
   grid-template-columns: minmax(0, 1fr);
   align-items: start;
-  min-height: 76px;
   border: 1px solid var(--line);
   background: rgba(255, 255, 255, 0.55);
   overflow: visible;
 }
正解:min-height 留在内层 button,外层 grid 容器不加

外层 grid 容器自身无高度约束,会跟随唯一子项(button)的实际高度撑开;button 的 min-height 保证空状态不塌陷。卡片背景和边框随着 grid 容器撑开,完整覆盖所有子行。

别用 overflow: hidden 掩盖

给内层 button 加 overflow: hidden 看起来会「消除」浮出的时间戳,但实际上是把它裁掉了——内容一多(多行标题)问题立刻复现,且更难排查。治根不治标。

一句话外卖

外层 grid 容器的 min-height 是写给容器自己的,不要让它替代内层 button 做高度保底——否则 Chromium 会用它来意外封顶 button 的渲染盒,最后一行子元素就飞出去了。