- name
- accessibility-guide
- description
- Accessibility guidelines for writing component examples in fundamental-styles with proper ARIA, semantic HTML, and WCAG compliance
# Accessibility Guide for fundamental-styles
**When to use this skill:**
- Writing HTML examples for component documentation
- Creating new component demos
- Reviewing component accessibility
- Fixing accessibility issues in examples
- Questions about ARIA roles and attributes
## Core Principles
### 1. Semantic HTML First
Always prefer native HTML elements over ARIA roles when possible.
✅ **DO:**
```html
<button>Click me</button>
<nav>...</nav>
<header>...</header>
<input type="checkbox" />
```
❌ **DON'T:**
```html
<div role="button">Click me</div>
<div role="navigation">...</div>
<div role="banner">...</div>
<div role="checkbox"></div>
```
**Why:** Native elements provide built-in keyboard support, focus management, and semantics without JavaScript.
### 2. Labels MUST Be Properly Connected
Every form control must have an accessible label.
✅ **DO - Use `for` and `id`:**
```html
<label for="username">Username:</label> <input id="username" type="text" name="username" />
```
✅ **DO - Nested label (alternative):**
```html
<label>
Username:
<input type="text" name="username" />
</label>
```
✅ **DO - Using `aria-labelledby` for complex labels:**
```html
<span id="label-firstname">First Name</span>
<span id="label-required">(required)</span>
<input type="text" aria-labelledby="label-firstname label-required" />
```
❌ **DON'T - Missing connection:**
```html
<label>Username</label>
<input type="text" />
<!-- No for/id connection -->
```
❌ **DON'T - Non-existent ID reference:**
```html
<label for="nonexistent">Username</label>
<input id="username" type="text" />
<!-- Mismatched IDs -->
```
### 3. ID References Must Be Valid
When using `aria-labelledby`, `aria-describedby`, or `for` attributes, the referenced IDs **must exist in the DOM**.
✅ **DO:**
```html
<h2 id="dialog-title">Confirm Action</h2>
<div role="dialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
<p id="dialog-desc">Are you sure you want to proceed?</p>
<button>Confirm</button>
</div>
```
❌ **DON'T:**
```html
<div role="dialog" aria-labelledby="missing-title">
<!-- ID 'missing-title' doesn't exist -->
</div>
```
**Validation checklist:**
- [ ] Every `for` attribute points to an existing `id`
- [ ] Every `aria-labelledby` references existing ID(s)
- [ ] Every `aria-describedby` references existing ID(s)
- [ ] Every `aria-controls` references existing ID(s)
- [ ] IDs are unique within the page
## ARIA Role Requirements
### Roles That Require Companion Attributes
Some ARIA roles **only work when paired with specific attributes**. Missing these makes the component inaccessible.
#### 1. Range Widgets (slider, spinbutton, progressbar, scrollbar)
**MUST include:**
- `aria-valuenow` - Current value
- `aria-valuemin` - Minimum value
- `aria-valuemax` - Maximum value
✅ **DO:**
```html
<div role="slider" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" aria-label="Volume" tabindex="0"></div>
<div role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress">60%</div>
```
❌ **DON'T:**
```html
<div role="slider" aria-label="Volume" tabindex="0"></div>
<!-- Missing aria-valuenow, aria-valuemin, aria-valuemax -->
```
**Optional but recommended:**
- `aria-valuetext` - Human-readable value (e.g., "Small", "50%", "$1,000")
#### 2. Checked/Selectable Widgets (checkbox, radio, switch, option, menuitemcheckbox, menuitemradio, tab)
**MUST include:**
- `aria-checked` for checkbox, radio, switch, menuitemcheckbox, menuitemradio
- `aria-selected` for option, tab
✅ **DO:**
```html
<div role="checkbox" aria-checked="false" aria-label="Subscribe to newsletter" tabindex="0"></div>
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">Tab 1</button>
<div role="option" aria-selected="false">Option 1</div>
```
❌ **DON'T:**
```html
<div role="checkbox" aria-label="Subscribe" tabindex="0"></div>
<!-- Missing aria-checked -->
<button role="tab">Tab 1</button>
<!-- Missing aria-selected -->
```
#### 3. Expandable Widgets (combobox, disclosure, menu with submenus)
**MUST include:**
- `aria-expanded` - "true" when expanded, "false" when collapsed
✅ **DO:**
```html
<button aria-expanded="false" aria-controls="submenu-1">File Menu</button>
<ul id="submenu-1" hidden>
<li>New</li>
<li>Open</li>
</ul>
<div role="combobox" aria-expanded="false" aria-haspopup="listbox" aria-owns="listbox-1">
<input type="text" />
<ul role="listbox" id="listbox-1" hidden>
...
</ul>
</div>
```
❌ **DON'T:**
```html
<button aria-controls="submenu-1">File Menu</button>
<!-- Missing aria-expanded -->
```
#### 4. Popup Triggers (combobox, menu button)
**MUST include:**
- `aria-haspopup` - Type of popup ("menu", "listbox", "tree", "grid", "dialog", or "true")
✅ **DO:**
```html
<button aria-haspopup="menu" aria-expanded="false">Options</button>
<div role="combobox" aria-haspopup="listbox" aria-expanded="false">
<input type="text" />
</div>
```
#### 5. Set Position Attributes (aria-posinset, aria-setsize)
**These MUST be used together** when not all items are present in the DOM (e.g., virtual scrolling, lazy loading).
✅ **DO:**
```html
<!-- Showing items 1, 3, 11, 19 out of 118 total -->
<ul role="listbox">
<li role="option" aria-setsize="118" aria-posinset="1">Item 1</li>
<li role="option" aria-setsize="118" aria-posinset="3">Item 3</li>
<li role="option" aria-setsize="118" aria-posinset="11">Item 11</li>
<li role="option" aria-setsize="118" aria-posinset="19">Item 19</li>
</ul>
```
❌ **DON'T:**
```html
<li role="option" aria-posinset="3">Item 3</li>
<!-- Missing aria-setsize -->
```
**When to omit:**
- When all items are in the DOM (browser can calculate automatically)
- Set `aria-setsize="-1"` if total size is unknown
#### 6. Set Orientation (menu, menubar, toolbar, listbox, slider, tablist, tree)
Some roles have implicit orientation. Override when needed.
**Defaults:**
- `menu` → implicit `aria-orientation="vertical"`
- `menubar` → implicit `aria-orientation="horizontal"`
- `slider` → implicit `aria-orientation="horizontal"`
- `tablist` → implicit `aria-orientation="horizontal"`
✅ **DO - Override when different from default:**
```html
<!-- Horizontal menu needs explicit orientation -->
<div role="menu" aria-orientation="horizontal">
<button role="menuitem">File</button>
<button role="menuitem">Edit</button>
</div>
<!-- Vertical tabs need explicit orientation -->
<div role="tablist" aria-orientation="vertical">
<button role="tab">Tab 1</button>
<button role="tab">Tab 2</button>
</div>
```
#### 7. Modal Dialogs
**MUST include:**
- `aria-modal="true"` (or `aria-modal="false"` for non-modal)
- `aria-labelledby` OR `aria-label` to name the dialog
✅ **DO:**
```html
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm Delete</h2>
<p>Are you sure?</p>
<button>Cancel</button>
<button>Delete</button>
</div>
```
❌ **DON'T:**
```html
<div role="dialog">
<h2>Confirm Delete</h2>
<!-- Missing aria-modal and aria-labelledby -->
</div>
```
### Composite Widgets - Container Requirements
Some roles **require specific child roles** to be valid.
#### 1. Radio Groups
`radiogroup` MUST contain `radio` elements.
✅ **DO:**
```html
<div role="radiogroup" aria-labelledby="group-label">
<span id="group-label">Select size:</span>
<div role="radio" aria-checked="false" tabindex="0">Small</div>
<div role="radio" aria-checked="true" tabindex="0">Medium</div>
<div role="radio" aria-checked="false" tabindex="0">Large</div>
</div>
```
❌ **DON'T:**
```html
<!-- Isolated radio without radiogroup -->
<div role="radio" aria-checked="false">Small</div>
<div role="radio" aria-checked="false">Medium</div>
```
**Rules:**
- Only ONE radio can have `aria-checked="true"` within a radiogroup
- All radios in group must have `tabindex` (typically only checked one has `tabindex="0"`)
#### 2. Tab Interface
`tablist` MUST contain `tab` elements. Each `tab` should control a `tabpanel`.
✅ **DO:**
```html
<div role="tablist" aria-label="Product categories">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">Electronics</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">Clothing</button>
</div>
عرض على GitHub