design-innovations / bones

Accessible HTML code patterns for common UI widgets such as tabs, menus, dialogs, etc.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

BONES

Widgets are stronger with bones. Does your widget have bones?

Contents

  1. Introduction
  2. Accordion
  3. Alert Dialog
  4. Breadcrumbs
  5. Carousel
  6. Checkbox
  7. Combobox
  8. Details
  9. Dialog
  10. Fake Menu
  11. Fake Tabs
  12. Flyout
  13. Input Validation
  14. Listbox
  15. Listbox Button
  16. Menu
  17. Menu Button
  18. Page Notice
  19. Pagination
  20. Radio
  21. Switch
  22. Tabs
  23. Tooltip

Introduction

Bones provides lean, mean, semantic HTML markup for widgets; ensuring maximum Accessibility, SEO and Site Speed performance. Bones markup uses ARIA only where strictly WAI-ARIA necessary.

Bones advocates the Progressive Enhancement web-design strategy; building the web in a layered fashion that allows everyone to access the most important content and functionality.

Bones favors the the BEM (block, element, modifier) methodology for naming its classes.

The accordion is simply a list of details widgets. Each details summary should include a relevant heading level tag.

<ul class="accordion" role="list" aria-roledescription="accordion">
    <li>
        <details class="details">
            <summary><h3>Panel 1</h3></summary>
            <ul>
                <li><a href="http://www.ebay.com">Item 1</a></li>
                <li><a href="http://www.ebay.com">Item 2</a></li>
                <li><a href="http://www.ebay.com">Item 3</a></li>
            </ul>
        </details>
    </li>
    <li>
        <details class="details">
            <summary><h3>Panel 2</h3></summary>
            <ul>
                <li><a href="http://www.ebay.com">Item 1</a></li>
                <li><a href="http://www.ebay.com">Item 2</a></li>
                <li><a href="http://www.ebay.com">Item 3</a></li>
            </ul>
        </details>
    </li>
</ul>
<div class="dialog" role="alertdialog" aria-labelledby="dialog-title" aria-modal="true">
    <div class="dialog__window">
        <div class="dialog__header">
            <h2 class="dialog__title" id="dialog-title">Alert Dialog Title</h2>
        </div>
        <div class="dialog__main">
            <!-- dialog content goes here -->
        </div>
        <div class="dialog__footer">
            <button class="dialog__close" type="button">Close</button>
        </div>
    </div>
</div>

An alert dialog should visibly and programmatically obscure the content in the main window underneath. Therefore it should be coded as a modal alert dialog, using the aria-modal property.

The content is an ordered list of links inside a navigation landmark region.

<nav class="crumbs" aria-labelledby="crumbs_heading" role="navigation">
    <h2 id="crumbs_heading">You are here</h2>
    <ol>
        <li><a href="http://www.ebay.com">Great Grandparent Page</a></li>
        <li><a href="http://www.ebay.com">Grandparent Page</a></li>
        <li><a href="http://www.ebay.com">Parent Page</a></li>
        <li><a aria-current="location">Current Page</a></li>
    </ol>
</nav>

CSS can be used to generate a separator image or glyph using the ::after pseudo element.

A carousel is a list of items. These items can contain anything - text, images, links, tiles, cards, etc. - but we advise against using complex widgets. The usual accessibility rules apply to the contents of these items (tab-order, semantics, etc).

For the server rendered markup you can choose to render as little or as much of the full list as you wish. Client-side script can lazy-load in additional items as desired.

<div aria-labelledby="carousel-title" aria-roledescription="carousel" class="carousel" role="group">
    <span aria-live="polite" role="status">
        <h2 class="carousel__title" id="carousel-title">
            <span>Title</span>
            <span class="clipped"> - slide n of n</span>
        </h2>
    </span>
    <button aria-disabled="false" aria-label="Previous slide - Title"></button>
    <ul>
        <li aria-hidden="false">...</li>
        <li aria-hidden="false">...</li>
        <li aria-hidden="false">...</li>
        ...
    </ul>
    <button aria-disabled="false" aria-label="Next slide - Title"></button>
</div>

JavaScript must maintain the tabindex and aria-hidden state of items as they scroll in and out of view. Items that are not in view need a tabindex value of "-1" an aria-hidden value of "true" to ensure items are hidden from keyboard users and screen reader users respectively.

JavaScript must also maintain the state of the heading inside the live region.

For small touch screens, you may wish to utilise swipe gestures. In which case pagination buttons can be hidden offscreen, appearing only on keyboard focus (i.e. 'stealth' buttons) for keyboard users.

