---
sidebar_position: 20
tags: [Web, HTML, a11y]
---

# Accessibility

## Semantics

- Semantical HTML section [guide](https://css-tricks.com/how-to-section-your-html).
- Semantic HTML [presentation](http://justineo.github.io/slideshows/semantic-html/#/).
- Semantic [search element](https://www.sarasoueidan.com/blog/in-quest-of-search).

:::tip[HTML First over ARIA]

```html
<!--div role="banner"-->
<header></header>

<!--div role="navigation"-->
<nav></nav>

<!--div role="main"-->
<main></main>

<!--div role="region"-->
<section [accessible name]></section>

<!--div role="complementary"-->
<aside></aside>

<!--div role="contentinfo"-->
<footer></footer>

<!--div role="form"-->
<form></form>

<div role="search"></div>
```

:::

## Structure

- Semantics section [reference](http://www.html5jscss.com/html5-semantics-section.html).
- `<header>`: `role="banner"`
- `<nav>`: `role="navigation"`
- `<main>`: `role="main"`
- `<aside>`: `role="complementary"`
- `<section>`: `role="region"`
- `<article>`: `role="article"`
- `<footer>`: `role="contentinfo"`

```html
<header>
  <nav>
    <ul>
      <li><a></a></li>
    </ul>
  </nav>
</header>

<main>
  <section></section>
</main>

<footer></footer>
```

## Heading

- 7 heading levels: `<div role="heading" aria-level="7"></div>`
- One `<h1>` per page

## Navigation

- Have a HTML sitemap.
- Support keyboard navigation (Key and Tab Index).
- Breadcrumbs a11y:
  - `aria-label="breadcrumbs"`
  - `aria-label="page"`

```html
<nav aria-label="breadcrumbs">
  <ol>
    <li>
      <a href="https://example.com/"> Home </a>
    </li>
    <li>
      <a href="https://example.com/products"> Products </a>
    </li>
    <li>
      <a href="https://example.com/products/childrens-clothing"> Children's clothing </a>
    </li>
    <li>
      <a href="https://example.com/products/childrens-clothing/shoes" aria-current="page"> Shoes </a>
    </li>
  </ol>
</nav>
```

## Section

```html
<section aria-labelledby="sectionHeader1">
  <h2 id="sectionHeader1">A great section</h2>
</section>
<section aria-labelledby="sectionHeader2">
  <h2 id="sectionHeader2">An even better section</h2>
</section>
```

## Article

The `<article>` element is used to represent a fully self-contained region of content

```html
<article>
  <header>
    <h1>Why you should buy more cheeses than you currently do</h1>
  </header>
  <section>
    <header>
      <h2>Part 1: Variety is spicy</h2>
    </header>
    <!-- cheesy content -->
  </section>
  <section>
    <header>
      <h2>Part 2: Cows are great</h2>
    </header>
    <!-- more cheesy content -->
  </section>
</article>
```

## Reference

- `<cite>`
- `<q>`
- `<blockquote>`
- `<code>`

```html
<p>
  Every time Kenny is killed, Stan will announce
  <q cite="http://en.wikipedia.org/wiki/Kenny_McCormick#Cultural_impact"> Oh my God, you/they killed Kenny! </q>.
</p>
```

```html
<blockquote cite="https://www.huxley.net/bnw/four.html">
  <p>Words can be like X-rays, if you use them properly – they'll go through anything. You read and you're pierced.</p>
</blockquote>

<cite>– Ados Huxley, Brave New World</cite>
```

## Link

```html
<article>
  <h2 id="article1-title">My article</h2>
  <p>Article brief description with truncation...</p>
  <a href="article1-url" aria-labelledby="article1-title">Read more</a>
</article>
```

## Text

- `<b>`
- `<strong>`
- `<mark>`
- `<ins>`
- `<del>`
- `<abbr>`: 专有名词解释 `<abbr title="HyperText Markup Language">HTML</abbr>`

不要将 `<b>` 元素与 `<strong>`、`<em>` 或 `<mark>` 元素混淆:

- `<strong>` 元素表示某些重要性的文本
- `<em>` 强调文本
- `<mark>` 元素表示某些相关性的文本

### Color

- Devtool inspect elements A11Y for color contrast ratio.
- Color contrast: more than `4.5:1` ratio.
- Don't forget `::selection`.

### Spacing

- `line-height` of blocks of text should be **1.5**.
- space between paragraphs should be **1.5 times** the `line-height`
  (so a minimum of `2.25 rem`).
- Line height (line spacing) to at least **1.5 times** the font size.
- Spacing following paragraphs to at least **2 times** the font size.
- Letter spacing (tracking) to at least **0.12** times the font size.
- Word spacing to at least **0.16** times the font size.

## Button

Use `<button>` for clickable elements

## Image

- `alt=""`

## SVG

- `<title>`
- `<desc>`

```html
<svg width="100" height="75">
  <title>Dark rectangle</title>
  <desc>A grey rectangle with rounded corners and a dark green border</desc>
  <rect width="75" height="50" rx="20" ry="20" fill="#666" stroke="#229b23" stroke-fill="1" />
</svg>
```

## Figure

```html
<figure aria-labelledby="image-alt">
  <img src="" alt="" />
  <br />
  <figcaption id="image-alt"></figcaption>
</figure>
```

## Audio

- `src=""`
- `type=""`

## Form

### Input

- `label[for]` input.
- `aria-label` and `aria-describedby` for input hint.
- `aria-invalid` for error input.
- `aria-hidden` for hidden input.

```html
<form role="form">
  <label for="name">Name:</label>
  <input id="name" name="name" type="text" />
</form>
```

```html
<form role="form">
  <label for="name">Name:</label>
  <span class="prefix-input">
    <span class="prefix-icon" id="name-icon" aria-label="Input Prefix Icon">
      <icon />
    </span>
    <input id="name" name="name" type="text" aria-describedby="name-icon" />
  </span>
</form>
```

```html
<form role="form">
  <label for="email-address"> Your Email Address </label>
  <span id="email-error"> Error: Your email address must contain an @ symbol </span>
  <input id="email-address" name="email-address" type="email" aria-describedby="email-error" aria-invalid="true" />
</form>
```

```tsx
export default function Field() {
  return (
    <>
      <div className="user-code-field">
        <input
          id="userCode"
          aria-describedby={
            errors.userCode ? 'user-code-error' : 'user-code-help'
          }
        />
        <span id="user-code-help" className="user-code-help">
          Enter your 4 digit user code
        </span>
      </div>
      {errors.userCode && (
        <div id="user-code-error" role="alert" className="error">
          You must enter your 4 character user code
        </div>
      )}
    </>
  )
}
```

### Group

With `fieldset` and `legend`:

```html
<form role="form">
  <fieldset>
    <legend>Choose one of these three items:</legend>
    <input id="one" type="radio" name="items" value="one" />
    <label for="one">Choice One</label><br />
    <input id="two" type="radio" name="items" value="two" />
    <label for="two">Choice Two</label><br />
    <input id="three" type="radio" name="items" value="three" />
    <label for="three">Choice Three</label>
  </fieldset>
</form>
```

## Time

```html
<time datetime="2016-09-15">Thursday, September 15<sup>th</sup></time>
```

## Address

```html
<footer>
  <section class="contact" vocab="http://schema.org/" typeof="LocalBusiness">
    <h2>Contact us!</h2>
    <address property="email">
      <a href="mailto:us@example.com">us@example.com</a>
    </address>
    <address property="address" typeof="PostalAddress">
      <p property="streetAddress">123 Main St., Suite 404</p>
      <p>
        <span property="addressLocality">Your Town</span>, <span property="addressRegion">AK</span>,
        <span property="postalCode">12345</span>
      </p>
      <p property="addressCountry">United States of America</p>
    </address>
  </section>
</footer>
```

## Keys and Tabindex

```html
<a id="second" href="" accesskey="c"></a>
```

```ts
document.addEventListener('keyup', (event) => {
  switch (event.keyCode) {
    // escape
    case 27:
      // exit
      break
    // enter || space bar
    case 13 || 32:
      // submit or something
      break
    // left arrow
    case 37:
      // move back / previous
      break
    // right arrow
    case 39:
      // move forward
      break
    // up arrow
    case 38:
      // move up
      break
    // down arrow
    case 40:
      // move down
      break
    default:
      throw new Error('Unsupported key!')
  }
})
```

```ts
/**
 * Traps the tab key inside of the context, so the user can't accidentally get
 * stuck behind it.
 *
 * Note that this does not work for VoiceOver users who are navigating with
 * the VoiceOver commands, only for default tab actions. We would need to
 * implement something like the inert attribute for that (see https://github.com/WICG/inert)
 * @param  {object} e the Event object
 */
export function trapTabKey(e, context) {
  if (e.key !== 'Tab')
    return

  const focusableItems = getFocusable(context)
  const focusedItem = document.activeElement

  const focusedItemIndex = focusableItems.indexOf(focusedItem)

  if (e.shiftKey) {
    if (focusedItemIndex === 0) {
      focusableItems[focusableItems.length - 1].focus()
      e.preventDefault()
    }
  } else {
    if (focusedItemIndex === focusableItems.length - 1) {
      focusableItems[0].focus()
      e.preventDefault()
    }
  }
}
```

## Self-Closing Tags

Self-closing tags (`<tag />`) **do not exist** in HTML.
If a trailing / (slash) character is present in the start tag of an HTML element,
HTML parsers ignore that slash character:

```xml
<div>This text is inside the div.</div>
<div />This text is inside the div.

<input />This text is outside the input.
<input>This text is outside the input.</input>
```

Further reading:

- Void elements in [HTML](https://developer.mozilla.org/en-US/docs/Glossary/Void_element).
- Self-closing tags in [JSX](https://jakearchibald.com/2023/against-self-closing-tags-in-html).
- Self-closing tags in [Svelte](https://github.com/sveltejs/svelte/issues/11052).

## ARIA

[Web Accessibility Initiative - Accessible Rich Internet Applications](https://www.w3.org/WAI/intro/aria):

- `aria-label`.
- `aria-labelledby="dropdownMenuButton"`: dropdown/form>.
- `aria-describedBy`: input + small.

```html
<label id="l1" for="f3">label text</label>
<input type="text" id="f3" aria-labelledby="l1 l2" />
<p>other content</p>
<span tabindex="-1" id="l2">more label text</span>

<div aria-describedby="test">text</div>
<div id="test" role="tooltip">tooltip text</div>

<div role="dialog" aria-label="login" aria-describedby="log1">
  <div id="log1" tabindex="-1">Provide user name and password to login.</div>
</div>
```

- `aria-disabled="true"`: disable element.
- `aria-hidden="true"`.
- `aria-controls="navbarSupportedContent"`: navigation/select.
- `aria-expanded="false"`: dropdown.
- `aria-haspopup="true"`: dropdown/popup.
- `aria-current="pages`: breadcrumb.
- `aria-valuenow`/`aria-valuemin`/`aria-valuemax`: progress.
- [role](https://developer.mozilla.org/docs/Web/Accessibility/ARIA/Roles).
  - `<header>`: `role="banner"`.
  - `<nav>`: `role="navigation"`.
  - `<main>`: `role="main"`.
  - `<section>`: `role="region"`.
  - `<article>`: `role="article"`.
  - `<aside>`: `role="complementary"`.
  - `<footer>`: `role="contentinfo"`.
  - `<form>`: `role="form"`.
  - 7th heading level: `<div role="heading" aria-level="7"></div>`.
  - `role="button"`.
  - `role="checkbox"`.
  - `role="gridcell"`.
  - `role="link"`.
  - `role="menuitem"`.
  - `role="menuitemcheckbox"`.
  - `role="menuitemradio"`.
  - `role="option"`.
  - `role="progressbar"`.
  - `role="radio"`.
  - `role="scrollbar"`.
  - `role="searchbox"`.
  - `role="separator (when focusable)"`.
  - `role="slider"`.
  - `role="spinbutton"`.
  - `role="switch"`.
  - `role="tab"`.
  - `role="tabpanel"`.
  - `role="textbox"`.
  - `role="tooltip"`.
  - `role="treeitem"`.
  - `role="presentation"`: removes the semantics of an element.
    If set an interactive or focusable element to `role="presentation"`,
    assistive technology user will not know what it is or how to use it.
  - `role="application"`.

```html
<button class="list-expander" aria-expanded="false" aria-controls="expandable-list-1">Expand List</button>
<ul id="expandable-list-1">
  <li><a href="http://example.com">Sample Link</a></li>
  <li><a href="http://example.com">Sample Link 2</a></li>
  <li><a href="http://example.com">Sample Link 3</a></li>
</ul>
```

```ts
const listExpander = document.querySelector('.list-expander')
const list = document.querySelector('#expandable-list-1')

listExpander.addEventListener('click', (e) => {
  if (list.getAttribute('aria-expanded') === 'true')
    list.setAttribute('aria-expanded', 'false')
  else
    list.setAttribute('aria-expanded', 'true')
})
```

### Dialog

```html
<div id="dialog_layer" class="dialogs">
  <div
    id="dialog1"
    role="dialog"
    aria-labelledby="dialog1_label"
    aria-describedby="dialog1_desc"
    aria-modal="true"
    class="hidden"
  >
    <h2 id="dialog1_label" class="dialog_label">Address Added</h2>
    <p id="dialog1_desc" class="dialog_desc">
      The address you provided has been added to your list of delivery addresses. It is ready for immediate use. If you
      wish to remove it, you can do so from
      <a href="#" onclick="openDialog('dialog2', this)"> your profile. </a>
    </p>
    <div class="dialog_form_actions">
      <button type="button" id="dialog1_close_btn" onclick="closeDialog(this)">OK</button>
    </div>
  </div>
  <div
    id="dialog2"
    role="dialog"
    aria-labelledby="dialog2_label"
    aria-describedby="dialog2_desc"
    aria-modal="true"
    class="hidden"
  >
    <h2 id="dialog2_label" class="dialog_label">End of the Road!</h2>
    <p id="dialog2_desc" class="dialog_desc">
      You activated a fake link or button that goes nowhere! The link or button is present for demonstration purposes
      only.
    </p>
    <div class="dialog_form_actions">
      <button type="button" id="dialog2_close_btn" onclick="closeDialog(this)">Close</button>
    </div>
  </div>
</div>
```

## Best Practices

A11y audit [list](https://web.dev/lighthouse-accessibility):

- Keyboard-only navigation.
- Voice control.
- Screen reader.
- High contrast mode.
- Dark mode.
- Browser zoom.
- Don't use `aria-hidden` on the `<body>` element.
- Complete meta header:
  - Add missing languages.
  - Make sure `document` has a `title` element.
  - Tool: `react-helmet`.
- Fix low text contrast:
  确保文本与其背景保持足够的对比.
- 不要将颜色作为传达信息的唯一手段 (色盲/弱).
- Add missing alternative text.
- Remove empty links and buttons.
- 注意表单:
  - Add missing labels.
  - 提供输入焦点的视觉提示.
- 避免组件识别障碍.
- Make sure `IDs` and `Keys` of elements are unique.
- Required [context role](https://web.dev/aria-required-parent).
- Required [aria attribute](https://web.dev/aria-required-attr).
- Valid [aria attribute](https://web.dev/aria-valid-attr-value).

## Checklist

- [W3C ARIA Usage Rule](https://www.w3.org/TR/aria-in-html)
- [WebAIM WCAG (Web Content Accessibility Guidelines) 2 Checklist](https://webaim.org/standards/wcag/checklist)
- [A11Y Project](https://github.com/a11yproject/a11yproject.com)
- [A11Y 101](https://dev.to/inhuofficial/101-digital-accessibility-tips-and-tricks-4728)

## Library

- [LightHouse](https://github.com/GoogleChrome/lighthouse)
- [AXE DevTools](https://github.com/dequelabs/axe-core)
- [ESLint JSX A11Y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y)
- [React A11Y](https://github.com/reactjs/react-a11y)

## References

- `WAI-ARIA`: improve website’s accessibility with [`WAI-ARIA`](https://www.sitepoint.com/wai-aria).
- `ARIA`: W3C [official examples](https://github.com/w3c/aria-practices).
- `WebAIM`: Million [project](https://webaim.org/projects/million).
- A11y community: learn about [a11y](https://www.ta11y.org/learning).
- Cognitive a11y [resources](https://stephaniewalter.design/blog/neurodiversity-and-ux-essential-resources-for-cognitive-accessibility).
- A11y for [PDF](https://blog.pope.tech/2024/04/25/how-to-check-and-fix-pdf-accessibility-issues).
- A11y for [design system](https://cerovac.com/a11y/2024/04/make-sure-that-your-design-system-is-accessible).
- Assistive technology for [readers](https://veroniiiica.com/a-to-z-of-assistive-technology-for-reading).
