---
name: accessibility-rules
description: "网页 A11y 无障碍性规范。适用于所有前端 UI 交互界面设计、HTML 标签搭建及自定义交互控件开发。"
globs: ["src/**/*.tsx", "src/**/*.html", "**/*.tsx", "**/*.jsx", "**/*.html"]
alwaysApply: false
updated: 2026-05-22
---

# 网页 A11y 无障碍性规范

> [!IMPORTANT]
> 优秀的网页交互设计应该拥抱每一个人。AI 助手在编写 HTML 或 TSX UI 组件时，必须优先确保语义化标签的正确选用，以及屏幕阅读器与键盘可访问性（a11y）的完整适配。

## 1. 适用场景

当您设计通用组件、自定义按钮表单、引入 SVG 图标与多媒体资源，或构建复杂的悬浮导航、模态弹窗（Modals）交互时生效。

## 2. 操作规则

1. **坚持语义化标签 (Semantic HTML)**：
   - **绝对禁止**使用无语义的 `<div>` 或 `<span>` 裸写并直接绑定 `onClick` 事件来扮演按钮。凡是充当可交互按钮的元素，必须无条件选用真正的 `<button>` 或具有超链接属性的 `<a>` 标签。
2. **多媒体 Alt 文字底线 (Image Alt Text)**：
   - 所有的 `<img>` 标签必须明确声明 `alt` 属性。
   - **内容型图片**：`alt` 必须是包含图文说明的高质量简短描述。
   - **纯装饰性图片/图标**：必须声明 `alt=""` 或 `aria-hidden="true"`，以此指示屏幕阅读器（Screen Reader）安全地自动忽略，保障阅读流连贯。
3. **自定义交互的 ARIA 修饰 (ARIA Attributes & TabIndex)**：
   - 对于非标准的原生复杂交互控件（如自定义的 Modal 弹窗或下拉 Tab 选项卡），必须显式配置 `role`（如 `role="dialog"`）以及必要的 ARIA 动态状态修饰（如 `aria-expanded`、`aria-haspopup`）。
   - 自定义可聚焦元素必须赋予 `tabIndex={0}` 保证能够被 Tab 键锁定，并绑定键盘事件（如 `onKeyDown` 拦截 `Enter`/`Space` 键），实现全键盘免鼠标操作。

## 3. 禁止事项

- 严禁为了去除边框而在 CSS 中使用全局 `outline: none;` 或 `:focus { outline: 0; }`，除非同时提供了同样醒目的替代高亮聚焦态样式（如 `:focus-visible` 边框高亮）。

## 4. 验证方式

- 运行 Jest HTML a11y 工具包或执行 `npm run lint` 触发 `eslint-plugin-jsx-a11y` 检查。

## 5. 代码对比示例

### ❌ 错误示例（非语义化、屏幕阅读器瞎眼、键盘完全无法操作）

```tsx
// 违反规则 2.1：使用 div 假冒 button，屏幕阅读器无法读取它是按钮
// 违反规则 2.3：不可被 Tab 聚焦，无法接受键盘回车/空格触发，非鼠标用户无法操作系统
// 违反规则 2.2：图片缺失 alt 描述
import React from 'react';

export const BadCustomButton: React.FC = () => {
  const handleClick = () => alert("已支付！");
  
  return (
    <div className="pay-btn" onClick={handleClick}>
      <img src="https://cdn.example.com/pay-icon.png" />
      <span>确认支付</span>
    </div>
  );
};
```

###  正确方向（完美语义化、键盘友好聚焦、ARIA 状态完备）

```tsx
import React, { KeyboardEvent } from "react";

export const GoodInteractiveButton: React.FC = () => {
  const handleAction = () => alert("已成功发起安全支付！");

  // 遵循规则 2.3：支持键盘回车或空格事件触发，关怀全键盘无障碍操作用户
  const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
    if (event.key === "Enter" || event.key === " ") {
      event.preventDefault();
      handleAction();
    }
  };

  return (
    /* 
      遵循规则 2.1：选用标准原生 <button> 标签，自动享有完美无障碍焦点、TabIndex 属性及屏幕阅读器语义声明
      遵循规则 2.3：在 CSS 中对于 focus 状态保留醒目的 focus-visible 自定义发光环 
    */
    <button
      onClick={handleAction}
      onKeyDown={handleKeyDown}
      className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-2"
    >
      {/* 遵循规则 2.2：由于此图片为纯装饰性图标，直接声明 alt="" 且配置 aria-hidden 避免嘈杂的阅读器播报 */}
      <img 
        src="https://cdn.example.com/pay-icon.png" 
        alt="" 
        aria-hidden="true" 
        className="w-4 h-4"
      />
      <span className="font-medium">确认支付</span>
    </button>
  );
};
```