Native HTML checkboxes are 100% accessible by default. To ensure correct grouping and group label semantics, groups of checkboxes should always be placed inside of a fieldset with legend.

<fieldset>
    <legend>Auction Type</legend>
    <div>
        <input id="freeshipping" type="checkbox" name="freeshipping" />
        <label for="freeshipping">Free Shipping</label>
    </div>
    <div>
        <input id="endssoon" type="checkbox" name="endssoon" />
        <label for="endssoon">Ends soon</label>
    </div>
    <div>
        <input id="zerobids" type="checkbox" name="zerobids" />
        <label for="zerobids">Zero bids</label>
    </div>
</fieldset>

For vertically stacked checkboxes, simply switch the spans to divs.

Custom Checkbox Icon

To create a custom checkbox style, a foreground SVG can be used as a facade over the real checkbox.

<span class="checkbox">
  <input class="checkbox__control" id="freeshipping" type="checkbox" name="freeshipping" />
  <span class="checkbox__icon" hidden>
    <svg aria-hidden="true" class="checkbox__unchecked" focusable="false">
      <use xlink:href="#icon-checkbox-unchecked"></use>
    </svg>
    <svg aria-hidden="true" class="checkbox__checked" focusable="false">
      <use xlink:href="#icon-checkbox-checked"></use>
    </svg>
  </span>
</span>

This markup assumes that the symbol definitions for #icon-checkbox-unchecked and #icon-checkbox-checked exist on the page. The hidden property ensures that the SVG icon is not visible alongside the native icon when the page is in a non-CSS state. This hidden property should be overriden by CSS.

Don't forget to add a label for the checkbox!

Collapsed state:

<div class="combobox" id="combobox-1">
    <span class="combobox__control">
        <input name="combobox-1-name" type="text" role="combobox" autocomplete="off" aria-expanded="false" aria-owns="combobox-1-listbox" />
    </span>
    <ul id="combobox-1-listbox" role="listbox">
        <li role="option" id="combobox-1-option-1">Option 1</li>
        <li role="option" id="combobox-1-option-2">Option 2</li>
        <li role="option" id="combobox-1-option-3">Option 3</li>
        ...
    </ul>
</div>

Expanded state:

<div class="combobox combobox--expanded" id="combobox-1">
    <span class="combobox__control">
        <input name="combobox-1-name" type="text" role="combobox" autocomplete="off" aria-expanded="true" aria-owns="combobox-1-listbox" />
    </span>
    <ul id="combobox-1-listbox" role="listbox">
        <li role="option" id="combobox-1-option-1">Option 1</li>
        <li role="option" id="combobox-1-option-2">Option 2</li>
        <li role="option" id="combobox-1-option-3">Option 3</li>
        ...
    </ul>
</div>

JavaScript must update the aria-activedescendant attribute on the textbox to reflect the state of the currently active descendant listbox item.

Optionally, a separate button can be added to manually expand and collapse the combobox. Typically though, the combobox will automatically expand and collapse on focus and blur, meaning any such button element is redundant.

Uses the native HTML <details> tag. IE and Edge browsers require a CSS and JavaScript polyfill.

<details class="details">
    <summary>Details</summary>
    <ul>
        <li><a href="http://www.ebay.com">Link 1</a></li>
        <li><a href="http://www.ebay.com">Link 2</a></li>
        <li><a href="http://www.ebay.com">Link 3</a></li>
    </ul>
</details>
<div class="dialog" role="dialog" aria-labelledby="dialog-title" aria-modal="true">
    <div class="dialog__window">
        <div class="dialog__header">
            <h2 class="dialog__title" id="dialog-title">Dialog Title</h2>
            <button aria-label="Close dialog" class="dialog__close" type="button"></button>
        </div>
        <div class="dialog__main">
            <!-- dialog content goes here -->
        </div>
    </div>
</div>

Typically, but not always, a dialog will visibly and programmatically obscure the content in the main window underneath. Therefore it should be coded as a modal dialog, using the aria-modal property.

A fake menu is styled like a regular menu, but it contains a list of links and/or buttons instead of menu items. No ARIA roles are required. If the href value of a fake menu item matches the current page url, then add aria-current="page" to that anchor tag.

<ul>
    <li><a href="http://www.ebay.com">Link Text</a></li>
    <li><a href="http://www.ebay.com">Link Text</a></li>
    <li><a href="http://www.ebay.com">Link Text</a></li>
</ul>

A button that opens a fake menu.

