PAYATHON 2026

响应式 checkout 流程应使用 tables 还是 divs?

支付小周

明确结论

不要把整个 checkout 流程都做成 table,也不要为了布局方便,把所有内容都换成没有语义的 div。

可以按下面的原则选择元素:

  • 如果订单明细具有稳定的行列关系,用户需要按列比较价格、数量和小计,就使用 <table>。
  • 如果商品项在移动端需要重新排成卡片,其中还有颜色选择、数量输入、删除等复杂控件,优先使用 <ul>、<li>、article、section 等语义元素,并配合 CSS Grid 布局。
  • 收货地址、配送方式和支付信息属于表单,应使用 <form>、<fieldset>、<legend>、<label> 等元素。
  • <div> 可以用于布局,但不能代替按钮、表单、列表或表格本身的语义。

判断标准不是简单地在 table 和 div 之间二选一,而是看内容是否具有真正的二维表格关系,以及移动端是否需要调整信息顺序。

为什么 table 在这类页面中容易失控

HTML table 适合表达行列数据。浏览器和辅助技术可以识别表头与数据单元格之间的关系,在桌面端也方便用户快速比较商品价格和小计。

但 checkout 中的商品行通常不只是数据,还会包含:

  • 商品图片和名称
  • SKU、规格、颜色等属性
  • 数量输入框
  • 库存或配送提示
  • 删除、收藏等操作
  • 单价、优惠和小计

这些内容在桌面端可以排成一行,到了移动端,往往需要重新组合图片、商品信息、数量和操作按钮。table 的布局模型可以调整列宽或支持横向滚动,却不适合随意改变单元格的顺序。

如果为了适配移动端,强行把 <tr>、<td> 设置为 display: block,视觉上虽然可能变成卡片,但表格原有的结构和可访问性会更难理解。不同浏览器和辅助技术对此类处理的表现也可能不同。

推荐的页面结构

checkout 可以按功能拆分,不必让整个页面只使用一种元素:

  1. 商品编辑区域使用语义列表或 CSS Grid。
  2. 订单金额汇总使用 <dl>,因为这里表达的是名称与数值之间的对应关系。
  3. 收货地址、支付方式等区域使用标准表单结构。
  4. 只读且需要按列比较的订单明细可以使用 <table>。
  5. 布局和响应式变化交给 CSS,不要用 table 模拟页面网格。

这样拆分,通常比维护多套略有差异的 table 组件更稳定。

适合复杂商品操作的实现

下面的结构把每件商品作为一个独立条目。桌面端显示为规则网格,移动端可以直接变成卡片,不需要复制两套 HTML。

<ul class="cart-items">
  <li class="cart-item">
    <a class="cart-item__product" href="/products/linen-shirt">
      <img
        class="cart-item__image"
        src="/images/linen-shirt.jpg"
        alt=""
        width="96"
        height="96"
      >

      <span class="cart-item__details">
        <strong>Linen Shirt</strong>
        <span>Color: Navy</span>
        <span>Size: M</span>
      </span>
    </a>

    <div class="cart-item__price">
      <span class="field-label">单价</span>
      <span>¥299.00</span>
    </div>

    <div class="cart-item__quantity">
      <label for="quantity-product-123">数量</label>
      <input
        id="quantity-product-123"
        name="items[123][quantity]"
        type="number"
        min="1"
        max="10"
        value="1"
        inputmode="numeric"
      >
    </div>

    <div class="cart-item__subtotal">
      <span class="field-label">小计</span>
      <strong>¥299.00</strong>
    </div>

    <button
      class="cart-item__remove"
      type="button"
      aria-label="从购物车中删除 Linen Shirt"
    >
      删除
    </button>
  </li>
</ul>
.cart-items {
  margin: 0;
  padding: 0;
  list-style: none;
}

.cart-item {
  display: grid;
  grid-template-columns:
    minmax(16rem, 1fr)
    minmax(6rem, auto)
    minmax(7rem, auto)
    minmax(7rem, auto)
    auto;
  grid-template-areas: "product price quantity subtotal remove";
  gap: 1rem;
  align-items: center;
  padding: 1rem 0;
  border-bottom: 1px solid #d9d9d9;
}

.cart-item__product {
  grid-area: product;
  display: flex;
  gap: 1rem;
  color: inherit;
  text-decoration: none;
}

.cart-item__image {
  flex: 0 0 auto;
  object-fit: cover;
}

