空状态必须实现dom、语义、样式、js状态四者对齐;应复用真实内容容器的标签与class,避免孤立div;优先用v-if或条件渲染控制存在性,慎用:empty;文案与按钮需结构化嵌入并确保可访问性。

空状态页面不能靠视觉提示“凑合用”,必须让 DOM、语义、样式、JS 状态四者对齐,否则屏幕阅读器不读、键盘用户跳过、页面加载抖动、SSR 与客户端状态错乱。
空状态该用 <div> 还是 <code><section></section>?
看它是否构成一个逻辑区块。比如整个“订单列表区域”或“搜索结果区”,用 <section></section> 更合适;但如果只是表格下方一行提示,用 <div> 更轻量、更可控。滥用 <code><section></section> 会让屏幕阅读器误判结构层级,反而降低可访问性。
关键判断点:
- 这个空提示是否和它上方的标题、筛选栏、操作按钮属于同一功能上下文?是 →
<section></section> - 它是否独立存在、不参与任何语义化导航流(如没有 heading 关联)?是 →
<div> <li>真实内容容器是 <code><ul class="list"></ul>,那空状态也得是<ul class="list" aria-live="polite">暂无订单</ul>,不是另起一套结构 - 真实列表是
<ul class="order-list"></ul>→ 空状态也用<ul class="order-list" aria-live="polite" aria-label="订单列表为空">暂无订单记录</ul> - 若真实结构是卡片网格
<div class="card-grid">,空状态也得是 <code><div class="card-grid" aria-live="polite">暂无内容</div> - 所有 padding/margin/border 都来自
.card-grid或.order-list,不另设.empty类控制布局 - Vue 中优先用
v-if="list.length"控制整个<ul></ul>存在性 - React 中写成
{list.length === 0 ? <nodata></nodata> : list.map(...)},别用list?.map(...) || <nodata></nodata> - 纯 JS 渲染前先检查
data.length === 0,再决定插入列表 HTML 还是空提示 HTML - 真要用
:empty,构建时必须压缩 HTML(移除空白),或手动写成<ul></ul>紧凑格式
为什么不能直接写 <div class="empty">暂无数据</div>?
它在 DOM 层面就脱离了原本的内容语义链。屏幕阅读器看到的是一个孤立的 <div>,无法感知“这里本该是订单列表”。更严重的是,在 flex/grid 布局中,<code><div class="empty"> 和真实 <code><ul class="list"></ul> 的盒模型往往不一致,导致高度突变、列错位、响应式断层。
正确做法是复用真实容器的 class 和语义标签:
:empty 伪类为啥经常不生效?
浏览器把换行符、制表符、空格都算作文本节点,<ul>\n</ul> 在 DOM 中不是空的。模板引擎(EJS/Twig)、Vue/React 的插值语法,甚至手写 HTML 里的回车,都会让 :empty 失效。
别依赖它做核心判断:
空状态文案和按钮怎么嵌进结构里?
空状态不是静态装饰,是用户下一步动作的起点。文案必须带上下文,按钮必须可操作、有指向。
结构顺序要固定:图标 → 标题 → 说明文字 → 操作按钮(如有)。例如:
-
暂无订单记录
您还没有下单,快去挑选心仪商品吧
注意:
- 按钮必须是
<button type="button"></button>,避免表单提交干扰 - 如果空状态里有按钮,隐藏时必须用
display: none,不能只靠opacity: 0或visibility: hidden—— 后两者仍可能被键盘聚焦或读屏器识别 - 移动端按钮热区 ≥ 44×44px,文字字号不小于 14px,确保可点击可读
最难的不是写出空状态,是让它在数据反复切换(空→有→空)时不闪、不跳、不漏焦点、不读错。所有这些,都藏在容器复用、<template></template> 使用、aria-live 配合和 JS 节点挂载的细节里。