<div class="fake-menu">
    <button aria-expanded="false">Fake Menu</button>
    <div>
        <ul>
            <li><a href="http://www.ebay.com">Link Text</a></li>
            <li><a href="http://www.ebay.com">Link Text</a></li>
            <li><a href="http://www.ebay.com">Link Text</a></li>
        </ul>
    </div>
</div>

Fake tabs look like regular tabs, but behave like a list of links. No JavaScript is required.

<nav aria-labelledby="fake-tabs-title" class="fake-tabs" role="navigation">
    <h2 class="clipped" id="fake-tabs-title">Fake Tabs Heading</h2>
    <ul>
        <li>
            <a aria-current="page" href="http://www.ebay.com/1">Page 1</a>
        </li>
        <li>
            <a href="http://www.ebay.com/2">Page 2</a>
        </li>
        <li>
            <a href="http://www.ebay.com/3">Page 3</a>
        </li>
    </ul>
</nav>

NOTE: We say 'Current Page', rather than 'Current Tab', because the controls are links, not tabs.

A flyout might open on click, focus or hover, on any kind of button, input or link. Ideally, the content element can immediately follow the host element in the DOM. This structure ensures a seamless and natural reading order and focus order.

<div class="flyout">
    <button class="flyout__host" aria-expanded="false">Button</button>
    <div class="flyout__content">
        <!-- overlay content -->
    </div>
</div>

If the overlay cannot be adjacent to the button, then a modifier class will be needed for styling purposes:

<div class="flyout flyout--expanded">
    <div>
        <button class="flyout__host" aria-expanded="true">Button</button>
    </div>
    <div class="flyout__content">
        <!-- overlay content -->
    </div>
</div>

Note that Menu, Fake Menu, Tooltip & Combobox are special instances of flyouts, but follow the same general pattern.

Input validation messages depend on client-side JavaScript and are considered an enhancement to full, server-side form validation.

Valid State

The error message container is a live-region, and can be primed and ready in the server-side code, or injected dynamically at error-time with client-side JavaScript.

<div class="input-validation">
    <span>
        <label for="input1">Input 1</label>
        <input aria-describedby="input1-description" aria-invalid="false" id="input1" name="input1" type="text" />
    </span>
    <span aria-live="polite" class="input-validation__status" role="status">
        <span class="input-validation__description" id="input1-description" >
            <!-- this content should be empty in a valid state, populated in an invalid state -->
        </span>
    </span>
</div>

For Voiceover to correctly detect live-region updates, the updates must happen on direct-descendant(s) of the live-region, not on the live-region itself.

Invalid State

Changing the content of a directly-descendant element will trigger a live-region update.

<div class="input-validation">
    <span>
        <label for="input1">Input 1</label>
        <input aria-describedby="input1-description" aria-invalid="true" id="input1" name="input1" type="text" />
    </span>
    <span aria-live="polite" class="input-validation__status" role="status">
        <span class="input-validation__description" id="input1-description">
            <!-- this content should be empty in a valid state, populated in an invalid state -->
        </span>
    </span>
</div>

Notice that the aria-described attribute supplements the live-region, by using the same live-region content as a description for the input. Descriptions can be read at any time, live-regions only once at the time they are triggered.

Be warned that even a hidden description (using display: none or hidden property) will be announced. Therefore if you plan on simply toggling the display of an error message, be sure to also toggle the presence of the aria-describedby attribute.

The listbox pattern is intended as a JavaScript alternative to the HTML select element. It can be single-select or multi-select.

<span class="listbox">
    <div role="listbox" tabindex="0">
        <div class="listbox__option" role="option">
            <span>Option 1</span>
            <span class="listbox__status"></span>
        </div>
        <div class="listbox__option" role="option">
            <span>Option 2</span>
            <span class="listbox__status"></span>
        </div>
        <div class="listbox__option" role="option">
            <span>Option 3</span>
            <span class="listbox__status"></span>
        </div>
    </div>
</span>

When a single-select listbox receives focus for the first time, the first option should be selected if the user does not specify a selection.

An initial selection can be specified by applying the aria-selected state to one or more options.

Opens a listbox via a button flyout.

<span class="listbox-button">
    <button class="listbox-button__button" aria-expanded="false" aria-haspopup="listbox">
        <span> <!-- flex container -->
            <span>Option 1</span>
            <span class="listbox-button__icon-expand"></span>
        </span>
    </button>
    <div class="listbox-button__listbox" hidden>
        <div role="listbox" tabindex="0">
            <div class="listbox__option" role="option">
                <span>Option 1</span>
                <span class="listbox__status"></span>
            </div>
            <div class="listbox__option" role="option">
                <span>Option 2</span>
                <span class="listbox__status"></span>
            </div>
            <div class="listbox__option" role="option">
                <span>Option 3</span>
                <span class="listbox__status"></span>
            </div>
        </div>
    </div>
