Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
sap avatar

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-guide

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs4
repo stars231
Last updatedAugust 3, 2026
Repositorysap/fundamental-styles

What it does

Accessibility guidelines for writing fundamental-styles component examples with semantic HTML, proper ARIA, and WCAG compliance.

Files

SKILL.mdMarkdownGitHub ↗

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 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:

<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-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>
<!-- 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 → 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:

<!-- 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:

<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 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:

<!-- 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 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>
    <!-- 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:

  • 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>
    <!-- No aria-label, no visible text -->
</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" 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 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">
            <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" 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>
        </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-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

ComponentRequired KeysAction
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" 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

  • [ ] 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
  • [ ] for attributes point to existing id values
  • [ ] 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.

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.