| 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:
<button>Click me</button>
<nav>...</nav>
<header>...</header>
<input type="checkbox" />
❌ DON'T:
<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:
<label for="username">Username:</label> <input id="username" type="text" name="username" />
✅ DO - Nested label (alternative):
<label>
Username:
<input type="text" name="username" />
</label>
✅ DO - Using aria-labelledby for complex labels:
<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:
<label>Username</label>
<input type="text" />
❌ DON'T - Non-existent ID reference:
<label for="nonexistent">Username</label>
<input id="username" type="text" />
3. ID References Must Be Valid
When using aria-labelledby, aria-describedby, or for attributes, the referenced IDs must exist in the DOM.
✅ DO:
<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:
<div role="dialog" aria-labelledby="missing-title">
</div>
Validation checklist:
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:
<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:
<div role="slider" aria-label="Volume" tabindex="0"></div>
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:
<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:
<div role="checkbox" aria-label="Subscribe" tabindex="0"></div>
<button role="tab">Tab 1</button>
3. Expandable Widgets (combobox, disclosure, menu with submenus)
MUST include:
aria-expanded - "true" when expanded, "false" when collapsed
✅ DO:
<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:
<button aria-controls="submenu-1">File Menu</button>
4. Popup Triggers (combobox, menu button)
MUST include:
aria-haspopup - Type of popup ("menu", "listbox", "tree", "grid", "dialog", or "true")
✅ DO:
<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:
<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:
<li role="option" aria-posinset="3">Item 3</li>
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:
<div role="menu" aria-orientation="horizontal">
<button role="menuitem">File</button>
<button role="menuitem">Edit</button>
</div>
<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:
<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:
<div role="dialog">
<h2>Confirm Delete</h2>
</div>
Composite Widgets - Container Requirements
Some roles require specific child roles to be valid.
1. Radio Groups
radiogroup MUST contain radio elements.
✅ DO:
<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:
<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:
<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>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">Electronics content</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>Clothing content</div>
❌ DON'T:
<button role="tab">Electronics</button>
<div role="tablist">
<button>Not a tab</button>
</div>
<button role="tab" aria-selected="true">Tab 1</button>
Rules:
- Each
tab should have aria-controls pointing to its tabpanel
- Each
tabpanel should have aria-labelledby pointing to its tab
- Only ONE tab has
aria-selected="true" at a time
- Selected tab has
tabindex="0", others have tabindex="-1"
3. Menu / Menubar
menu or menubar MUST contain menuitem, menuitemcheckbox, or menuitemradio elements.
✅ DO:
<nav role="menubar" aria-label="Main navigation">
<button role="menuitem" aria-haspopup="menu" aria-expanded="false">File</button>
<button role="menuitem" aria-haspopup="menu" aria-expanded="false">Edit</button>
</nav>
<ul role="menu">
<li role="menuitem">New</li>
<li role="menuitem">Open</li>
<li role="separator"></li>
<li role="menuitemcheckbox" aria-checked="true">Show toolbar</li>
</ul>
❌ DON'T:
<div role="menu">
<button>New</button>
</div>
Note: role="separator" is allowed but doesn't count toward set size.
4. Listbox
listbox MUST contain option elements.
✅ DO:
<ul role="listbox" aria-label="Countries">
<li role="option" aria-selected="false">USA</li>
<li role="option" aria-selected="true">Canada</li>
<li role="option" aria-selected="false">Mexico</li>
</ul>
5. Tree
tree MUST contain treeitem elements.
✅ DO:
<ul role="tree" aria-label="File explorer">
<li role="treeitem" aria-expanded="true" aria-level="1">
Documents
<ul role="group">
<li role="treeitem" aria-level="2">File1.txt</li>
<li role="treeitem" aria-level="2">File2.txt</li>
</ul>
</li>
</ul>
Required for parent treeitems:
aria-expanded - Whether children are visible
Recommended:
aria-level - Depth in hierarchy (1-indexed)
6. Grid / Table
grid or table MUST contain row elements, which contain gridcell or cell elements.
✅ DO:
<div role="grid" aria-label="Spreadsheet">
<div role="row">
<div role="columnheader">Name</div>
<div role="columnheader">Age</div>
</div>
<div role="row">
<div role="gridcell">John</div>
<div role="gridcell">30</div>
</div>
</div>
Prefer native HTML:
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td>
<td>30</td>
</tr>
</tbody>
</table>
Common Component Patterns
Pattern 1: Form with Label and Error Message
✅ DO:
<div class="fd-form-item">
<label class="fd-form-label fd-form-label--required" for="email"> Email </label>
<input
id="email"
class="fd-input is-error"
type="email"
aria-invalid="true"
aria-describedby="email-error"
required
/>
<span id="email-error" class="fd-form-message fd-form-message--error"> Please enter a valid email address </span>
</div>
Key attributes:
for and id connect label to input
aria-invalid="true" indicates error state
aria-describedby points to error message ID
required attribute for validation
Pattern 2: Button with Icon
✅ DO - Icon is decorative:
<button class="fd-button" aria-label="Close">
<span class="sap-icon--decline" aria-hidden="true"></span>
</button>
✅ DO - Icon with text:
<button class="fd-button">
<span class="sap-icon--save" aria-hidden="true"></span>
Save
</button>
❌ DON'T - Missing label on icon-only button:
<button class="fd-button">
<span class="sap-icon--decline"></span>
</button>
Rules:
- Icon-only buttons MUST have
aria-label or aria-labelledby
- Decorative icons should have
aria-hidden="true"
- Never rely on icon alone for meaning
Pattern 3: Select / Dropdown
✅ DO - Native select:
<label for="country">Country:</label>
<select id="country" class="fd-select">
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
✅ DO - Custom select (combobox pattern):
<label for="country-input">Country:</label>
<div class="fd-select">
<div
class="fd-select__control"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-owns="country-listbox"
tabindex="0"
id="country-input"
>
<span class="fd-select__text-content">Select a country</span>
<button class="fd-button fd-button--transparent fd-select__button" aria-label="Show options" tabindex="-1">
<i class="sap-icon--slim-arrow-down"></i>
</button>
</div>
<ul role="listbox" id="country-listbox" class="fd-select__menu" >
United States
Canada
Key attributes for custom select:
- Combobox has
aria-expanded, aria-haspopup="listbox", aria-owns
- Listbox has matching
id
- Options have
aria-selected
- Button is
tabindex="-1" (only combobox is in tab order)
Pattern 4: Checkbox Group
✅ DO:
<fieldset class="fd-fieldset">
<legend class="fd-fieldset__legend">Preferences</legend>
<div class="fd-form-item">
<input id="newsletter" type="checkbox" class="fd-checkbox" />
<label class="fd-checkbox__label" for="newsletter">
<span class="fd-checkbox__checkmark" aria-hidden="true"></span>
<span class="fd-checkbox__text">Subscribe to newsletter</span>
</label>
</div>
<div class="fd-form-item">
<input id="updates" type="checkbox" class="fd-checkbox" />
<label = =>
Receive product updates
Rules:
- Use
<fieldset> and <legend> to group related checkboxes
- Each checkbox has unique
id and matching for attribute
- Decorative checkmark element has
aria-hidden="true"
Pattern 5: Dialog / Modal
✅ DO:
<div class="fd-dialog fd-dialog--active" role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<div class="fd-dialog__content">
<header class="fd-dialog__header">
<h2 id="dialog-title" class="fd-title fd-title--h5">Confirm Action</h2>
<button class="fd-button fd-button--transparent" aria-label="Close">
<span class="sap-icon--decline" aria-hidden="true"></span>
</button>
</header>
<div class="fd-dialog__body">
<p>Are you sure you want to proceed?</p>
</div>
<footer class="fd-dialog__footer fd-bar fd-bar--footer">
Confirm
Cancel
Key attributes:
role="dialog" with aria-modal="true"
aria-labelledby pointing to title ID
- Close button has
aria-label
- Focus should be trapped within dialog when active
- First focusable element should receive focus on open
Pattern 6: Tabs
✅ DO:
<div class="fd-tabs">
<ul class="fd-tabs__list" role="tablist" aria-label="Product sections">
<li class="fd-tabs__item">
<button
class="fd-tabs__link"
role="tab"
aria-selected="true"
aria-controls="tab-panel-1"
id="tab-1"
tabindex="0"
>
Overview
</button>
</li>
<li class="fd-tabs__item">
<button
class="fd-tabs__link"
role="tab"
aria-selected="false"
aria-controls="tab-panel-2"
id="tab-2"
tabindex="-1"
>
Specifications
</button>
</>
Overview content
Specifications content
Key attributes:
- Tablist has
aria-label or aria-labelledby
- Each tab has
aria-selected, aria-controls, unique id
- Selected tab has
tabindex="0", others have tabindex="-1"
- Each tabpanel has
id and aria-labelledby pointing to its tab
- Inactive panels have
hidden attribute
Pattern 7: Alert / Message Strip
✅ DO:
<div class="fd-message-strip fd-message-strip--error" role="alert">
<p class="fd-message-strip__text">There was an error processing your request.</p>
<button class="fd-button fd-button--transparent fd-message-strip__close" aria-label="Dismiss alert">
<span class="sap-icon--decline" aria-hidden="true"></span>
</button>
</div>
Key attributes:
role="alert" automatically has aria-live="assertive" and aria-atomic="true"
- Alerts are announced immediately by screen readers
- Close button has
aria-label
For status messages (less urgent):
<div class="fd-message-strip fd-message-strip--success" role="status">
<p class="fd-message-strip__text">Changes saved successfully.</p>
</div>
Pattern 8: Breadcrumb Navigation
✅ DO:
<nav aria-label="Breadcrumb">
<ol class="fd-breadcrumb">
<li class="fd-breadcrumb__item">
<a class="fd-breadcrumb__link" href="/">Home</a>
</li>
<li class="fd-breadcrumb__item">
<a class="fd-breadcrumb__link" href="/products">Products</a>
</li>
<li class="fd-breadcrumb__item">
<span class="fd-breadcrumb__link" aria-current="page"> Product Details </span>
</li>
</ol>
</nav>
Key attributes:
- Wrapped in
<nav> with aria-label="Breadcrumb"
- Current page has
aria-current="page"
- Use semantic list (
<ol>) for structure
Keyboard Interaction Requirements
Many ARIA roles require keyboard support. Document these in examples.
Common Keyboard Patterns
| Component | Required Keys | Action |
|---|
| Button | Enter, Space | Activate |
| Link | Enter | Follow link |
| Checkbox | Space | Toggle |
| Radio | Arrow keys | Navigate between options |
| Tabs | Arrow keys | Navigate tabs, Tab moves to panel |
| Menu | Arrow keys | Navigate, Enter activate |
| Dialog | Esc | Close |
| Combobox | Arrow keys | Navigate options, Enter select |
| Slider | Arrow keys | Adjust value |
Focus Management
✅ DO:
<div role="button" tabindex="0" onclick="...">Click me</div>
<div role="dialog" aria-modal="true" tabindex="-1">...</div>
<div role="radiogroup">
<div role="radio" tabindex="0">Option 1</div>
<div role="radio" tabindex="-1">Option 2</div>
</div>
❌ DON'T:
<div role="button">Click me</div>
<button tabindex="1">Bad practice</button>
Rules:
- Interactive custom elements MUST have
tabindex="0" or tabindex="-1"
- Use
tabindex="-1" for programmatically focusable elements
- Never use positive
tabindex values (1, 2, 3, etc.)
Testing Checklist
When creating component examples, verify:
Semantic Structure
Labels & Names
ARIA Roles & Attributes
Keyboard & Focus
Screen Reader Testing
Tools
- Use browser DevTools Accessibility inspector
- Run axe DevTools or WAVE extensions
- Validate with W3C HTML validator
Resources
Quick Reference
Most Common Mistakes
- ❌ Icon-only button without label
- ❌ Form input without label connection
- ❌
aria-labelledby pointing to non-existent ID
- ❌ Role without required attributes (e.g., slider without values)
- ❌ Custom widget without keyboard support
- ❌ Using ARIA when native HTML would work
- ❌ Missing
aria-expanded on expandable widgets
- ❌ Tab interface without
aria-selected
- ❌ Dialog without
aria-modal and label
- ❌ Using
aria-posinset without aria-setsize
Remember: The first rule of ARIA is to use native HTML when possible. Only use ARIA roles and attributes when HTML semantics are insufficient.