</span>

A menu contains commands (menuitem, menuitemradio, or menuitemcheckbox) that execute JavaScript. If you require a non-JavaScript fallback, consider using native form controls (e.g. regular buttons, radios and checkboxes).

<div class="menu">
    <div role="menu">
        <div role="presentation">
            <div role="menuitem" tabindex="0">Button 1</div>
            <div role="menuitem" tabindex="-1">Button 2</div>
        </div>
        <hr />
        <div role="presentation">
            <div aria-checked="true" role="menuitemradio" tabindex="-1">Radio Button 1 (checked)</div>
            <div aria-checked="false" role="menuitemradio" tabindex="-1">Radio Button 2 </div>
            <div aria-checked="false" role="menuitemradio" tabindex="-1">Radio Button 3</div>
        </div>
        <hr />
        <div role="presentation">
            <div aria-checked="true" role="menuitemcheckbox" tabindex="-1">Checkbox 1 (checked)</div>
            <div aria-checked="true" role="menuitemcheckbox"  tabindex="-1">Checkbox 2 (checked)</div>
        </div>
    </div>
</div>

If you only require a flat list of menu items, with no groups or separators, you can instead use a more compact form of markup:

<div class="menu">
    <div role="menu">
        <div role="menuitem" tabindex="0">Button 1</div>
        <div role="menuitem" tabindex="-1">Button 2</div>
    </div>
</div>

In all cases, a menu requires a rovingtabindex for its menu items.

A menu button opens a menu in a flyout.

<div class="menu-button">
    <button aria-expanded="false" aria-haspopup="true" class="menu-button__button">
        <span> <!-- flex container (optional) -->
            <span>Open Menu</span>
            <span class="menu-button__icon-expand"></span>
        </span>
    </button>
    <div class="menu-button__menu" hidden>
        <div role="menu">
            <div role="presentation">
                <div role="menuitem" tabindex="0">Button 1</div>
                <div role="menuitem" tabindex="-1">Button 2</div>
            </div>
            <hr />
            <div role="presentation">
                <div aria-checked="true" role="menuitemradio" tabindex="-1">Radio Button 1 (checked)</div>
                <div aria-checked="false" role="menuitemradio" tabindex="-1">Radio Button 2 </div>
                <div aria-checked="false" role="menuitemradio" tabindex="-1">Radio Button 3</div>
            </div>
            <hr />
            <div role="presentation">
                <div aria-checked="true" role="menuitemcheckbox" tabindex="-1">Checkbox 1 (checked)</div>
                <div aria-checked="true" role="menuitemcheckbox"  tabindex="-1">Checkbox 2 (checked)</div>
            </div>
        </div>
    </div>
</div>

Use role=region and aria-label to mark the page notice as a landmark for assistive technology.

<section class="page-notice" role="region" aria-label="Page notice: high priority">
    <p>Something went wrong. Please try again.</p>
</section>

If the page notice content or display will be dynamically updated on the client, wrap the notice with role="alert" or role="status" to create a live region for assistive technology.

<span role="alert">
    <section class="page-notice" role="region" aria-label="Page notice: high priority">
        <p>Oops. Something is still wrong. Please try again later.</p>
    </section>
</span>

The pagination pattern allows a user to navigate back and forwards through a URL based dataset, or jump directly to any specific URL in that set.

Pagination links may update the results immediately on the client via AJAX, or on the server via a full page reload. In both cases, the browser's URL will be updated.

The example below assumes that the first result set item is the current page (hence the 'Previous' link would appear 'disabled' in this state).

<nav class="pagination" aria-labelledby="pagination-heading" role="navigation">
    <span aria-live="off">
        <h2 id="pagination-heading" class="clipped">Results Pagination - Page 1</h2>
    </span>
    <a aria-disabled="true" aria-label="Previous Page" class="pagination__previous" href="1.html">
        <svg aria-hidden="true">
            <use xlink:href="#icon-chevron-light-left"></use>
        </svg>
    </a>
    <ol>
        <li>
            <a aria-current="page" href="1.html">1</a>
        </li>
        <li>
            <a href="2.html">2</a>
        </li>
        <li>
            <a href="3.html">3</a>
        </li>
    </ol>
    <a aria-label="Next Page" class="pagination__next" href="2.html">
        <svg aria-hidden="true">
            <use xlink:href="#icon-chevron-light-right"></use>
        </svg>
    </a>
