首页
/ freeCodeCamp Applied Accessibility 课程:用 tabindex 为元素添加键盘焦点的原理与实战

freeCodeCamp Applied Accessibility 课程:用 tabindex 为元素添加键盘焦点的原理与实战

2026-09-06 21:17:04作者:侯霆垣

本篇指南基于 freeCodeCamp 课程库中 "Applied Accessibility"(应用无障碍)模块的挑战 Use tabindex to Add Keyboard Focus to an Element(挑战 id:587d7790367417b2b2512ab0)展开,讲解 HTML tabindex 属性的三种取值行为、如何在非交互元素上启用键盘焦点并让 CSS :focus 伪类生效,并结合课程解析器源码(challenge-parser 管道)说明挑战文件中 seed、测试断言与答案是如何从 Markdown 解析为结构化数据的,帮助读者既掌握无障碍实战技能,又理解课程数据背后的工程实现。

挑战在课程结构中的位置

该挑战文件位于 curriculum/challenges/english/blocks/applied-accessibility/587d7790367417b2b2512ab0.md,属于英文课程块 applied-accessibility。从 applied-accessibility.json 可以看到:

  • helpCategoryHTML-CSSblockLayoutlegacy-challenge-list
  • challengeOrder 共编排 21 个挑战,覆盖 alt 文本、语义化地标元素、label/fieldset 表单无障碍、对比度、描述性链接文本、access key 等主题;
  • 本挑战(587d7790367417b2b2512ab0,标题 "Use tabindex to Add Keyboard Focus to an Element")排在第 20 位,紧接其后的是姊妹挑战 587d7790367417b2b2512ab1("Use tabindex to Specify the Order of Keyboard Focus for Several Elements"),两者共同构成 tabindex 的完整知识链。

挑战文件采用 YAML frontmatter + 分区正文的格式,frontmatter 关键字段包括:

id: 587d7790367417b2b2512ab0
title: Use tabindex to Add Keyboard Focus to an Element
challengeType: 0
dashedName: use-tabindex-to-add-keyboard-focus-to-an-element
forumTopicId: 301027

正文按约定划分为 --description--(讲解)、--instructions--(任务)、--hints--(提示+测试断言)、--seed--(初始代码)、--solutions--(参考答案)等分区,这些分区名与解析器插件严格对应(见第 5 节)。

核心原理:tabindex 的三种取值行为

挑战讲解部分指出,tabindex 属性围绕"元素键盘焦点"有三种不同功能:属性写在标签上即表示该元素可以获得焦点,而其整数值(正数、负数、零)决定具体行为:

取值 行为 典型场景
tabindex="0" 元素可聚焦,并加入页面默认 Tab 顺序,顺序与元素在 HTML 源码中出现的顺序一致 divspanp 等非交互元素成为焦点停靠点
负值(通常为 -1 元素可通过编程方式聚焦,但不能通过键盘 Tab 到达 弹窗 div 激活时把焦点移入(element.focus());挑战文档明确说明该用法超出本挑战范围
正数(>= 1 显式指定 Tab 顺序:焦点按 1、2、3… 依次轮转,之后才轮到 tabindex="0" 和默认可聚焦元素 少数需要控制焦点流的场景(见第 6 节的姊妹挑战)

链接(<a>)、表单控件(<input> 等)天然按 HTML 源码顺序接收键盘焦点;而对 divspanp 这类默认不可聚焦的元素,加上 tabindex="0" 即可获得同等能力:

<div tabindex="0">I need keyboard focus!</div>

注意(原文档 Note):负值 tabindex(典型为 -1)表示元素可聚焦但键盘不可达,一般用于以编程方式把焦点带入内容(例如弹窗 div 被激活时),这超出了该挑战系列的范围。

实战任务:让 Ninja Survey 的说明文字可聚焦

任务描述

场景:Camper Cat 制作了一份新的问卷来收集用户信息。他知道输入框会自动获得键盘焦点,但希望键盘用户在使用 Tab 遍历表单项时,能在**说明文字(<p> 标签)**上停一下。任务要求:

  1. 给包含表单说明的 <p> 标签添加 tabindex 属性,值设为 0
  2. 附带收益(Bonus):使用 tabindex 后,CSS 伪类 :focus 也能在该 <p> 标签上生效。

初始代码(seed)

挑战文件 --seed-contents-- 分区提供的完整初始代码如下,其中已预先写好了依赖 p:focus 的样式:

<head>
  <style>
  p:focus {
    background-color: yellow;
  }
  </style>
</head>
<body>
  <header>
    <h1>Ninja Survey</h1>
  </header>
  <section>
    <form>
      <p>Instructions: Fill in ALL your information then click <b>Submit</b></p>
      <label for="username">Username:</label>
      <input type="text" id="username" name="username"><br>
      <fieldset>
        <legend>What level ninja are you?</legend>
        <input id="newbie" type="radio" name="levels" value="newbie">
        <label for="newbie">Newbie Kitten</label><br>
        <input id="intermediate" type="radio" name="levels" value="intermediate">
        <label for="intermediate">Developing Student</label><br>
        <input id="master" type="radio" name="levels" value="master">
        <label for="master">9th Life Master</label>
      </fieldset>
      <br>
      <fieldset>
      <legend>Select your favorite weapons:</legend>
      <input id="stars" type="checkbox" name="weapons" value="stars">
      <label for="stars">Throwing Stars</label><br>
      <input id="nunchucks" type="checkbox" name="weapons" value="nunchucks">
      <label for="nunchucks">Nunchucks</label><br>
      <input id="sai" type="checkbox" name="weapons" value="sai">
      <label for="sai">Sai Set</label><br>
      <input id="sword" type="checkbox" name="weapons" value="sword">
      <label for="sword">Sword</label>
      </fieldset>
      <br>
      <input type="submit" name="submit" value="Submit">
    </form><br>
  </section>
  <footer>&copy; 2018 Camper Cat</footer>
</body>

参考答案(solution)

完整答案(--solutions-- 分区)与 seed 仅有一行不同——给 <p> 添加 tabindex="0"

<p tabindex="0">Instructions: Fill in ALL your information then click <b>Submit</b></p>

其余部分(<head> 样式、<header><form> 内的 label/input/fieldset、<footer>)与 seed 完全一致。加上该属性后,键盘用户 Tab 遍历时焦点会在说明文字处停留,同时触发 p:focus { background-color: yellow; } 的视觉反馈——这正是"Bonus"所指::focus 伪类只匹配可聚焦且当前持有焦点的元素,tabindex<p> 获得该能力的开关。

测试断言:两条测试如何验证答案

挑战的 --hints-- 分区包含"提示文本 + 测试代码块"两两成对的内容,解析后成为判题用的 tests 数组。本挑战有两条断言:

// 测试 1:确认 <p> 上存在 tabindex 属性
assert.isNotNull(document.querySelector('p')?.getAttribute('tabindex'));
// 测试 2:确认其值精确为字符串 '0'
assert.equal(document.querySelector('p')?.getAttribute('tabindex'), '0');

两个实现细节值得注意:

  • getAttribute 返回的是字符串而非数字,因此第二条断言比较的是 '0' 而不是 0
  • querySelector('p')?.getAttribute(...) 使用了可选链,页面不存在 <p> 时断言安全地得到 undefined 并失败,而不是抛出异常。

从 Markdown 到结构化数据:课程解析器实现

这些分区并非静态文本,而是被 tools/challenge-parser/parser/index.js 中的 unified/remark 管道解析为挑战对象。从源码结构看(index.js 第 24-69 行),管道按序挂了这些插件:

  1. remark-parse 将 Markdown 解析为 mdast 树;
  2. remark-frontmatter 把顶部的 YAML 段转为 yaml 节点,再由 add-frontmatter 插件 提取为 idtitlechallengeTypedashedName 等字段;
  3. validateSections 先校验所有分区标记;
  4. add-seed 插件:读取 # --seed-- 分区下的 ## --seed-contents--,把其中的代码块提取为 challengeFiles(含 contents 字段),即本挑战那份完整的 Ninja Survey HTML 就经由它进入课程数据;
  5. add-solution 插件:读取 # --solutions-- 分区并按分隔线切分为 solutions 数组;
  6. add-tests 插件:读取 # --hints-- 分区,强制要求节点成对出现(提示文本后必须紧跟测试代码块,否则抛出 "Hints must be in pairs" 错误),再用 lodash/chunk 按两两分组映射为 { text, testString } 测试项(add-tests.js 第 5-32 行)——本挑战的两条 assert 代码块正是这样成为 testString 的;
  7. addText 插件把 descriptioninstructions 等分区的 mdast 节点转成 HTML 字符串存入对应字段。

解析结果随后要通过与 challenge-schema.js 定义的 Joi 模式校验。可以确认的关键约束包括:tests 字段是 .required()第 375-389 行),每项 text 必填、testString 允许空串;challengeType 限定在 0-33;solutions 为"数组的数组"(Joi.array().items(Joi.array().items(fileJoi).min(1))),即支持多份答案。因此本挑战文件中"两条 hint 断言 + 一份 solution"的写法,恰好满足模式要求的最小完整形态。

进阶:正数 tabindex 控制 Tab 顺序及其注意事项

姊妹挑战 587d7790367417b2b2512ab1.md 紧接本挑战,讲解 tabindex >= 1 的行为,可作为本主题的自然延伸:

  • 设置 tabindex="1" 的元素会最先获得焦点,随后按 2、3 等指定值轮转,最后才到默认顺序与 tabindex="0" 的元素:
<div tabindex="1">I get keyboard focus, and I get it first!</div>
<div tabindex="2">I get keyboard focus, and I get it second!</div>
  • 该挑战的场景是把搜索 input 与提交 input 分别设为 tabindex="1"tabindex="2",使其成为 Tab 顺序的前两项(页面被 CSS 定位到右上角);
  • 原文档同时给出两条无障碍警告:一旦显式设置 Tab 顺序,就会覆盖基于 HTML 源码的默认顺序,可能让用户困惑(他们预期从页面顶部开始导航);另外部分浏览器在点击某元素后会从 Tab 顺序的中段开始,该挑战的 seed 中额外放置了一个带 tabindex="1"div.overlay 元素,作用是保证用户总是从 Tab 顺序的起点开始;
  • 结论性建议:正数 tabindex 技术上可行,但在无障碍层面"使用前需谨慎"(take care before applying it)。

最佳实践小结

结合本挑战与课程源码证据,tabindex 的使用可归纳为:

  1. 优先不加:能用原生可聚焦元素(链接、表单控件)或语义化标签解决的,就不要动 tabindex
  2. 需要把非交互元素纳入焦点流时,用 tabindex="0" 并配合 :focus 样式提供可见焦点反馈,同时记住语义上更合适的做法仍是优先选择可交互元素;
  3. 需要编程式聚焦时(如打开弹窗),用 tabindex="-1" + JS focus(),它不会干扰键盘 Tab 顺序;
  4. 避免正数 tabindex:它会全局覆盖默认 Tab 顺序并可能破坏"从页面顶部开始"的用户预期,仅在确有必要且经过无障碍评估时使用。

课程侧的验证逻辑也提供了可复用的测试写法:用 document.querySelector(...)?.getAttribute('tabindex') 先判非空、再比对字符串值(如 '0''1'),即可在断言层面同时验证"属性存在"与"取值正确"两个目标。

登录后查看全文
热门项目推荐
相关项目推荐