使用导航地标区域(Navigation Landmark):为无障碍与 AI Agent 构建可跳转的页面导航结构
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
导航地标(landmark)是屏幕阅读器用户理解页面结构、快速跳转到不同导航区域的核心机制。本文基于 Front-End-Checklist 仓库中navigation-landmark规则(skills/navigation-landmark/SKILL.md)的完整实现文档展开,结合仓库内 header.tsx、footer.tsx、table-of-contents.tsx 等真实组件源码,讲解如何用<nav>元素配合 ARIA 标签区分多个导航区域,并给出可复制的 HTML、React、Next.js 实战代码与验证清单。读完本文,你将掌握导航地标的语义、命名规范、React 组件封装方式、跳过链接(skip link)实现,以及如何在真实项目中自查与验证。
一、规则概览:什么是导航地标
| 属性 | 值 |
|---|---|
| 规则名 | navigation-landmark |
| 优先级 | high(高) |
| 难度 | beginner(入门) |
| 预计耗时 | 15 分钟 |
| 类别 | html / accessibility |
规则的核心要求是:页面导航应使用<nav>元素包裹,并通过恰当的 ARIA 标签(aria-label或aria-labelledby)区分多个导航区域。在仓库的规则系统中,该技能描述为:
Page navigation uses nav elements with proper ARIA labels to distinguish multiple navigation regions.
对应的 SKILL.md 给出了速查清单:
- 将导航链接包裹在
<nav>元素中; - 使用
aria-label区分多个<nav>区域; - 提供跳过链接(skip links)以绕过重复导航;
- 屏幕阅读器用户依靠地标进行页面跳转。
仓库中的规则文档(references/rule.md)进一步说明:导航地标帮助用户理解页面结构并快速跳转到不同的导航区块。同时,该规则与landmark-regions(见 packages/content/rules/en/accessibility/landmark-regions.mdx)、html5-semantic-elements(见 packages/content/rules/en/html/html5-semantic-elements.mdx)等规则经常在真实审计中一起出现、互相影响。
二、为什么重要:没有地标时的导航困境
屏幕阅读器用户可以通过地标快速跳转到并识别不同的导航区域。如果没有这些地标,用户必须逐个 Tab 遍历每个链接才能找到目标——在一个包含主导航、面包屑、目录、侧栏、页脚导航的复杂页面上,这可能意味着要穿越几十个链接。
导航地标本质上把"线性扫描"升级为"按区域跳转":
- 用 NVDA 时按
D键可在地标之间循环; - 用 VoiceOver 时通过转子(rotor)选择地标;
- 键盘用户则依赖跳过链接直达主内容。
三、完整的 HTML 示例:五种导航区域一次到位
规则文档给出了一份完整的参考页面,涵盖主导航、面包屑、页内目录、侧栏导航与页脚导航五种典型场景:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Page Title</title> </head> <body> <!-- Skip link (first element) --> <a href="#main-content" class="skip-link"> Skip to main content </a> <header> <!-- Primary navigation --> <nav aria-label="Main"> <ul> <li><a href="/">Home</a></li> <li><a href="/products">Products</a></li> <li><a href="/about">About</a></li> <li><a href="/contact">Contact</a></li> </ul> </nav> </header> <!-- Breadcrumb navigation --> <nav aria-label="Breadcrumb"> <ol> <li><a href="/">Home</a></li> <li><a href="/products">Products</a></li> <li><a href="/products/widgets" aria-current="page">Widgets</a></li> </ol> </nav> <main id="main-content"> <!-- Page content --> <!-- In-page navigation (table of contents) --> <nav aria-label="Table of contents"> <h2>On this page</h2> <ul> <li><a href="#section-1">Section 1</a></li> <li><a href="#section-2">Section 2</a></li> <li><a href="#section-3">Section 3</a></li> </ul> </nav> <h1>Page Title</h1> <section id="section-1">...</section> <section id="section-2">...</section> <section id="section-3">...</section> </main> <aside> <!-- Sidebar navigation --> <nav aria-label="Related pages"> <h2>Related</h2> <ul> <li><a href="/related-1">Related Page 1</a></li> <li><a href="/related-2">Related Page 2</a></li> </ul> </nav> </aside> <footer> <!-- Footer navigation --> <nav aria-label="Footer"> <ul> <li><a href="/privacy">Privacy Policy</a></li> <li><a href="/terms">Terms of Service</a></li> <li><a href="/sitemap">Sitemap</a></li> </ul> </nav> </footer> </body> </html>几个要点:
- 跳过链接必须是
<body>的第一个可聚焦元素,这样键盘用户按 Tab 的第一步就能发现它; - 面包屑用
<ol>(有序列表)表达层级顺序,且当前页用aria-current="page"标注; - 目录放在
<main>内部、<h1>之前,属于"页内导航"。
四、导航地标类型参考表
不同导航区域应使用一致的命名惯例,规则文档给出了如下参考:
| 区域 | 元素 | aria-label 示例 |
|---|---|---|
| 主导航 | <nav> | "Main navigation" |
| 次级导航 | <nav> | "Secondary navigation" |
| 页脚导航 | <nav> | "Footer navigation" |
| 面包屑 | <nav> | "Breadcrumb" |
| 分页 | <nav> | "Pagination" |
| 目录 | <nav> | "Table of contents" |
五、React 导航组件:从 HTML 到可复用组件
规则文档提供了 React 版的组件封装思路,包含NavLink、主导航、面包屑、页脚导航四个组件。下面整理为可直接使用的完整版本:
interface NavLinkProps { href: string children: React.ReactNode isCurrent?: boolean } function NavLink({ href, children, isCurrent }: NavLinkProps) { return ( <li> <a href={href} aria-current={isCurrent ? 'page' : undefined} > {children} </a> </li> ) } interface MainNavProps { links: Array<{ href: string; label: string }> currentPath: string } function MainNav({ links, currentPath }: MainNavProps) { return ( <nav aria-label="Main"> <ul className="main-nav"> {links.map(link => ( <NavLink key={link.href} href={link.href} isCurrent={currentPath === link.href} > {link.label} </NavLink> ))} </ul> </nav> ) } interface BreadcrumbProps { items: Array<{ href: string; label: string }> } function Breadcrumb({ items }: BreadcrumbProps) { return ( <nav aria-label="Breadcrumb"> <ol className="breadcrumb"> {items.map((item, index) => { const isLast = index === items.length - 1 return ( <li key={item.href}> {isLast ? ( <span aria-current="page">{item.label}</span> ) : ( <> <a href={item.href}>{item.label}</a> <span aria-hidden="true"> / </span> </> )} </li> ) })} </ol> </nav> ) } interface FooterNavProps { sections: Array<{ title: string links: Array<{ href: string; label: string }> }> } function FooterNav({ sections }: FooterNavProps) { return ( <nav aria-label="Footer"> <div className="footer-nav"> {sections.map(section => ( <div key={section.title} className="footer-nav__section"> <h3>{section.title}</h3> <ul> {section.links.map(link => ( <li key={link.href}> <a href={link.href}>{link.label}</a> </li> ))} </ul> </div> ))} </div> </nav> ) }设计要点:
aria-current只在当前页链接上输出,其他链接输出undefined即不渲染该属性,避免无意义属性污染 DOM;- 面包屑的最后一个条目是
<span>而非链接,并用aria-current="page"标识当前位置;分隔符/用aria-hidden="true"从无障碍树中隐藏; - 面包屑
<ol>的每个<li>都需要唯一的key,规则文档使用item.href作为 key。
六、跳过链接(Skip Links):绕过重复导航的钥匙
跳过链接让键盘与屏幕阅读器用户跳过重复出现的导航区块直达主内容。规则文档给出了多目标跳过链接的写法与配套 CSS:
function SkipLinks() { return ( <div className="skip-links"> <a href="#main-content" className="skip-link"> Skip to main content </a> <a href="#main-nav" className="skip-link"> Skip to navigation </a> <a href="#search" className="skip-link"> Skip to search </a> </div> ) }配套 CSS 使跳过链接在默认状态下移出可视区域,聚焦时才出现:
.skip-link { position: absolute; top: -40px; left: 0; padding: 8px 16px; background: #000; color: #fff; z-index: 1000; transition: top 0.2s; } .skip-link:focus { top: 0; }需要注意:#main-content这类锚点目标最好配合tabindex="-1"(如<main id="main-content" tabindex="-1">),以便部分浏览器中聚焦位置能正确移动——这一点与仓库中 html5-semantic-elements.mdx 的示例一致。
七、Next.js 布局实战:从本仓库源码验证
规则文档给出了 Next.jsapp/layout.tsx的骨架:在<header>放主导航,在<footer>放分组导航,<main id="main-content">包裹{children}。
这个骨架在本仓库中有完全对应的生产实现,可以直接对照学习:
1. 头部主导航:双区域命名
apps/web/components/navigation/header.tsx 中,桌面与移动分别使用独立的<nav>区域:
- 桌面导航:
<nav className="hidden items-center gap-1 md:flex" aria-label="Main navigation">(第 106 行),包含 Rules / Checklists / Guides 三个入口; - 移动菜单:
<nav id="mobile-menu" ... aria-label="Mobile navigation">(第 216 行),并通过aria-controls="mobile-menu"与aria-expanded={mobileMenuOpen}的汉堡按钮联动; - 搜索按钮使用
aria-label="Search rules"(第 150 行),图标自身aria-hidden="true"以避免冗余朗读。
两个<nav>使用不同的aria-label("Main navigation" vs "Mobile navigation"),正是"多个导航区域必须有唯一标签"这一规则在生产中的体现。
2. 页脚导航:用 aria-labelledby 关联可见标题
apps/web/components/navigation/footer.tsx 的FooterLinkColumn组件展示了另一种命名方式——当导航区域带有可见标题时,用aria-labelledby引用标题 id,而不是重复aria-label:
function FooterLinkColumn({ title, titleId, links }: FooterLinkColumnProps) { return ( <nav aria-labelledby={titleId}> <h3 id={titleId} className="font-medium text-[11px] text-foreground-subtle uppercase tracking-[0.22em]" > {title} </h3> <ul className="mt-4 space-y-3"> {links.map(link => ( <li key={link.href}> ... </li> ))} </ul> </nav> ) }页面中实际传入了三个列:<FooterLinkColumn title="Explore" titleId="footer-product-nav" ... />、<FooterLinkColumn title="Project" titleId="footer-project-nav" ... />、<FooterLinkColumn title="For AI Agents" titleId="footer-agents-nav" ... />,分别输出三个带唯一 id 的页脚导航区域。
3. 目录导航:IntersectionObserver + aria-current
apps/web/components/rules/detail/table-of-contents.tsx 实现了一个<nav aria-label="Table of contents">(第 88 行),通过IntersectionObserver追踪当前滚动位置所在标题,并高亮当前项:
- 扫描
article容器内h2、h3(minLevel=2、maxLevel=3)带id的标题; - 当前激活项使用
aria-current={activeId === heading.id ? 'location' : undefined}(第 101 行); - 标题无 id 时不渲染目录(返回
null),避免出现空的<nav>。
注意:这里的aria-current值用的是"location"而非"page"——"location"表示"集合中当前的元素"(适合目录、面包屑这类位置指示),"page"表示"当前页面链接"(适合分页、导航链接),两者语义不同,可按场景选用。
八、何时用 aria-label,何时用 aria-labelledby
规则文档给出了两者的取舍:
<!-- aria-label: Short, simple label --> <nav aria-label="Main"> <!-- No visible heading --> </nav> <!-- aria-labelledby: Reference visible heading --> <nav aria-labelledby="footer-nav-heading"> <h2 id="footer-nav-heading">Quick Links</h2> <ul>...</ul> </nav>判断标准:
- 没有可见标题时用
aria-label,提供简短、明确的名称(如 "Main"、"Footer"); - 已有可见标题时优先用
aria-labelledby引用该标题的id——这样标题文本会自动成为地标的可访问名称,且标题内容更新时名称自动同步,避免标签与视觉文本不一致; - 仓库页脚正是后者的范例:
<h3 id={titleId}>与<nav aria-labelledby={titleId}>一一对应。
九、常见错误与纠正
规则文档列出的四类高频错误,审计时值得逐一对照:
<!-- ❌ Multiple nav without labels --> <nav>...</nav> <nav>...</nav> <!-- ✓ Labeled navigation regions --> <nav aria-label="Main">...</nav> <nav aria-label="Footer">...</nav> <!-- ❌ Using div instead of nav --> <div class="navigation">...</div> <!-- ✓ Semantic nav element --> <nav aria-label="Main">...</nav> <!-- ❌ Nesting nav elements --> <nav> <nav>...</nav> </nav> <!-- ✓ Separate nav regions --> <nav aria-label="Primary">...</nav> <nav aria-label="Secondary">...</nav>补充两个边界判断:
- 不要过度使用
<nav>:规则文档明确提示,<nav>只应用于主要导航区块,次要链接组(如社交图标)不需要成为导航地标——地标过多会稀释每个地标的可用性。仓库中社交图标(GitHub、X 链接)直接放在 header 容器内而非<nav>中,即遵循此原则; - 多个
<nav>必须标签唯一,否则屏幕阅读器朗读时用户无法区分"Navigation、Navigation"到底指哪个。
十、验证清单:如何确认导航地标达标
规则文档给出了 6 步验证流程,同时 SKILL.md 要求"验证最终浏览器输出的标记,而不是框架抽象层源码"(Validate the final browser-facing markup, not just the source framework abstraction):
- 使用屏幕阅读器地标导航:NVDA 下按
D键循环地标,VoiceOver 下使用转子(rotor)查看地标列表; - 确认每个 nav 区域有唯一标签:地标列表中不应出现多个无名称或重名的 Navigation;
- 检查跳过链接在聚焦时出现:Tab 到第一个元素时,跳过链接应滑入可视区域(对应 CSS
:focus { top: 0 }); - 验证当前页链接的
aria-current:当前所在页面/条目应正确输出aria-current="page"或"location"; - 测试键盘遍历所有链接:确保每个
<nav>内链接均可 Tab 到达、焦点顺序符合预期; - 确认 nav 数量与预期区域一致:主导航、面包屑、目录、页脚各应恰好出现一次,无多余或缺失。
自动化方面,仓库规则体系中 WAVE、axe DevTools、Lighthouse 等工具(见 html5-semantic-elements.mdx 的 Tools & Validation 一节)均可辅助检测地标结构;React/Next.js 项目中建议直接查看服务端渲染后的 HTML 或浏览器 DOM,因为客户端组件(如 header.tsx 是'use client'组件)最终输出由运行时决定。
十一、关联规则与延伸阅读
导航地标不是孤立规则,在仓库的规则图谱中它与以下规则密切相关,常被一同审计:
- landmark-regions:地标区域正确性,要求每个地标类型在适当位置只出现一次(如全页只有一个
<main>),同类型多地标必须加标签; - html5-semantic-elements:语义化 HTML 元素,
<nav>与<header>、<main>、<footer>共同构成页面骨架; landmark-one-main:全页唯一<main>地标;breadcrumb-navigation:面包屑导航本身的无障碍实现,与导航地标命名直接相关。
仓库中所有规则以结构化 MDX 存储于 packages/content/rules/en,每条规则都带有tldr、whyItMatters、check/fix/explain/codeReview 四类 prompt 及aiContext,可直接被 Agent 与 LLM 检索复用——这也正是 Front-End-Checklist 面向"人类与 AI Agent"双受众定位的体现。
小结:导航地标是无障碍页面结构的基石。用<nav>包裹导航、用唯一aria-label/aria-labelledby命名、用跳过链接绕过重复区域,再配合aria-current标注当前位置,就能让屏幕阅读器用户、键盘用户与 AI Agent 都高效地"按区域跳转"。对照本仓库的 header.tsx、footer.tsx、table-of-contents.tsx 三个真实组件,即可在生产级 Next.js 项目中落地这套规范。
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考