
Accessibility Guide
- 4 installs
- 231 repo stars
- Updated August 3, 2026
- sap/fundamental-styles
Accessibility guidelines for writing fundamental-styles component examples with semantic HTML, proper ARIA, and WCAG compliance.
About
Provides accessibility rules for authoring and reviewing HTML component examples in SAP's fundamental-styles library. A developer uses it when writing component demos or fixing accessibility issues in examples.
- Prefers semantic HTML over ARIA roles
- Covers WCAG-compliant ARIA usage for component demos
Accessibility Guide by the numbers
- 4 all-time installs (skills.sh)
- +2 installs in the week ending Jul 19, 2026 (Skillselion tracking)
- Ranked #1,817 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/sap/fundamental-styles --skill accessibility-guideAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 231 |
| Last updated | August 3, 2026 |
| Repository | sap/fundamental-styles ↗ |
What it does
Accessibility guidelines for writing fundamental-styles component examples with semantic HTML, proper ARIA, and WCAG compliance.
Files
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" />
<!-- No for/id connection -->❌ DON'T - Non-existent ID reference:
<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:
<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">
<!-- ID 'missing-title' doesn't exist -->
</div>Validation checklist:
- [ ] Every
forattribute points to an existingid - [ ] Every
aria-labelledbyreferences existing ID(s) - [ ] Every
aria-describedbyreferences existing ID(s) - [ ] Every
aria-controlsreferences 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 valuearia-valuemin- Minimum valuearia-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>
<!-- 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-checkedfor checkbox, radio, switch, menuitemcheckbox, menuitemradioaria-selectedfor 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>
<!-- 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:
<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>
<!-- Missing aria-expanded -->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:
<!-- 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:
<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→ implicitaria-orientation="vertical"menubar→ implicitaria-orientation="horizontal"slider→ implicitaria-orientation="horizontal"tablist→ implicitaria-orientation="horizontal"
✅ DO - Override when different from default:
<!-- 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"(oraria-modal="false"for non-modal)aria-labelledbyORaria-labelto 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>
<!-- 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:
<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:
<!-- 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 hastabindex="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:
<!-- Tab without tablist container -->
<button role="tab">Electronics</button>
<!-- Tablist without tabs -->
<div role="tablist">
<button>Not a tab</button>
</div>
<!-- Tab not controlling any panel -->
<button role="tab" aria-selected="true">Tab 1</button>
<!-- No aria-controls pointing to tabpanel -->Rules:
- Each
tabshould havearia-controlspointing to itstabpanel - Each
tabpanelshould havearia-labelledbypointing to itstab - Only ONE tab has
aria-selected="true"at a time - Selected tab has
tabindex="0", others havetabindex="-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>
<!-- Missing role="menuitem" -->
</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:
forandidconnect label to inputaria-invalid="true"indicates error statearia-describedbypoints to error message IDrequiredattribute 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>
<!-- No aria-label, no visible text -->
</button>Rules:
- Icon-only buttons MUST have
aria-labeloraria-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" hidden>
<li role="option" aria-selected="false">United States</li>
<li role="option" aria-selected="false">Canada</li>
</ul>
</div>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 class="fd-checkbox__label" for="updates">
<span class="fd-checkbox__checkmark" aria-hidden="true"></span>
<span class="fd-checkbox__text">Receive product updates</span>
</label>
</div>
</fieldset>Rules:
- Use
<fieldset>and<legend>to group related checkboxes - Each checkbox has unique
idand matchingforattribute - 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">
<button class="fd-button fd-button--emphasized">Confirm</button>
<button class="fd-button fd-button--transparent">Cancel</button>
</footer>
</div>
</div>Key attributes:
role="dialog"witharia-modal="true"aria-labelledbypointing 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>
</li>
</ul>
<div class="fd-tabs__panel" role="tabpanel" id="tab-panel-1" aria-labelledby="tab-1">Overview content</div>
<div class="fd-tabs__panel" role="tabpanel" id="tab-panel-2" aria-labelledby="tab-2" hidden>
Specifications content
</div>
</div>Key attributes:
- Tablist has
aria-labeloraria-labelledby - Each tab has
aria-selected,aria-controls, uniqueid - Selected tab has
tabindex="0", others havetabindex="-1" - Each tabpanel has
idandaria-labelledbypointing to its tab - Inactive panels have
hiddenattribute
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 hasaria-live="assertive"andaria-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>witharia-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 | <kbd>Enter</kbd>, <kbd>Space</kbd> | Activate |
| Link | <kbd>Enter</kbd> | Follow link |
| Checkbox | <kbd>Space</kbd> | Toggle |
| Radio | <kbd>Arrow keys</kbd> | Navigate between options |
| Tabs | <kbd>Arrow keys</kbd> | Navigate tabs, <kbd>Tab</kbd> moves to panel |
| Menu | <kbd>Arrow keys</kbd> | Navigate, <kbd>Enter</kbd> activate |
| Dialog | <kbd>Esc</kbd> | Close |
| Combobox | <kbd>Arrow keys</kbd> | Navigate options, <kbd>Enter</kbd> select |
| Slider | <kbd>Arrow keys</kbd> | Adjust value |
Focus Management
✅ DO:
<!-- Ensure interactive elements are focusable -->
<div role="button" tabindex="0" onclick="...">Click me</div>
<!-- Move focus to dialog when opened -->
<div role="dialog" aria-modal="true" tabindex="-1">...</div>
<!-- Manage tab order in roving tabindex pattern -->
<div role="radiogroup">
<div role="radio" tabindex="0">Option 1</div>
<!-- Selected -->
<div role="radio" tabindex="-1">Option 2</div>
<!-- Not selected -->
</div>❌ DON'T:
<!-- Missing tabindex on interactive role -->
<div role="button">Click me</div>
<!-- Not keyboard accessible -->
<!-- Using positive tabindex values -->
<button tabindex="1">Bad practice</button>Rules:
- Interactive custom elements MUST have
tabindex="0"ortabindex="-1" - Use
tabindex="-1"for programmatically focusable elements - Never use positive
tabindexvalues (1, 2, 3, etc.)
Testing Checklist
When creating component examples, verify:
Semantic Structure
- [ ] Uses native HTML elements when possible
- [ ] Proper heading hierarchy (h1 → h2 → h3)
- [ ] Lists use
<ul>,<ol>, or<dl> - [ ] Tables use
<table>with proper headers
Labels & Names
- [ ] All form inputs have associated labels
- [ ]
forattributes point to existingidvalues - [ ] Icon-only buttons have
aria-label - [ ] Regions/landmarks have labels when multiple exist
ARIA Roles & Attributes
- [ ] Required companion attributes are present
- [ ] All ID references (
aria-labelledby,aria-controls, etc.) point to existing elements - [ ] Composite widgets contain required child roles
- [ ] State attributes reflect current state (
aria-expanded,aria-checked,aria-selected)
Keyboard & Focus
- [ ] All interactive elements are keyboard accessible
- [ ] Custom interactive roles have appropriate
tabindex - [ ] Focus is visible
- [ ] Tab order is logical
Screen Reader Testing
- [ ] Test with NVDA (Windows), JAWS (Windows), or VoiceOver (Mac/iOS)
- [ ] Verify accessible names are announced correctly
- [ ] Verify state changes are announced
- [ ] Verify alerts/live regions are announced
Tools
- Use browser DevTools Accessibility inspector
- Run axe DevTools or WAVE extensions
- Validate with W3C HTML validator
Resources
- ARIA Authoring Practices: https://www.w3.org/WAI/ARIA/apg/
- MDN ARIA Reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA
- WCAG 2.1 Guidelines: https://www.w3.org/WAI/WCAG21/quickref/
- WebAIM Articles: https://webaim.org/articles/
Quick Reference
Most Common Mistakes
1. ❌ Icon-only button without label 2. ❌ Form input without label connection 3. ❌ aria-labelledby pointing to non-existent ID 4. ❌ Role without required attributes (e.g., slider without values) 5. ❌ Custom widget without keyboard support 6. ❌ Using ARIA when native HTML would work 7. ❌ Missing aria-expanded on expandable widgets 8. ❌ Tab interface without aria-selected 9. ❌ Dialog without aria-modal and label 10. ❌ 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.