.cart-item__details {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.cart-item__price {
  grid-area: price;
}

.cart-item__quantity {
  grid-area: quantity;
}

.cart-item__quantity label,
.field-label {
  display: block;
  margin-bottom: 0.25rem;
  font-size: 0.875rem;
  color: #555;
}

.cart-item__quantity input {
  width: 5rem;
}

.cart-item__subtotal {
  grid-area: subtotal;
}

.cart-item__remove {
  grid-area: remove;
}

@media (max-width: 767.98px) {
  .cart-item {
    grid-template-columns: 6rem minmax(0, 1fr);
    grid-template-areas:
      "product product"
      "price subtotal"
      "quantity quantity"
      "remove remove";
    align-items: start;
  }

  .cart-item__product {
    min-width: 0;
  }

  .cart-item__remove {
    justify-self: start;
    min-height: 44px;
  }
}

这里用 class 管理样式,DOM 的阅读顺序仍然合理。CSS Grid 只负责视觉排列,所以即使样式没有加载,用户依然能够读取内容和使用表单。

在实际项目中,还要根据业务规则决定数量变化后如何更新:立即更新、输入框失去焦点后更新,或者点击“更新购物车”后统一提交。不能只依赖 JavaScript 更新界面,服务端仍需验证数量、库存、价格和可购买状态。

金额汇总不需要 table

订单金额通常是标签与数值的对应关系,用 <dl> 更合适:

<dl class="order-summary">
  <div>
    <dt>商品小计</dt>
    <dd>¥598.00</dd>
  </div>

  <div>
    <dt>配送费</dt>
    <dd>¥20.00</dd>
  </div>

  <div>
    <dt>优惠</dt>
    <dd>−¥50.00</dd>
  </div>

  <div class="order-summary__total">
    <dt>应付总额</dt>
    <dd>¥568.00</dd>
  </div>
</dl>
.order-summary > div {
  display: flex;
  justify-content: space-between;
  gap: 2rem;
  padding: 0.5rem 0;
}

.order-summary dd {
  margin: 0;
  text-align: end;
  white-space: nowrap;
}

.order-summary__total {
  margin-top: 0.5rem;
  border-top: 1px solid #999;
  font-weight: 700;
}

仍然应该使用 table 的情况

如果页面展示的是只读订单明细,而且用户确实需要比较各列,table 仍然是合适的选择。移动端可以先使用横向滚动,不必破坏表格结构。

<div class="table-scroll" tabindex="0" aria-label="订单商品明细,可横向滚动">
  <table class="order-table">
    <caption>订单商品明细</caption>
    <thead>
      <tr>
        <th scope="col">商品</th>
        <th scope="col">单价</th>
        <th scope="col">数量</th>
        <th scope="col">小计</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Linen Shirt,Navy,M</th>
        <td>¥299.00</td>
        <td>2</td>
        <td>¥598.00</td>
      </tr>
    </tbody>
  </table>
</div>
.table-scroll {
  max-width: 100%;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
}

.order-table {
  width: 100%;
  min-width: 40rem;
  border-collapse: collapse;
}

.order-table th,
.order-table td {
  padding: 0.75rem;
  text-align: start;
  border-bottom: 1px solid #ddd;
}

.order-table td:last-child {
  white-space: nowrap;
}

横向滚动并不适合所有场景,但对于列关系重要、主要用于读取的表格,它通常比强制把每个单元格改成块级元素更可靠。

表单区域应保持标准语义

地址或支付表单不要用 table 对齐标签。应使用 <fieldset> 和 <label>,再通过 Grid 控制布局:

<fieldset class="address-fields">
  <legend>收货地址</legend>

  <div class="form-field">
    <label for="full-name">姓名</label>
    <input id="full-name" name="fullName" autocomplete="name" required>
  </div>

  <div class="form-field">
    <label for="postal-code">邮政编码</label>
    <input
      id="postal-code"
      name="postalCode"
      autocomplete="postal-code"
      inputmode="numeric"
      required
    >
  </div>

  <div class="form-field form-field--wide">
    <label for="street-address">详细地址</label>
    <input
      id="street-address"
      name="streetAddress"
      autocomplete="street-address"
      required
    >
  </div>
</fieldset>
.address-fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
}

.address-fields legend,
.form-field--wide {
  grid-column: 1 / -1;
}

.form-field label,
.form-field input {
  display: block;
  width: 100%;
}

@media (max-width: 575.98px) {
  .address-fields {
    grid-template-columns: 1fr;
  }
}

实施时需要注意

  • 不要分别维护桌面端 table 和移动端卡片。重复的 DOM 容易造成字段不同步、id 重复、焦点重复,以及辅助技术重复朗读等问题。
  • 如果商品图片只是装饰,使用 alt=""。如果图片承担商品识别功能,则提供简洁、且不重复商品名称的替代文本。
  • 每个输入控件都应关联 <label>,不能只依赖 placeholder。
  • 删除商品应使用 <button type="button"> 或明确的提交按钮,不要给 <div> 绑定点击事件来代替按钮。
  • 不能只用颜色区分规格,还要显示颜色名称或其他文字说明。
  • 删除、价格变化和库存不足等动态结果应提供清楚的反馈。必要时,可以使用经过测试的 aria-live 区域。
  • 移动端需要保证合理的点击区域,并检查键盘操作、焦点顺序、错误提示和屏幕阅读器输出。
  • Bootstrap 可以处理断点、间距和基础表单样式,但 HTML 结构仍应根据内容语义确定。
  • 不要只按设备名称设置断点。应在内容开始拥挤、控件难以操作的位置切换布局。

如果购物车条目包含大量可编辑控件,而且移动端需要明显调整排列顺序,语义列表配合 CSS Grid 通常更容易维护。真正需要按行列比较的只读数据则继续使用 table。混合使用这些结构,可以更好地兼顾语义、可维护性和易用性。

备注:内容仅供参考。