shadcn-ui 的 Radix Dialog 这两个警告到底在说什么?为什么会报?怎么修?

问题

最近在项目里遇到两个来自 Radix Dialog 的控制台提示:

它们不是 "功能错误",但属于 无障碍(a11y)级别的警告:Radix 在开发环境主动提醒你对话框缺少 "可被屏幕阅读器正确理解" 的关键语义。

本文基于 coco-app(React 18 + TS + Tailwind + shadcn-ui)里真实踩坑的修复过程总结。


1. 背景:Radix Dialog 需要什么语义?

一个可访问的 Dialog 至少需要:

  • 可访问名称(accessible name) :告诉读屏软件 "这个弹窗叫啥"
    • 对应:<DialogTitle />(内部映射到 aria-labelledby
  • 可访问描述(accessible description) :告诉读屏软件 "这个弹窗在说啥/要用户做啥"
    • 对应:<DialogDescription />(内部映射到 aria-describedby

Radix 的示例结构也是这个顺序(Title + Description + 内容 + Close 等)。


2. 报错 1:为什么必须要 DialogTitle

2.1 报错含义

DialogContent requires a DialogTitle...

意思是:你的 <DialogContent /> 里没有提供标题,导致 Dialog 没有可访问名称。读屏用户打开弹窗时,不知道这是 "更新提示" 还是 "删除确认"。

2.2 coco-app 的修复方式

我们在 UpdateApp 弹窗中增加了一个 "对视觉隐藏、对读屏可见" 的标题:

  • 文件:src/components/UpdateApp/index.tsx:164
  • 代码形态:
tsx 复制代码
<DialogTitle className="sr-only">{t("update.title")}</DialogTitle>

为什么用 sr-only

  • Tailwind 的 sr-only 能达到UI 不变,读屏可读 的效果(有些 shadcn 模板会有现成的 VisuallyHidden 组件)。

3. 报错 2:为什么必须要 DialogDescription / aria-describedby

3.1 报错含义

Warning: Missing Description or aria-describedby={undefined} for {DialogContent}.

意思是:Dialog 没有可访问描述,或者你显式把 aria-describedby 置为 undefined 但又没有描述节点关联上。

Radix 的逻辑大致是:

  • 你提供 <DialogDescription />:Radix 自动把它的 id 绑定到 aria-describedby
  • 你不提供 <DialogDescription />:Radix 会提醒你 "缺描述",避免读屏用户只听到标题但不知道要做什么

3.2 coco-app 的修复方式

我们把原先展示更新说明的 div 替换为 DialogDescription(UI class 不变,只换组件语义):

  • 文件:src/components/UpdateApp/index.tsx:179-193
  • 代码形态:
tsx 复制代码
<DialogDescription className="text-sm leading-5 py-2 text-foreground text-center">
  {updateInfo ? ... : t("update.date")}
</DialogDescription>

这样 Radix 就能自动生成正确的 aria-describedby,warning 消失。


4. "洁癖":它不是 bug,但是就是在控制台报红了...

shadcn-ui 的 Dialog 本质是对 Radix Dialog 的一层轻封装(项目里对应 src/components/ui/dialog.tsx),它不会强制你必须写 Title/Description。

4.1 为什么更容易踩坑?

因为 UI 上你可能觉得:

  • 我已经有图标(logo)
  • 我已经有一段说明文字(div/p)
  • 我不想显示标题

视觉上满足 ≠ 语义上满足 。读屏依赖的是 aria-labelledby/aria-describedby 的关联,而不是你页面里有没有一个看起来像标题的 div


5. 最推荐的写法、更标准的写法

5.1 标题不想显示:用 sr-only

tsx 复制代码
<DialogTitle className="sr-only">{t("xxx.title")}</DialogTitle>

5.2 描述存在:用 DialogDescription

tsx 复制代码
<DialogDescription className="sr-only">
  {t("xxx.description")}
</DialogDescription>

是否一定要隐藏 Description?

  • 不一定。像更新弹窗这种 "正文就是描述",直接用 DialogDescription 包住正文最自然。

6. 也可以手动 aria-describedby 吗?可以,但更容易出错

你当然可以自己写:

tsx 复制代码
<DialogContent aria-describedby="my-desc">
  <div id="my-desc">...</div>
</DialogContent>

但坑在于:

  • id 可能忘了写 / 重复
  • 条件渲染导致节点不在 DOM(aria 指向不存在的 id)
  • 重构时删掉了 id 没发现
  • 多弹窗复用组件时 id 冲突

所以在 shadcn/Radix 体系里,优先使用 DialogTitle / DialogDescription 让 Radix 负责关联更稳。


7. 真正的"坑点清单"(建议以后 review 的时候对照)

  • 只写了 <DialogContent />,把标题/正文都塞进普通 div
  • 标题用视觉元素表达(比如 logo 或大号文本),但没用 DialogTitle
  • 描述是条件渲染的,导致有时没有 DialogDescription
  • 想隐藏标题却直接不写(应该隐藏而不是删除)

8. coco-app 里的落地实践(最终结论)

在 coco-app 里,我们最终遵循了一个简单规则:

  • 每个 DialogContent 内部都应该有且只有一个语义标题:DialogTitle
  • 只要弹窗有 "说明性文本",优先用 DialogDescription 承载
  • 如果 UI 不需要展示标题/描述:用 sr-only 隐藏(而不是不写)
相关推荐
MrBread2 小时前
突破限制:vue-plugin-hiprint 富文本支持深度解析与解决方案
前端·开源
用户4099322502122 小时前
Vue3中v-if与v-for为何不能在同一元素上混用?优先级规则与改进方案是什么?
前端·vue.js·后端
与兰同馨2 小时前
【踩坑实录】一次 H5 页面在 PC 端的滚动条与轮播图修复全过程(Vue + Vant)
前端
全栈技术负责人2 小时前
前端架构演进之路——从网页到应用
前端·架构
T___T2 小时前
React Props:从基础使用到高级组件封装
前端·react.js
汉堡大王95272 小时前
React组件通信全解:父子、子父、兄弟及跨组件通信
前端·javascript·前端框架
霍理迪2 小时前
CSS继承,优先级以及字体样式
前端·css
LeeHK2 小时前
在项目中调试vue2源码,watch,nextTick执行顺序梳理
前端
爱敲点代码的小哥3 小时前
json序列化和反序列化 和 数组转成json格式
java·前端·json