contenteditable 必须配 tabindex="0" 才能键盘聚焦,仅设 contenteditable="true" 无法 tab 进入,违反 wcag 2.1.1;tabindex="-1" 仅支持 js 聚焦,正数会破坏阅读顺序,pointer-events:none 或 user-select:none 也会阻断光标定位。

contenteditable 必须配 tabindex="0" 才能键盘聚焦
写了 contenteditable="true" 却点不进去、Tab 不进去,不是浏览器 bug,是漏了 tabindex="0"。这个组合才是可编辑 + 可聚焦的最小合法单元。
常见错误现象:
- 只写
contenteditable,元素不可键盘进入,违反 WCAG 2.1.1(键盘可访问性) - 写了
tabindex="-1"——可编程聚焦(el.focus()),但无法自然 Tab 进入 - 写了
tabindex="5"——强行插入 tab 流,打乱阅读顺序,屏幕阅读器会跳读 - 父级或子级有
pointer-events: none或user-select: none,光标定位失败
实操建议:
- 启用时同步设置:
el.setAttribute('contenteditable', 'true'); el.tabIndex = 0; - 禁用时还原:
el.setAttribute('contenteditable', 'false'); el.tabIndex = -1; - 避免在
<script></script>、<meta>上用contenteditable——浏览器静默忽略
data-* 属性值全是字符串,且不响应变更
data- 属性不是状态容器,它只读取初始 HTML 中的值,所有值都是字符串:哪怕 data-count="42",JS 里拿到的也是 "42";data-active="true" 是字符串 "true",不是布尔值。
常见错误现象:
- React/Vue 组件 mount 后调用
el.setAttribute('data-id', '500'),组件没重渲染 - 把
data-config='{"theme":"dark"}'当 JSON 解析,没加try/catch,JSON.parse()报错中断初始化 - SSR 渲染的 HTML 中
data-值被客户端 JS 覆盖,但框架没监听属性变化,导致首屏与 JS 状态不一致
实操建议:
- 需要类型转换时手动处理:
Number(el.dataset.count)、el.dataset.active === "true" - 连字符命名必须转驼峰:
data-user-id→dataset.userId,不是dataset.userid或dataset.Userid - 数字开头的属性名(如
data-1st-place)只能用方括号:dataset["1st-place"] - 框架内状态管理别依赖
dataset,改用 props/state 或attributeChangedCallback(自定义元素中)
lang 必须设在 html> 根标签上才生效
浏览器和辅助技术只识别 这个根声明。改 或某个 <div> 的 <code>lang,对 SEO、语音朗读、自动翻译提示、字体 fallback 几乎没影响。
常见错误现象:
- SPA 切换语言后只更新了
document.body.lang,VoiceOver 仍用旧语种朗读 - Chrome 自动翻译按钮不出现,或点了之后把中文翻成英文再翻回中文,语义错乱
- 中英混排时引号间距异常,因为浏览器没识别出语言切换
实操建议:
- SSR 页面:服务端根据
Accept-Language头输出 - SPA 页面:语言切换时执行
document.documentElement.lang = "ja-JP",不是body,也不是只改 state - 值必须符合 BCP 47:小写字母 + 连字符 + 地区码(如
zh-CN、en-GB),zh_cn、ZH-CN、Chinese都无效 - 局部多语言内容需显式声明:
<blockquote lang="fr">Je suis français.</blockquote>
hidden 属性对 script/meta 等元素无效
hidden 看似万能隐藏开关,但它在某些元素上根本不起作用——不报错、不警告,只是安静失效。
哪些元素上 hidden 无效:
-
<script></script>、<meta>、<link>、<style></style>—— 浏览器直接忽略hidden属性 -
<title></title>、<base>—— 同样静默失效
为什么?这些元素本身不参与渲染流,hidden 的语义是“从渲染树中移除”,而它们压根不在那里。
实操建议:
- 想控制脚本加载逻辑,用 JS 动态创建/移除
<script></script>标签,别指望hidden - 想隐藏元信息,删掉整个标签,而不是加
hidden - 对普通 DOM 元素(
<div>、<code><p></p>等),hidden是安全可靠的,等价于display: none但语义更强 - 旧版 Safari/IE 不支持
hidden,需 fallback:el.hidden ? el.style.display = 'none' : el.style.display = ''
最常被忽略的一点:全局属性不是“贴上去就完事”的装饰品,它们各自有明确的触发边界和行为契约。比如 contenteditable 和 tabindex 是绑定关系,lang 是层级继承关系,data- 是单次快照关系。混淆这些契约,就会在可访问性、SEO、框架响应、用户输入等环节突然掉链子。