</nav>

NOTE: The heading need only be wrapped in an ARIA live-region if client-side pagination is implemented (i.e. partial page updates).

Native HTML radio buttons are 100% accessible by default. To ensure correct grouping and group label semantics, radio buttons should always be placed inside of a fieldset with legend.

<fieldset>
    <legend>Radio Group Title</legend>
    <span>
        <input id="radio-group1_input1" name="radio-group1" type="radio" value="1" checked />
        <label for="radio-group1_input1">Input 1</label>
    </span>
    <span>
        <input id="radio-group1_input2" name="radio-group1" type="radio" value="2" />
        <label for="radio-group1_input2">Input 2</label>
    </span>
    <span>
        <input id="radio-group1_input3" name="radio-group1" type="radio" value="3" />
        <label for="radio-group1_input3">Input 3</label>
    </span>
</fieldset>

For vertically stacked radios, simply switch the spans to divs.

Custom Radios

To create a custom radio style, foreground SVG can be used as a facade over the real radio.

<span class="radio" hidden>
    <input class="radio__control" id="radio-group1_input1" type="radio" value="3" name="radio-group-1" />
    <svg aria-hidden="true" class="radio__unchecked" focusable="false">
        <use xlink:href="#icon-radio-unchecked"></use>
    </svg>
    <svg aria-hidden="true" class="radio__checked" focusable="false">
        <use xlink:href="#icon-confirmation"></use>
    </svg>
</span>

This markup assumes that the symbol definitions for #icon-radio-unchecked and #icon-radio-checked exist on the page. The hidden property ensures that the SVG icon is not visible alongside the native icon when the page is in a non-CSS state. This hidden property should be overriden by CSS.

Don't forget to add a label!

A switch behaves a bit like a checkbox - it can be on or off (i.e checked or unchecked). The key difference is that a switch is not a true form control. It typically executes JavaScript on the client when toggled (i.e. without a full page reload) rather than storing form data to be sent to the server.

<span class="switch">
    <span class="switch__control" role="switch" tabindex="0" aria-checked="false"></span>
    <span class="switch__button"></span>
</span>

The following version of the switch uses a checkbox under the hood. It should be used if you require the switch to store data inside of a form. As mentioned above however, this is not the intended purpose of a switch. You may wish to consider using an actual checkbox instead.

<span class="switch">
    <input class="switch__control" role="switch" type="checkbox" aria-checked="false" />
    <span class="switch__button"></span>
</span>
<div class="tabs" id="tabs-1">
    <h2 class="clipped">Tabs Heading</h2>
    <div class="tabs__items" role="tablist">
        <div class="tabs__item" role="tab" aria-controls="tabs-1-panel-1" aria-selected="true" id="tabs1_tab1" tabindex="0">
            <span>Tab 1</span>
        </div>
        <div class="tabs__item" role="tab" aria-controls="tabs-1-panel-2" aria-selected="false" id="tabs1_tab2" tabindex="-1">
            <span>Tab 2</span>
        </div>
        <div class="tabs__item" role="tab" aria-controls="tabs-1-panel-3" aria-selected="false" id="tabs1_tab3" tabindex="-1">
            <span>Tab 3</span>
        </div>
    </div>
    <div class="tabs__content">
        <div aria-labelledby="tabs1_tab1" class="tabs__panel" role="tabpanel" id="tabs-1-panel-1">
            <h3>Panel 1 heading</h3>
            <!-- panel content goes here -->
        </div>
        <div aria-labelledby="tabs1_tab2" class="tabs__panel" role="tabpanel" hidden id="tabs-1-panel-2">
            <h3>Panel 2 heading</h3>
            <!-- panel content goes here -->
        </div>
        <div aria-labelledby="tabs1_tab3" class="tabs__panel" role="tabpanel" hidden id="tabs-1-panel-3">
            <h3>Panel 3 heading</h3>
            <!-- panel content goes here -->
        </div>
    </div>
</div>

Tooltip structure is almost identical to flyout structure:

<span class="tooltip">
    <input aria-describedby="tooltip-0" class="tooltip__host" type="submit" value="Submit" />
    <div class="tooltip__overlay" id="tooltip-0" role="tooltip">
        <p>Hint content</p>
    </div>
</span>

Role of tooltip is required on the overlay, and aria-describedby attribute is required on the trigger element.

About

Accessible HTML code patterns for common UI widgets such as tabs, menus, dialogs, etc.

License:MIT License