屏幕阅读器只认第一个标签,因为它是landmark角色(role="main")的语义锚点而非语法校验项;nvda、voiceover等工具扫描dom时仅将首个绑定m键跳转逻辑,后续所有被静默忽略或触发重复播报,导致用户行为不可预测。

为什么屏幕阅读器只认第一个标签
因为<main></main>当作“主内容区”并绑定 M 键跳转逻辑;后续所有<main></main>会被静默忽略,或触发重复播报“主内容开始”,用户无法预测行为。
常见错误现象:
- SPA 路由切换后旧
<main></main>没卸载,新页面又挂载一个 → 搜索<main></main>发现两个,但按 M 键只跳到第一个(可能是已隐藏的旧内容) - 服务端模板未区分列表页/详情页,共用同一套
<main></main>包裹逻辑 → 渲染出嵌套或并存结构 - Next.js 的
app/layout.tsx里直接写<main></main>→ hydration 后全局壳层和子页面各有一个,Lighthouse 报duplicate-main
main必须是body的直接子元素,否则读屏器跳转失效
<main></main>不能嵌套在<header></header>、<nav></nav>、<footer></footer>里,这是 W3C 明文禁止的语义冲突,不是兼容性问题。一旦写成<header><main>...</main></header>,主流读屏器大概率跳过该<main></main>,甚至整个地标识别链断裂。
正确结构只有一种:<header></header><main></main><nav></nav><footer></footer>。注意:
-
<header></header>可以作为<main></main>的**直接子元素**,但仅限当前页面独有元信息(如文章<h1></h1>+<time></time>),不是网站全局头部 - 如果用
<div class="container">包住<code><main></main>,它就不再是body的直接子元素 → Lighthouse 不报错但读屏器失效 - Vue/React 中若组件模板用
<template></template>包裹多个根节点,容易把<main></main>意外塞进父级语义区块内,需手动检查最终渲染 DOM -
<article></article>(单篇博客正文) -
<ol></ol>(商品列表页的核心列表) -
<form></form>(独立提交表单,如注册页) - 纯文本段落(无复用性)
-
<nav></nav>(全局导航) -
<footer></footer>(页脚版权) -
<aside></aside>(侧边推荐、广告) -
<header></header>(网站 Logo + 主导航) - 打开 DevTools(F12),切到 Elements 面板,Ctrl+F / Cmd+F 搜索
<main></main> - 检查匹配结果:只应出现一个
<main></main>开始标签和一个结束标签 - 特别注意 SPA 场景——滚动到底部再搜一次,避免漏掉异步加载后挂载的冗余
<main></main> - 用 VoiceOver 按 M 键、NVDA 按 D 键,观察是否跳转到预期位置;若卡在导航或反复提示“主内容开始”,就是位置或数量错了
什么内容该进,什么必须踢出去
判断标准极简:这块内容复制粘贴到另一个页面还能原样用吗?能,就不属于<main></main>。语义归属比视觉权重重要得多。
✅ 允许放进<main></main>:
❌ 必须踢出去:
⚠️ 注意:局部“主内容”场景(如弹窗、卡片)别硬塞<main></main>,改用<section role="region" aria-labelledby="id"></section>替代。
如何快速验证当前页面是否合规
不能靠肉眼扫代码,必须实操验证:
最隐蔽的坑是:浏览器不报错,DevTools 不警告,Lighthouse 却标红,而读屏器行为已经不可控——等用户反馈时,问题早已在线上跑了好几周。











