Skip to content

ianmcburnie/a11y-first-workshop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

a11y-first-workshop : A Web Accessibility Workshop

HTML is fully accessible by default, but whenever a developer chooses the wrong tag, wrong ARIA attribute, or adds a line of CSS or JavaScript, they run the risk of breaking that out-of-the-box accessibility.

Starting completely from scratch, this workshop will progressively build up the UI for a typical eCommerce homepage, sign-in page, and SRP, introducing many common accessibility principles, techniques and gotchas along the way.

No prior accessibility experience is necessary, but we assume some basic familiarity with HTML, CSS and JavaScript. If you get lost or lose your way, every step of every chapter has a "here's one I made earlier" that you can copy-and-paste from into your file.

If you want to run through this workshop yourself, or follow along with the instructor, please follow these steps:

  1. Clone this repo to your local nodejs environment
  2. Run yarn && yarn start (or use npm install && npm start if you prefer)
  3. Open http://localhost:3000/myindex.html
  4. Edit files myindex.html, myapp.css and mymain.js. The browser will auto-refresh.

Table of Contents

Chapter 1: Introduces Structure & Semantics

Chapter 1 builds a mock version of a typical eCommerce homepage.

Whenever I build a new page or component, I always start with the markup. I don't even think about the CSS and JavaScript until I am happy with the structure and semantics. In fact, I will go as far as to disable CSS and JavaScript in my browser to ensure that the raw markup is accessible and functional.

Some developers, on the other hand, like to start with the JavaScript first. Or the "bells and whistles" as I like to say. To me this is like a builder placing down all of the household electronics (television, microwave, internet router, etc) on the plot of land before the foundations have been laid, walls built and windows put in. Yes, it's exactly like that. Okay, okay maybe I need a better analogy ;-)

  1. Blank Page
  2. Meta Data
  3. Paragraphs
  4. Lists
  5. Headings
  6. Landmarks
  7. Images
  8. Background Images
  9. Table Layout
  10. Grid System
  11. Cards
  12. Inline Frames

Blank Page

We'll begin with our myindex.html page. The page content for this step is left intentionally blank. It already contains some accessibility "gotchas", which we will begin to identify & address in the next step.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <!-- uncomment the following 2 lines when instructed -->
        <!-- <link rel="stylesheet" href="https://ir.ebaystatic.com/cr/v/c1/skin/v7.0.2/ds4/skin.min.css"/> -->
        <!-- <link rel="stylesheet" href="https://ir.ebaystatic.com/cr/v/c1/skin/v6.3.3/ds4/grid-full.min.css"/> -->
        <link rel="stylesheet" href="myapp.css"/>
    </head>
    <body>
        <!-- intentionally blank -->
    </body>
    <script src="myapp.js"></script>
    <script>$_mod.ready();</script>
</html>
  1. Test page with screen reader, notice nothing is announced

Meta Data

Let's begin adding some additional meta data to the page.

  1. Add lang="en" to the html tag
  2. Add <meta name="viewport" content="width=device-width, initial-scale=1"> to head
  3. Add <title>Meta - A11Y First Workshop</title> to head
  4. Test page with screen reader, notice title is now announced

We have introduced 3 key pieces of 'meta' data:

  • Language attribute ensures correct pronunciation, amongst other things.
  • Viewport meta must ensure pinch-to-zoom is not disabled.
  • Title ensures the user can orient themselves.
    • See WCAG 2.4.2. Titles identify the current location without requiring users to read or interpret page content.

Paragraphs

  1. Add three paragraphs of content to the page (HTML below)
  2. Ensure VoiceOver web rotor settings include 'Static Text'.
  3. Navigate VO to web area (show that voiceover can read desktop and other desktop applications too, not just web pages)
  4. Open VoiceOver rotor and explore what it can find on the page (hint: nothing much yet! only the static paragraph text).
  5. We ignore VO 'Web Spots' rotor as this is specific to Voiceover.

Screenshot of the VoiceOver Web Rotor settings

`<p>eBay is where the world goes to shop, sell, and give. Our mission is to be the world’s favorite destination for discovering great value and unique selection.</p>`
`<p>For over 20 years, we've been working to create more economic opportunity for everyone. And we're just getting started.</p>`
`<p>Copyright © 1995-2017 eBay Inc. All Rights Reserved.</p>`

Lists

  1. Add two unordered lists to the page (see markup below)
  2. Ensure VoiceOver web rotor settings include 'Lists'
  3. Explore lists with VO shortcuts and web rotor
  4. Demonstrate that VO stops on list element and announces number of items in each list
  5. Demonstrate that VO announces 'bullet' before each list item contents
<ul>
    <li>Mix It Up - 17 items by statesidelife</li>
    <li>At the Hearth - 9 items by designassembly</li>
    <li>Give Thanks - 35 items by ebayhomeeditor</li>
    <li>Guests of Honour - 30 items by ebaydealseditor</li>
</ul>

<ul>
    <li>Christmas Socks - $4.99</li>
    <li>MacBook Air - $799.99</li>
    <li>Xbox One 500GB - $169.99</li>
    <li>Playstation Pro 1TB - $419.99</li>
</ul>

Now open MacOS Accessibility Inspector and inspect the lists.

Screenshot of the MacOS Accessibility Inspector

Compare with Firefox inspector

Screenshot of the Firefox Accessibility Inspector

Discussion!

What's the difference between an unordered list and an ordered list?

Definition Lists

todo

Headings

  1. Add the following headings:
    • <h1>eBay</h1>
    • <h2>Collections</h2>
    • <h2>Daily Deals</h2>
    • <h2>Legal</h2>
  2. Navigate headings with voiceover keyboard shortcuts and web rotor

It's a bit odd to display a "Legal" header. It's probably not going to fly with designers, but SEO and screenreader users need it. Let's address that now.

  1. Add clipped class to the 'Legal' header
  2. Demo that legal header is now 'invisible'
  3. Demo the legal header is still conveyed to screen reader
  4. Show legal header in screen reader headings list
.clipped {
    border: 0;
    clip: rect(1px, 1px, 1px, 1px);
    -webkit-clip-path: inset(50%);
          clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

Landmarks

  1. Add the following landmark tags:
    • <header> around h1
    • <main> around lists
    • <footer> around legal content
  2. Notice there is no visual difference to the page. This is intentional.
  3. Display landmarks in screen reader landmarks lists. Notice that footer is not visible
  4. Add the following landmark roles
    • role="banner" to header
    • role="main" to main
    • role="contentinfo" to footer
  5. Display landmarks in screen reader landmarks lists. Notice that footer is now visible as contentinfo
footer[role="contentinfo"] {
    background-color: white;
    border-top: 1px solid #ccc;
    margin-top: 32px;
    padding-bottom: 8px;
}

Images

  1. Replace <header role="banner"><h1>eBay</h1></header> with <div><h1><img src="images/ebay-hires.png" /></h1></div> (lack of alt text intentional for now)
  2. Demonstrate behaviour of missing alt text in screen reader (it reads image url/filename)
  3. Add alt="ebay" to h1 content image
  4. Demonstrate behaviour of alt text with screen reader (it now reads 'ebay' text)
  5. Add image before each collection title, and for the time being set alt="collection title"
  6. Wrap title in paragraph tag
  7. Demonstrate that voiceover no longer announces 'bullet' for each list item
<div class="collections">
    <ul>
        <li>
            <img src="images/mix-it-up.jpg" alt="Mix It Up" />
            <p>Mix It Up - 17 items by statesidelife</p>
        </li>
        <li>
            <img src="images/at-the-hearth.jpg" alt="At the Hearth" />
            <p>At the Hearth - 9 items by designassembly</p>
        </li>
        <li>
            <img src="images/give-thanks.jpg" alt="Give Thanks" />
            <p>Give Thanks - 35 items by ebayhomeeditor</p>
        </li></a>
        <li>
            <img src="images/guests-of-honour.jpg" alt="Guests of Honour" />
            <p>Guests of Honour - 30 items by ebaydealseditor</p>
        </li>
    </ul>
</div>

Now that we have content that goes below the fold, we can demonstrate some keyboard accessibility.

  1. Use spacebar, up arrow, down arrow, page up, page down, home and end keys to scroll page

DISCUSSION!

Note that the image alt text is the same as the title. Technically speaking these images can be classed as presentational, because if the images were not displayed, we still have the same text below (the title text). We leave the alt text in place for now. Yes, it's a redundant/duplicate navigation for screen reader users, but not technically 'non-accessible'. When we convert the item to a tile, in an upcoming step, we will set this value to blank.

Background Images

Now let's try adding images in a different way, using CSS background images for the daily deals section.

<h2>Daily Deals</h2>
<div>
    <ul>
        <li>
            <span class="image" role="img" style="background-image: url('images/christmas-socks.jpg')" aria-label="Christmas Socks"></span>
            <p>Christmas Socks - $4.99</p>
        </li>
        <li>
            <span class="image" role="img" style="background-image: url('images/macbook-air.jpg')" aria-label="MacBook Air"></span>
            <p>MacBook Air - $799.99</p>
        </li>
        <li>
            <span class="image" role="img" style="background-image: url('images/xbox-one.jpg')" aria-label="Xbox One 500GB"></span>
            <p>Xbox One 500GB - $169.99</p>
        </li>
        <li>
            <span class="image" role="img" style="background-image: url('images/playstation-pro.jpg')" aria-label="Playstation 4 Pro"></span>
            <p>Playstation 4 Pro - $419.99</p>
        </li>
    </ul>
</div>
  1. Disable images in Firefox browser and point out that foreground images show alt text, background images do not

DISCUSSION

How do we decide between using foreground images and background images. Imagine a Google or Bing image search that returned no images when CSS is disabled, or no alternative text for images. With foreground images we can avoid that situation. Important images should never require CSS or JavaScript!

Table Layout

Layout the collections and deals in tables.

Demonstrate the problems with using table tags for layout.

  1. Replace the list of collections with a 4x4 table
  2. Replace the list of deals with a 1x4 table
  3. Demonstrate that screen reader announces rows & columns (voiceover also needs tbody and empty th to demo this)
  4. Add role="presentation" to the two tables
  5. Demonstrate that screen reader now no longer announces table dimensions

The final HTML should look like below:

<div class="collections">
    <table role="presentation">
        <tr>
            <td>
                <img src="images/mix-it-up.jpg" alt="Mix It Up" />
                <p>Mix It Up - 17 items by statesidelife</p>
            </td>
            <td>
                <img src="images/at-the-hearth.jpg" alt="At the Hearth" />
                <p>At the Hearth - 9 items by designassembly</p>
            </td>
        </tr>
        <tr>
            <td>
                <img src="images/give-thanks.jpg" alt="Give Thanks" />
                <p>Give Thanks - 35 items by ebayhomeeditor</p>
            </td>
            <td>
                <img src="images/guests-of-honour.jpg" alt="Guests of Honour" />
                <p>Guests of Honour - 30 items by ebaydealseditor</p>
            </td>
        </tr>
    </table>
</div>
<h2>Daily Deals</h2>
<div>
    <table role="presentation">
        <tr>
            <td>
                <span class="image" role="img" style="background-image: url('images/christmas-socks.jpg')" aria-label="Christmas Socks"></span>
                <p>Christmas Socks - $4.99</p>
            </td>
            <td>
                <span class="image" role="img" style="background-image: url('images/macbook-air.jpg')" aria-label="MacBook Air"></span>
                <p>MacBook Air - $799.99</p>
            </td>
            <td>
                <span class="image" role="img" style="background-image: url('images/xbox-one.jpg')" aria-label="Xbox One 500GB"></span>
                <p>Xbox One 500GB - $169.99</p>
            </td>
            <td>
                <span class="image" role="img" style="background-image: url('images/playstation-pro.jpg')" aria-label="Playstation 4 Pro"></span>
                <p>Playstation 4 Pro - $419.99</p>
            </td>
        </tr>
    </table>
</div>

Grid System

First of all, we are going to add a new DIV wrapper inside the body tag and footer tags. The following layout CSS will be needed:

body > div,
footer[role="contentinfo"] > div {
    margin: 0 auto;
    max-width: 1280px;
}

.collections img,
.deals img {
    display: block;
    max-width: 100%;
}

Demonstrate that in some cases, CSS can effect semantics.

  1. Revert the table changes made in the previous step (i.e. back to a list)
  2. Add div wrapper container inside of body (presentational step, see CSS below)
  3. Add div wrapper container inside of footer (presentational step, see CSS below)
  4. Add class .grid to the .collections and .deals
  5. Add grid__group grid__group--no-gutters grid__group--wrap to the collections list
  6. Add grid__group grid__group--no-gutters to the deals list
  7. Wrap each collection list item in a class="grid__cell grid__cell--one-half"
  8. Demonstrate that voiceover no longer announces list semantics
  9. Add role="list" to the lists to restore list semantics
  10. Add display: block and max-width: 100% to images (see CSS below) so they scale responsively (not strictly needed for a11y purposes).

The final HTML should look like below:

<div class="grid collections">
    <ul class="grid__group grid__group--no-gutters grid__group--wrap" role="list">
        <li class="grid__cell grid__cell--one-half">
            <img src="images/mix-it-up.jpg" alt="Mix It Up" />
            <p>Mix It Up - 17 items by statesidelife</p>
        </li>
        <li class="grid__cell grid__cell--one-half">
            <img src="images/at-the-hearth.jpg" alt="At the Hearth" />
            <p>At the Hearth - 9 items by designassembly</p>
        </li>
        <li class="grid__cell grid__cell--one-half">
            <img src="images/give-thanks.jpg" alt="Give Thanks" />
            <p>Give Thanks - 35 items by ebayhomeeditor</p>
        </li>
        <li class="grid__cell grid__cell--one-half">
            <img src="images/guests-of-honour.jpg" alt="Guests of Honour" />
            <p>Guests of Honour - 30 items by ebaydealseditor</p>
        </li>
    </ul>
</div>

<h2>Daily Deals</h2>
<div class="grid deals">
    <ul class="grid__group grid__group--no-gutters" role="list">
        <li class="grid__cell grid__cell--one-fourth">
            <span class="image" role="img" style="background-image: url('images/christmas-socks.jpg')" aria-label="Christmas Socks"></span>
            <p>Christmas Socks - $4.99</p>
        </li>
        <li class="grid__cell grid__cell--one-fourth">
            <img src="images/macbook-air.jpg" alt="MacBook Air"/>
            <p>MacBook Air - $799.99</p>
        </li>
        <li class="grid__cell grid__cell--one-fourth">
            <img src="images/xbox-one.jpg" alt="Xbox One 500GB"/>
            <p>Xbox One 500GB - $169.99</p>
        </li>
        <li class="grid__cell grid__cell--one-fourth">
            <img src="images/playstation-pro.jpg" alt="Playstation 4 Pro"/>
            <p>Playstation 4 Pro - $419.99</p>
        </li>
    </ul>
</div>

DISCUSSION!

Adding list-type: none (via grids css) to the lists means they are no longer announced as a list in some screen readers. We fix this issue by applying role="list" to each list.

Cards

A purely presentation step where we convert each collection item into a card using Skin CSS.

  1. Wrap each collection item with <div class="card"><div class="card__cell">...</div></div>

Inline Frames

Create a new iframe-content-1.html page with the following markup:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            body {
                margin: 0;
            }
            img:last-child {
                position: absolute;
                top: 20px;
                left: 20px;
            }
        </style>
    </head>
    <body>
        <img src="images/gift-cards-for-home.jpg" alt="" />
        <img src="images/advert-gift.jpg" alt="It's the gift they've been eyeing all year. Find it here." />
    </body>
</html>

Add the following CSS:

iframe {
    border: 0;
    height: 200px;
    margin: 1rem 0;
    width: 100%;
}
  1. Add iframe without title between header and main <iframe src="iframe-content-1.html" scrolling="no"></iframe>
  2. Demo that iframe is keyboard focusable in Firefox
  3. Demonstrate untitled iframe behaviour with screen reader
  4. Add title="Advert" to iframe and demo with screen reader
  5. This is the only recommended use of title attribute! (i.e. do not use it as a 'tooltip' on links)
  6. Navigate inside iframe with screen reader
  7. On main page, wrap iframe in <aside role="complementary">. Launch VoiceOver list of landmarks.

This is the final content of main page:

<aside role="complementary">
  `<iframe src="iframe-content-1.html" scrolling="no" title="Advert"></iframe>`
 </aside>

Chapter 2: Introduces Links

Chapter 2 continues to build upon our homepage, before moving onto a typical sign in page.

  1. Text Links
  2. Tiles
  3. Ambiguous Links
  4. Fake Buttons
  5. New Window Link
  6. Navigation Landmark
  7. Skip-to Link
  8. Enhanced Skip-to
  9. Custom Focus

Text Links

  1. Add links around collection and daily deals titles
  2. Temporarily add a {text-decoration: none}
  3. Demonstrate the challenges of finding hyperlinks within paragraph text
  4. Remove a {text-decoration: none}
  5. With screen reader disabled, use TAB key to navigate focus through links
  6. Demo different focus outline in Firefox
  7. Notice that hand cursor shows for links
  8. Notice that with focus on a link all of the page scroll keys still work.
  9. With screen reader on notice that visited links will be announce as 'visited'.
  10. Notice that it would be nice to have the image as a link to the item page too. We will address this in a later step.

Discussion

  • Should the link/title be a heading? I would say it is not strictly necessary for accessibility, but maybe so for SEO. Opinion is often divided here.

Tiles

  1. Move anchor tag from collection and deals titles and wrap it around the entire contents of list item
  2. Show keyboard focus indicator
  3. Set display: block on anchor to fix focus-indicator.
  4. Show fixed focus indicator
  5. Listen to anchor in screen reader and notice that link text is red twice
  6. Set the image alt value to blank
  7. Listen to anchor in screen reader and now link text is only read once
  8. Demonstrate what happens if we try to make seller profile a nested link inside of the tile.
  9. Notice that now all text in tile is blue. Can fix this with CSS.
a.item-tile {
    display: block;
}
a.item-tile p:last-child {
    color: #555;
}

Ambiguous Links

  1. Add "See all" link after collections and deals lists
  2. Align link to center of grid
  3. Demonstrate that we now have two 'ambiguous' links with same text that go to different places
  4. Append clipped text to each link text
  5. Demonstrate the difference if using aria-label
<div class="see-all">
    <a href="http://www.ebay.com/cln">See all<span class="clipped"> - Collections</span></a>
</div>
...
<div class="see-all">
    <a href="http://deals.ebay.com">See all<span class="clipped"> - Deals</span></a>
</div>
.see-all {
    text-align: center;
}
.see-all a {
    text-decoration: none;
}

Fake Buttons

  1. Add class="btn" to the two "see all" links
  2. Notice that skin hasn't styled them. Something is wrong. Skin enforces accessibility. We don't allow btn class on a link unless developer signifies they know what they are doing.
  3. Add class="fake-btn"
  4. Link is now styled as a button
  5. Demo that screen reader still reads them as links (which is correct)
  6. Explain that this can cause issues for customer service (non-sighted user reports UI control as a link, while sighted customer service person sees a button)
  7. The giveaway for mouse users is the hand cursor icon.
  8. The giveaway for keyboard users is the underline on focus.

New Window Link

Append the following link to the legal footer paragraph:

<a href="http://pages.ebay.com/help/policies/user-agreement.html" target="_blank">User Agreement</a>

Demonstrate that screen reader does not notify user of this behaviour when focus is on link.

Add clipped text:

<a href="http://pages.ebay.com/help/policies/user-agreement.html" target="_blank">User Agreement<span class="clipped"> - opens in new window or tab</span></a></p>

This lets screen reader users know about link behaviour, but sighted users still do not know. Let's fix that with an icon:

<a href="http://pages.ebay.com/help/policies/user-agreement.html" target="_blank">User Agreement <span class="icon icon--window" role="img" aria-label="Opens in new window or tab"></span></a>

The following CSS is needed for the icon:

span.icon {
    background-repeat: no-repeat;
    background-size: contain;
    display: inline-block;
}

span.icon--window {
    background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyOCAyNSI+PHBhdGggZmlsbD0iIzc2NzY3NiIgZD0iTS40OTcgNS43MDFoMTUuNjQ0Yy4yNzQgMCAuNDk3LjIyMi40OTcuNDk3di43MThhLjQ5Ny40OTcgMCAwIDEtLjQ5Ny40OTdILjQ5N0EuNDk3LjQ5NyAwIDAgMSAwIDYuOTE2di0uNzE4YzAtLjI3NC4yMjItLjQ5Ny40OTctLjQ5N3ptMS4yMTUgMFYyMy45NEgwVjUuNzAxaDEuNzEyem0xOC41MTUgNy43ODV2OS45MzlhLjU1Mi41NTIgMCAwIDEtLjU1Mi41NTJoLS42OTlhLjU1Mi41NTIgMCAwIDEtLjU1Mi0uNTUydi05LjkzOWMwLS4zMDUuMjQ3LS41NTIuNTUyLS41NTJoLjY5OWMuMzA1IDAgLjU1Mi4yNDcuNTUyLjU1MnpNMCAyMi4zMDJoMjAuMjA5djEuNzEySDB2LTEuNzEyek0yNS43NjcuMTYxaDEuOTE0djE2LjU4M2gtMS45MTRWLjE2MXptMS45MTQtLjE0N3YxLjkxNEgxMS4wOThWLjAxNGgxNi41ODN6TTI2LjMyMiAwbDEuMzUzIDEuMzUzTDEzLjM5OCAxNS42M2wtMS4zNTMtMS4zNTNMMjYuMzIyIDB6Ii8+PC9zdmc+);
    height: 12px;
    vertical-align: top;
    width: 12px;
}

Navigation Landmark

This is an opportunity to recap, and build upon, headings, landmarks and links.

  1. Add list of links (see HTML below) after banner tag
  2. Wrap links with <nav id="cat-nav" role="navigation">
  3. Add <h2 class="clipped" id="cat-nav">Categories</h2>
  4. Demo new navigation landmark in screen reader.
  5. Add aria-labelledby="main-nav" to nav tag
  6. Demo labelled navigation landmark in screen reader
<nav aria-labelledby="cat-nav-title" id="cat-nav" role="navigation">
    <h2 class="clipped" id="cat-nav-title">Categories</h2>
    <ul role="list">
        <li><a href="http://www.ebay.com/motors">Motors</a></li>
        <li><a href="http://www.ebay.com/motors">Fashion</a></li>
        <li><a href="http://www.ebay.com/motors">Electronics</a></li>
        <li><a href="http://www.ebay.com/motors">Collectibles &amp; Art</a></li>
        <li><a href="http://www.ebay.com/motors">Home &amp; Garden</a></li>
        <li><a href="http://www.ebay.com/motors">Sporting Goods</a></li>
        <li><a href="http://www.ebay.com/motors">Toys</a></li>
        <li><a href="http://www.ebay.com/motors">Business &amp; Industrial</a></li>
        <li><a href="http://www.ebay.com/motors">Music</a></li>
        <li><a href="http://www.ebay.com/motors">Holiday</a></li>
    </ul>
</nav>
#cat-nav [role=list] {
    border-bottom: 1px solid #ccc;
    border-top: 1px solid #ccc;
    display: flex;
    list-style-type: none;
    margin: 0;
    padding: 1em 0;
}
#cat-nav li {
    text-align: center;
    width: 128px;
}
#cat-nav a {
    font-size: 12px;
}

Skip-To Link

  1. Add <span class="skipto"><a href="#mainContent">Skip to main content</a></span> as first descendant of body
  2. Add id="mainContent" to main landmark
  3. Demo keyboard behaviour
  4. Add class clipped clipped--stealth to skip link
  5. Demo that skip link now only appears on keyboard focus
  6. Demo how screen reader focus does not get set on mainContent
  7. Add tabindex="-1" to main landmark
  8. Demo how screen reader focus is now set
  9. Demo that permanent tabindex on main element causes a focus outline when clicked with mouse or touch.
  10. Fix focus outline issue with CSS. This is perhaps the only time it is okay to remove focus outline.

Final HTML:

<span class="skipto">
    <a href="#mainContent" class="clipped clipped--stealth">Skip to main content</a>
</span>
a[href='#mainContent'] {
    background-color: LightYellow;
    left: 0;
    padding: 0.25em;
    top: 0;
}

#mainContent:focus {
    outline: 0 none;
}

DISCUSSION!

An experimental :focus-visible pseudo-selector aims to tackle the issue of only adding a focus outline if focus is set with keyboard.

CHECKPOINT: JavaScript

At this point we will be introducing some JavaScript. So please create a JavaScript file and include it on your page.

Skip-To Link Enhanced

Rather than adding a permanent tabindex to main, it would be better to set a temporary tabindex using JavaScript. The tabindex can be set when the skipto link is clicked, and then removed as soon as the target element loses focus.

querySelectorAllToArray('.skipto').forEach(function(el, i) {
    el.addEventListener('click', function() {
        var targetEl = document.querySelector(el.querySelector('a').getAttribute('href'));
        targetEl.setAttribute('tabindex', '-1');
        targetEl.focus();
    });
});

Custom Focus

Now we go back to our iframe content.

  1. Wrap image in link and demonstrate focus indicator issue when iframe body has zero margin
  2. Add a second image after the first <img src="images/advert-gift.jpg" alt="It's the gift they've been eyeing all year. Find it here." />
  3. Move link around this new image
  4. Create an advert that plenty of margin around hyperlink

This is the final content of the IFRAME:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            body {
                margin: 0;
            }
            a {
                position: absolute;
                top: 20px;
                left: 20px;
            }
        </style>
    </head>
    <body>
        <img src="images/gift-cards-for-home.jpg" alt="" />
        <a href="http://www.ebay.com/rpp/holidays/?&_trkparms=%26clkid%3D368520364132448429" target="_parent">
            <img src="images/advert-gift.jpg" alt="It's the gift they've been eyeing all year. Find it here." />
        </a>
    </body>
</html>

Custom Focus

We will use the image link inside of the iframe to demonstrate a custom focus indicator.

a:focus {
    outline: 2px dashed white;
}

CHECKPOINT: Sign In & Registration

We now move onto two new pages, sign in and registration. Create two files signin.html and reg.html. Both pages have the following content to begin with:

<body class="identity">
    <div role="main">
        <h1 class="clipped">Sign In and Registration</h1>
        <!-- content goes here -->
    </div>
</body>

Add the following styles to app.css:

body.identity [role="main"] {
    background-color: white;
    border: 1px solid #ccc;
    margin: 16px auto;
    padding: 16px;
    width: 400px;
}

Fake Tabs

Links are sometimes presented visually as tabs, but retain their link behaviour. It is a common mistake amongst developers to assign the ARIA tab related roles to these links.

Add the following list of links inside the main landmark:

<div>
    <ul>
        <li>
            <a href="fake-tabs.html">Sign In</a>
        </li>
        <li>
            <a href="../reg/fake-tabs.html">Register</a>
        </li>
    </ul>
    <div>
        <p>Sign-in form goes here</p>
    </div>
</div>

Demonstrate that everything we have already learnt about lists and links applies here. We could also use a navigation landmark around the list.

Now add the following Skin classes to style the links as fake tabs:

<div class="fake-tabs">
    <ul class="fake-tabs__items">
        <li class="fake-tabs__item fake-tabs__item--current">
            <a aria-current="page" href="#">Sign In</a>
        </li>
        <li class="fake-tabs__item">
            <a href="reg.html">Register</a>
        </li>
    </ul>
    <div class="fake-tabs__content">
        <form>
            <p>Sign-in form goes here</p>
        </form>
    </div>
</div>

Demonstrate that the tabs are still announced as links, and still show up in the screen reader list of links. This is the desired, expected behaviour.

The aria-current property announces which link in the list matches the current URL.

Discussion

A problem with fake-tabs is that they look too realstic. Keyboard users might try and use the arrow keys, rather than the TAB key.

Chapter 3: Introduces Buttons

This chapter introduces basic button semantics and behaviour.

Button Text

First of all, let's take care of some page layout, and wrap the logo in an "eyeball" block. This name will soon make sense ;-)

<header role="banner">
    <div id="eyeball">
        <h1><img src="../images/ebay-hires.png" alt="ebay" /></h1>
    </div>
</header>

This CSS will give us the basic layout we need.

#eyeball {
    align-items: center;
    display: flex;
    max-width: 1280px;
    margin: 0 auto;
}

Buttons may seem straightforward, after all we have a button tag that gives us everything we need. So why do some developers insist on using DIVS or LINKS? I really don't know.

Final HTML of eyeball block:

<div id="eyeball">
    <div>
        <h1><img src="../images/ebay-hires.png" alt="ebay" /></h1>
    </div>
    <div>
        <button type="button" onclick="alert('Under construction')">Shop by Category</button>
    </div>
</div>

Discussion

Why do we specify type of button?

Critical Icon

Above the eyeball goes the, yes you guessed it, the "eyebrow" block:

<div id="eyebrow">
    <div>
        <div>
            <!-- critical icon button will go here -->
        </div>
    </div>
</div>

Layout CSS:

#eyebrow {
    background-color: white;
    border-bottom: 1px solid #ccc;
    padding: 0.25em 0;
}

#eyebrow > div {
    display: flex;
    margin: 0 auto;
    max-width: 1280px;
}
  1. Append notification button to eyebrow (see HTML below)
  2. Fix up the CSS layout
  3. Navigate to button with screen reader and notice missing label
  4. Add an aria-label and demonstrate screen reader and/or a11y inspector
  5. Add a title attribute. Show how this "tooltip" is not keyboard accessible.
  6. Remove the title attribute. We will come back to an accessible tooltip later.

Eyebrow interim changes:

<div id="eyebrow">
    <div>
        <div>
            <button id="notifications" aria-label="Notifications" onclick="alert('You have no new notifications')">
                <span class="icon icon--notification"></span>
            </button>
        </div>
    </div>
</div>

CSS:

button#notifications {
    background: 0 none;
    border: 0 none;
}

span.icon--notification {
  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iIzc2NzY3NiIgZD0iTTEyLjA0OCAyMy41OWMxLjIyOSAwIDIuMjI0LS45OTUgMi4yMjQtMi4xNjZoLTQuMzljLS4wNiAxLjE3MS45MzcgMi4xNjYgMi4xNjYgMi4xNjZ6bTExLjUzMi00LjU2NnMtMi4xNjYtMS43NTYtMy4zMzctMy4xMDJjLS45MzctMS4xMTItLjkzNy0yLjUxNy0uOTM3LTQuMzMyIDAtMi40LjA1OS01LjQ0NC0yLjY5My04LjQyOUMxNS40NDIgMS44NzMgMTQuMDM3IDEuMjg4IDEzLjEuOTk1IDEzLjA0LjQxIDEyLjUxNiAwIDExLjkzIDBjLS41ODYgMC0xLjA1NC40MS0xLjE3MS45OTUtLjk5NS4yOTMtMi4zNDEuODc4LTMuNTEyIDIuMTY2QzQuNDk2IDYuMTQ2IDQuNDk2IDkuMTkgNC41NTQgMTEuNTljMCAxLjgxNS4wNTkgMy4yMi0uOTM3IDQuMzMyQzIuNDQ2IDE3LjI2OC4zMzkgMTkuMDI0LjI4IDE5LjAyNGMtLjIzNC4xNzYtLjM1MS41MjctLjIzNC44Mi4xMTcuMjkzLjM1LjQ2OC43MDIuNDY4aDIyLjI0NGEuNzYuNzYgMCAwIDAgLjcwMi0uNDY4Yy4yMzQtLjI5My4xMTctLjY0NC0uMTE3LS44MmguMDAzem0tMTEuNTMyLS4xNzVoLTkuMjVhMzEuNjU2IDMxLjY1NiAwIDAgMCAxLjk5LTEuOTljMS4zNDctMS41MjIgMS4yODktMy4zMzcgMS4yODktNS4yNjggMC0yLjQtLjA2LTQuODU5IDIuMzQtNy40MzRDOS45NCAyLjQ1OSAxMS45OSAyLjIyNSAxMS45OSAyLjIyNWguMDU5czIuMDQ5LjIzNCAzLjU3IDEuOTMyYzIuMzQyIDIuNTc2IDIuMzQyIDUuMDM0IDIuMzQyIDcuNDM0IDAgMS45MzItLjA2IDMuNzQ2IDEuMjg4IDUuMjY4YTMxLjY1NiAzMS42NTYgMCAwIDAgMS45OSAxLjk5aC05LjE5eiIvPjwvc3ZnPg==');
  height: 1rem;
  width: 24px;
}
  1. Demonstrate Windows High Contast Mode. The icon dissapears.
  2. Replace the background icon with a foreground icon

Final markup:

<button id="notifications" aria-label="Notifications" onclick="alert('You have no new notifications')">
    <svg aria-hidden="true" focusable="false" width="16" height="16">
        <use xlink:href="../icons.svg#icon-notification"></use>
    </svg>
</button>

DISCUSSION!

Should we change the mouse cursor when it hovers over this icon button?

Access Key

  1. Add attribute accesskey="n" to the notifications button
  2. Use accesskey to activate shortcut (e.g. CTRL+ALT+N for Safari)
  3. VoiceOver will announce availability of access key

DISCUSSION!

But how do sighted keyboard users know about this access key? Nooo, not with the title attribute (it's not keyboard accessible, remember). We need a tooltip.

Chapter 4: Introduces Flyouts

Flyouts are content that expands out from another element or page region.

Button Flyout

The eBay shop by category button is a good example of a flyout that opens on click. Let's enhance the "Shop by Category" button that we added previously.

  1. Walk through the additional markup required for a flyout: root, host & content.
  2. Discuss importance of DOM order for reading order and focus order
  3. Demonstrate how flyout should close after keyboard exits
  4. Add the 3 cols class. Discuss the temptation for developers to use grid semantics here and left/right arrow keys

Final HTML:

<div class="expander">
    <button class="expander__host" type="button">Shop by Category <span aria-hidden="true" class="icon icon--arrow-down" /></button>
    <div class="expander__content three-cols">
        <h3>Category 1</h3>
        <ul>
            <li><a href="http://www.ebay.com">Sub-Category 1</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 2</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 3</a></li>
        </ul>
        <h3>Category 2</h3>
        <ul>
            <li><a href="http://www.ebay.com">Sub-Category 1</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 2</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 3</a></li>
        </ul>
    </div>
</div>
.expander {
    position: relative;
}

.expander__content {
    background-color: white;
    display: none;
    left: 0;
    padding: 0.5em 1em;
    position: absolute;
    white-space: nowrap;
    z-index: 1;
}

.expander__content ul {
    line-height: 2rem;
    list-style: none;
    margin: 0;
    padding: 0;
}

.expander--expanded .expander__content {
    display: block;
}

.three-cols ul {
    column-count: 3;
}

span.icon--arrow-down {
  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAxMyI+PHBhdGggZmlsbD0iIzc2NzY3NiIgZD0iTTExLjYwNCAxMi45MjFMMCAwaDIzLjIxNWwtNS44MDcgNi40NjJ6Ii8+PC9zdmc+');
  height: 13px;
  width: 24px;
}

We utilise a makeup plugin for the expand/collapse behaviour.

querySelectorAllToArray('.expander').forEach(function(el, i) {
    const widget = new Expander(el, {
        collapseOnClickOut: true,
        collapseOnFocusOut: true,
        expandedClass: 'expander--expanded',
        expandOnClick: true,
        focusManagement: 'interactive'
    });
});

Link Flyout

This section shows the problem of opening a flyout on hover on a link. It also shows a workaround that uses a stealth button.

"Motors" list item before we make changes:

<li>
    <a href="http://www.ebay.com/motors">Motors</a>
</li>
  1. Using makeup-expander, add hover flyout behaviour to the "Motors" category link (HTML below)
  2. Demonstrate keyboard issue
  3. Convert it to a focus flyout
  4. Demonstrate that keyboard user now has to tab through flyout.
  5. Remove focus behaviour

Interim markup of "Motors" list item:

<li class="expander expander--hover">
    <a class="expander__host" href="http://www.ebay.com/motors">Motors</a>
    <div class="expander__content three-cols">
        <h3>Category 1</h3>
        <ul>
            <li><a href="http://www.ebay.com">Sub-Category 1</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 2</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 3</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 4</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 5</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 6</a></li>
        </ul>
        <h3>Category 2</h3>
        <ul>
            <li><a href="http://www.ebay.com">Sub-Category 1</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 2</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 3</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 4</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 5</a></li>
            <li><a href="http://www.ebay.com">Sub-Category 6</a></li>
        </ul>
    </div>
</li>
  1. Add a hidden stealth button (HTML below) that allows keyboard user to expand the flyout with ENTER or SPACE.

Final markup:

<li class="expander expander--hover">
    <a class="expander__host" href="http://www.ebay.com/motors">Motors</a>
    <span class="expander expander--click">
        <button class="expander__host clipped clipped--stealth">Expand Motors</button>
        <div class="expander__content three-cols">
            <h3>Category 1</h3>
            <ul>
                <li><a href="http://www.ebay.com">Sub-Category 1</a></li>
                <li><a href="http://www.ebay.com">Sub-Category 2</a></li>
                <li><a href="http://www.ebay.com">Sub-Category 3</a></li>
                <li><a href="http://www.ebay.com">Sub-Category 4</a></li>
                <li><a href="http://www.ebay.com">Sub-Category 5</a></li>
                <li><a href="http://www.ebay.com">Sub-Category 6</a></li>
            </ul>
            <h3>Category 2</h3>
            <ul>
                <li><a href="http://www.ebay.com">Sub-Category 1</a></li>
                <li><a href="http://www.ebay.com">Sub-Category 2</a></li>
                <li><a href="http://www.ebay.com">Sub-Category 3</a></li>
                <li><a href="http://www.ebay.com">Sub-Category 4</a></li>
                <li><a href="http://www.ebay.com">Sub-Category 5</a></li>
                <li><a href="http://www.ebay.com">Sub-Category 6</a></li>
            </ul>
        </div>
    </span>
</li>

Fake Menu

Eyebrow block before changes:

<div id="eyebrow">
    <div>
        <button id="notifications" aria-label="Notifications" onclick="alert('You have no new notifications')">
            <span class="icon icon--notification"></span>
        </button>
    </div>
</div>
  1. Add role=menu and demonstrate that VoiceOver now tells user this is a menu and which keys to use
  2. Remove role=menu

Eyebrow block after all changes:

Tooltip

Notifications block before changes:

<div>
    <button accesskey="n" id="notifications" aria-label="Notifications" onclick="alert('You have no new notifications')">
        <span class="icon icon--notification"></span>
    </button>
</div>
  1. Wrap notifications button in tooltip markup
<div class="expander expander--hover expander--focus">
    <button accesskey="n" aria-describedby="tooltip" id="notifications" aria-label="Notifications" onclick="alert('You have no new notifications')">
        <span class="icon icon--notification"></span>
    </button>
    <div class="expander__content" id="tooltip">Notifications (Access Key: N)</div>
</div>
.tooltip--expanded .flyout__overlay {
    background-color: LightYellow;
    display: block;
}

JavaScript:

querySelectorAllToArray('.expander--hover-and-focus').forEach(function(el, i) {
    const widget = new Expander(el, {
        collapseOnFocusOut: true,
        collapseOnMouseOut: true,
        expandedClass: 'expander--expanded',
        expandOnFocus: true,
        expandOnHover: true
    });
});

Infotip

todo

Chapter 5: Introduces Form Controls

This chapter continues on with our sign in and registration pages.

  1. Textbox
  2. Radio
  3. Customised Radio
  4. Select
  5. Customised Select
  6. Reset
  7. Submit
  8. Textbox Icon
  9. Checkbox
  10. Customised Checkbox
  11. Field Description

Textbox

Add textbox and label for email inside of form.

<label for="email">Email</label>
<input id="email" name="email" type="text" />
  1. We call it textbox (after the ARIA role) or text field
  2. Demo that textbox is focusable with TAB key
  3. Demo how ARROW key behaviour on textbox is different than on link.
  4. Talk about ARROW key behaviour and 'forms mode' of screen reader.
  5. Demo that ENTER key does nothing... yet
  6. Demo that screen reader announces value (or contents), label, type (edit text) and state (in no particular order)
  7. The name attribute is used by the server (key/value pair). The id attribute is used by the label tag
  8. Add disabled attribute to textbox and demonstrate screen reader behaviour (it now reads "dimmed")
  9. Remove disabled attribute
  10. Add readonly attribute to textbox and demonstrate screen reader behaviour (it now just says "text" instead of "edit text")
  11. Remove readonly attribute
  12. Add autofocus attribute to textbox and demonstrate behaviour on page load
  13. Remove autofocus attribute
  14. Add skin field related classes (field, field__label field__label--stacked and field__control)

Also add textbox & label for password, first name, last name and phone, adding Skin Textbox Classes. For example, final email field should look like this:

<div class="field">
    <label class="field__label field__label--stacked" for="email">Email</label>
    <div class="field__control textbox">
        <input class="textbox__control textbox__control--fluid" id="email" name="email" type="text" />
    </div>
</div>

Add the following CSS for layout of the side-by-side fields:

.field-group {
    display: flex;
    margin: 16px 0;
}

.field-group span.field {
    width: 100%;
}

Radio

We are done with the sign in page, and now move back to our registration page.

Radio buttons are our first introduction to using the ARROW keys. The TAB key moves keyboard focus into the radio group, the ARROW keys interact with the radio group buttons. Pressing the TAB key again moves keyboard focus off the radio group onto the next interactive element on the page.

  1. Add the HTML below to the fake tabs content panel
  2. Demonstrate keyboard behaviour
    • TAB key does not move from radio to radio
    • ARROW key selects
  3. Demonstrate screen reader semantics (type, label, state)
<input id="paccount" name="account_type" type="radio" value="p" />
<label for="paccount">Personal account</label>
<input id="baccount" name="account_type" type="radio" value="b" />
<label for="baccount">Business account</label>
  1. Add a fieldset and legend (clipped)
  2. Demonstrate additional screen reader semantics (group name/label)
  3. Notice that the number and index position of radio is announced, therefore we do not need to put radios inside of lists.
<fieldset>
    <legend class="clipped">Account Type</legend>
    <input id="paccount" name="account_type" type="radio" value="p" />
    <label for="paccount">Personal account</label>
    <input id="baccount" name="account_type" type="radio" value="b" />
    <label for="baccount">Business account</label>
</fieldset>

Add margins for the fieldset:

fieldset {
    margin: 16px 0;
}

Customised Radio

Skin enhances the native radios with a custom SVG style, while maintaining the accessibility of the underlying form controls.

Anytime you need to ensure the user makes only single selection (e.g. star rating), radio buttons should be used, and their appearance can be customised using background or foreground SVG.

<span class="radio">
    <input class="field__control radio__control" name="account_type" type="radio" value="p" />
    <span class="radio__icon"></span>
</span>

If you ever use role="radio" on a tag other than input, you must ensure that all keyboard and screen reader behaviour associated with native radios is met. And remember that only the input tag supports form data and browser autofill behaviour.

Select

Add a native HTML form select with label:

<div class="field">
    <label class="field__label field__label--stacked" for="dial-code">Dialing Code</label>
    <select id="dial-code" name="dc">
        <option value="1">United States +1</option>
        <option value="44">United Kingdom +44</option>
        <option value="1">Canada +1</option>
    </select>
</div>
  1. SPACE or ARROW key expands.
  2. ARROW keys highlight options, ENTER or SPACE selects.
  3. Screen reader announces control value, label, type

Customised Select

Skin provides a custom style for the select button, but not for the overlay.

<div class="field">
    <label class="field__label field__label--stacked" for="dial-code">Dialing Code</label>
    <span class="field__control select">
        <select class="select__control select__control--fluid" id="dial-code" name="dc">
            <option value="1">United States +1</option>
            <option value="44">United Kingdom +44</option>
            <option value="1">Canada +1</option>
        </select>
        <span class="select__icon" />
    </span>
</div>

If you ever try and construct a fully custom select control (i.e. including the overlay), you must ensure that all keyboard and screen reader behaviour associated with a native select control is met. And remember that only the select tag supports form data and browser autofill behaviour.

Reset

  1. Add reset button after submit button and demo it's behaviour
  2. Notice that a reset will reset all types of form controls, even listbox and radio, this is why it's imperative we use real controls to support this behaviour

Submit

Every form requires a submit button, otherwise keyboard accessibility of form is broken (ENTER key will not work, see above).

  1. Add <button type="submit">Register</button>
  2. Notice that mouse hand cursor does not show for buttons.
  3. Screen reader announces button value/label and type
  4. Demo SPACEBAR and ENTER key behaviour
  5. Demo that form submits an HTTP GET request by default.
  6. A submit button is the only button that should navigate to a new URL in this way.
  7. Demo that keyboard navigation starts from top of new page
  8. Add skin classes to button class="btn btn--primary"
  9. Add action="page-error.html" to the form tag
<div class="field">
    <button class="btn btn--fluid btn--primary" type="submit">Register</button>
</div>

Textbox Icons

DISCLAIMER: Textboxes without visual text labels are not recommended. If you insist on using the following approach, you should make sure it is used for very short, and very familiar, forms only.

For this step we move back to our signin.html page (it should currently only consist of fake tabs and an empty form).

But first, demo some sign in pages on the web. For example, t-mobile.com, nintendo.com, chase.com. Show how it can be easy to forget the 'label' after typing (i.e. hmm, did it ask for email or username? I guess I'll have to delete my text just to make sure).

An unfortunate recent trend in web design is to use the placeholder attribute as an alternative to the label tag. We strongly discourage this behaviour, especially for long forms, because of cognitive issues caused by the transient nature of placeholder text.

  1. Copy over the email address and password textboxes from the registration page.
  2. Remove the label tags
  3. Add placeholder attributes
  4. Add SVG icons
  5. The use attribute references the ID of an SVG symbol defined on same page, or in an external SVG file
  6. Add aria-hidden="true" to SVG tag (to hide presentational image)
  7. Add focusable="false" to SVG tag (for IE)

If you must implement this pattern, at a very minimum every input must have an aria-label attribute that will act in place of the missing label tag for screen readers (another alternative is to use the previously mentioned clipped class on actual label tags). Inline icons should also be used to mitigate problems caused by the lack of permanent visual label.

<div class="field">
    <div class="field__control textbox">
        <svg aria-hidden="true" class="textbox__icon" focusable="false">
            <use xlink:href="../icons.svg#svg-icon-mail"></use>
        </svg>
        <input aria-label="Email or username" class="textbox__control textbox__control--fluid" type="text" placeholder="Email or username" />
    </div>
</div>

<div class="field">
    <div class="field__control textbox">
        <svg aria-hidden="true" class="textbox__icon" focusable="false">
            <use xlink:href="../icons.svg#svg-icon-star"></use>
        </svg>
        <input aria-label="Password" class="textbox__control textbox__control--fluid" id="password" placeholder="Password" type="password" />
    </div>
</div>

Note that some older browsers do not support symbols defined in an external SVG file. If you need to support those browsers, the SVG symbols can instead be defined in the same HTML page or a JavaScript polyfill can be used.

NOTE: I currently don't have a better icon for password! It should ideally be a padlock or something.

Fortunately, we are now starting to see a shift away from this pattern towards floating labels, which counters the problems caused by transient placeholder text.

Checkbox

Add a checkbox to the sign in page.

<input type="checkbox" name="ssi" id="ssi"/>
<label for="ssi">Stay signed in</label>
  1. Demonstrate that checkbox is in default tab order with TAB key
  2. Demonstrate that checkbox state is toggle with SPACEBAR key

Customised Checkbox

With a bit of CSS tricker, it is possible to replace the default checkbox style with inline SVG.

First create the field boiler plate, similar to what we have for textboxes:

<div class="field">
    <span class="field__control checkbox">
        <input class="checkbox__control" id="ssi" name="ssi" type="checkbox" />
        <!-- checkbox icon goes here -->
    </span>
    <label class="field__label field__label--end" for="ssi">Stay signed in</label>
</div>
  1. The checkbox control remains in place but is now fully transparent.
  2. Mention that Skin decouples the checkbox class from the label. This allows great flexibility in terms of DOM structure and visual layout (e.g. a grid system)
  3. The SVG icon is going to sit underneath the transparent input control, therefore all click events still reach the native control

Now let's talk about the SVG.

<span class="checkbox__icon" hidden>
    <svg aria-hidden="true" focusable="false">
        <use xlink:href="../icons.svg#svg-icon-checkbox"></use>
    </svg>
</span>
  1. The hidden attribute prevents the SVG from appearing in a non-CSS state (FOUC) and supports progressive enhancement scenario
  2. The hidden attribute will be overriden by the CSS to display: inline-block, therefore it is important not to forget the aria-hidden="true" property on the SVG itself
  3. The checked attribute of the input dictates which SVG path is shown
  4. Notice that the checkbox has a custom focus indicator (dotted outline), this is because we cannot show the default focus outline due to it being transparent (opacity: 0)

If you ever use role="checkbox" on an tag other than an input, you must ensure that all keyboard and screen reader behaviour associated with a native checkbox is met. And remember that only the input tag supports form data and browser autofill behaviour.

Field Description

In addition to a short label, a field might also have longer descriptive text. For example, the eBay signin page has the text "Using a public or shared device? Uncheck to protect your account." next to the checkbox.

  1. Add <p id="ssi-description">Using a public or shared device? Uncheck to protect your account.</p>
  2. Add aria-describedby="ssi-description" to the checkbox
  3. Demonstrate that voiceover reads the description after a short pause
  4. Talk about the differences between a label and a description

In voiceover the length of pause is configurable, see screenshot below.

Screenshot of the Voiceover verbosity settings

Chapter 6: Introduces Form Validation

This chapter continues with our sign and registration pages introducing server-side and client-side validation.

  1. Required Field
  2. Page Error
  3. Field Error
  4. Dynamic Page Error
  5. Dynamic Field Error

Required Field

If a field is required, we should notify the user ahead of time, before they leave the field. There is a well established pattern for this. Required fields.

  1. Add an asterisk after label text for email & password
  2. Add aria-required="true" to textbox for email & password
  3. Add checked state to personal account type radio to set it as the default
  4. Demonstrate that screen reader reads this new 'required' state

The convention for sighted users is to add an asterisk next to each field. To convey the same information to assistive technology, we use the aria-required property.

Notice that the screen reader also reads the asterisk, which isn't too disastrous, but let's address this now, with aria-hidden.

  1. Replace the asterisk inside each required field label with <span aria-hidden="true">*</span>

Page Error

Duplicate your current signin.html and name it page-error.html.

At the start of the form, add the following error region:

<section aria-labelledby="attention-status" class="page-notice page-notice--attention" id="page-error" role="region" tabindex="-1">
    <h2 class="page-notice__status" id="attention-status">
        <span aria-label="Attention" role="img"></span>
    </h2>
    <div class="page-notice__content">
        <p>Please fix the following errors:</p>
        <ul role="list">
            <li><a href="#fname">First Name: please enter your first name</a></li>
            <li><a href="#lname">Last Name: please enter your last name</a></li>
        </ul>
    </div>
</section>

Demonstrate that a labelled section shows up in the screen reader list of landmarks. We have created a "custom" landmark.

If JavaScript is available we can enhance this experience by setting focus on the page notice. This is our first introduction to the concept of focus management, but bear in mind that this is one of the very few scenarios we would consider setting focus after a full server-side page load!

  1. Add tabindex="-1" to the page notice which allows programmatic focus (or this can be done in JS also)
  2. Add the script below to set focus after page load.
  3. Notice that the message cannot be re-focussed again with keyboard after focus is lost. This is intentional.

JavaScript:

var pageError = document.getElementById('page-error');
if (pageError) {
    pageError.setAttribute('tabindex', '-1');
    pageError.focus();
}

CSS:

.page-notice--attention ul {
    list-style-type: none;
    padding: 0;
}

.page-notice--attention a {
    color: #dd1e31;
}

Notice the skip-to links (remember we covered these in chapter 2). This may be a long form with many fields in the tab order. Skip links make life easier for keyboard user to get directly to those invalids fields.

Now imagine if we have a long form, and scroll down so that the page error notice is no longer visible. We don't want to have to remember where the errors were. We need to flag the fields themselves in some way.

Field Error

A typical design approach is to make the border or color of an invalid field red. However, we must not use colour alone to convey meaning. In this case the colour red conveys the meaning of invalid. We also need to add an icon and/or text to convey the meaning.

Let's add individual error descriptions after the first and last name fields. For example:

<div class="field__description field__description--error" id="fname-error">
    <span>Please enter your first name</span>
</div>
.field__description--error {
    color: #dd1e31;
}

This works well for sighted users. They see at a glance which fields are invalid. But screen reader users do not know the field is invalid when they are on the field. The solution is to use the aria-invalid property.

<input aria-invalid="true" class="textbox__control textbox__control--fluid" id="fname" name="fname" type="text" />

We can also use this property for styling purposes.

.textbox__control[aria-invalid="true"] {
    border-color: #dd1e31;
}

Okay, so the screen reader now says the field is invalid. But why is it invalid, and how can the user fix it? The solution is the aria-describedby attribute (remember we used this for field description of the checkbox on the sign page).

<input aria-describebby="fname-error" aria-invalid="true" class="textbox__control textbox__control--fluid" id="fname" name="fname" type="text" />

Viola! Now all users are informed that the page has an error, what the user must do to fix those errors, and convenient links to go directly to the error.

Dynamic Field Error

Why reload the page just to tell a user they entered an invalid value? We can use JavaScript to validate the field at any time.

Let's go back to our reg.html page, and add hidden error messages after the email, password and phone text boxes:

<div class="field__description field__description--error" id="email-error">
    <span hidden>Please enter a valid email address</span>
</div>

Add the class .field-validation to the fields containing the email, password and phone text boxes.

Now add a simple script that does the following:

  1. Get all field validation input elements
  2. Locates the status text belonging to those inputs (using aria-describedby)
  3. Add blur event listener to each input
  4. If the value is non-empty, remove the hidden attribute
  5. Else if the value empty set the hidden attribute
document.querySelectorAll('.field-validation input').forEach(function(item) {
    var statusText = document.querySelector('#' + item.getAttribute('aria-describedby') + ' span');
    item.addEventListener('blur', function(e) {
        if (this.value) {
            statusText.removeAttribute('hidden');
        } else {
            statusText.setAttribute('hidden', 'hidden');
        }
    })
});

NOTE: forEach on a query collection is not supported in some older browsers.

Now, after a value is entered, the error message appears when the field loses focus. Of course we aren't actually doing any real validation in this simple example. We have just hardcoded the error message to appear for any non-empty value. Writing a validation routine is not in the scope of this workshop, but is a fun exercise!

The error message appears for sighted users, but a screen reader user might miss this error message entirely if they use the TAB key to skip to the next field. The solution is to convert the error message container into an ARIA live region.

If you've ever heard of ARIA live regions, and wondered what they were and when to use them, this is a perhaps one of the best use cases. The general purpose of a live region is to announce when some new content has appeared on screen without using focus management. For example, in this case we do not want to use focus management to force focus back into the invalid input. Why? Well just imagine that the user might decide they want to come back to the form later, and want to navigate down to the help links in the footer. If we force them back to the invalid input then we have effectively create a focus trap which prevents them from reaching the footer.

<div aria-live="polite" class="field__description field__description--error" id="email-error">
    <span hidden>Please enter a valid email address</span>
</div>

Note that the aria-live attribute must go on the ancestor of the dynamic content, and not on the dymamic content itself (i.e. in this example it must go on the outer div, not the inner span).

Now when the error message text appears, it will announce the new text that displayed inside of the live region. The value of polite informs assistive technology to make this announcement after all other current announcements in queue. A value of assertive would push the announcement to the front of that queue. I wish it had been called 'rude'!

Dynamic Page Error

So we've made the inline error messages appear without a round trip to the server and a full page reload. We call this input validation, or field validation. How about making the page error appear instantly too, after clicking the submit button?

Basically, all we are going to do here is render the exact same markup as in the previous page error step, but this time we will render it on the client with JavaScript.

  1. Add an ID to the form
  2. Get a JavaScript reference to the form element by id
  3. Prevent the default form submission to avoid full page load/reload.
var regForm = document.getElementById('reg-form');

if (regForm) {
    regForm.addEventListener('submit', function(e) {
        e.preventDefault();
    });
}

In order to simplify our example, let's add a placeholder element for the client-side notice into our server rendered markup:

<div class="page-error-placeholder" tabindex="-1"></div>

Then update our script to update the innerHTML of the placeholder. Yes, this is a quick and dirty method for our demonstration purposes. A real life implementation would no doubt need a more sophisticated method of constructing the notice.

if (regForm) {
    var placeholderEl = regForm.querySelector('.page-error-placeholder');
    var template = '' +
        '<section aria-labelledby="error-status" class="page-notice page-notice--priority" id="page-error" role="region">' +
            '<h2 aria-label="Error notice" class="page-notice__status" id="error-status">' +
                '<svg aria-hidden="true" focusable="false">' +
                    '<use xlink:href="../icons.svg#svg-icon-priority"></use>' +
                '</svg>' +
            '</h2>' +
            '<span class="page-notice__cell page-notice__cell--align-middle">' +
                '<p>Please fix the following errors:</p>' +
                '<ul role="list">' +
                    '<li><a href="#fname">First Name: please enter your first name</a></li>' +
                    '<li><a href="#lname">Last Name: please enter your last name</a></li>' +
                '</ul>' +
            '</span>' +
        '</section>';

    regForm.addEventListener('submit', function(e) {
        e.preventDefault();
        placeholderEl.innerHTML = template;
    });
}

This might surprise you, but the page error wont be a live region, instead we'll use focus management.

Think about it. If a user has clicked the submit button, they have signaled their intent to proceed with the form. If there is an error, it doesn't make sense to announce the live region error and just leave them on the submit button. They can keep clicking the submit button but they cannot proceed while errors remain. So instead we move them back to the page error notice, where they can continue to fix the errors in a linear fashion.

Anyway, update the event handler so that focus is set on the placeholder:

regForm.addEventListener('submit', function(e) {
    e.preventDefault();
    placeholderEl.innerHTML = template;
    placeholderEl.focus();
});

Remember that we would also need to render the inline errors on the client too!

Chapter 7: Introduces ARIA Widgets

We now move from our homepage example, to a search results page (SRP) example.

Menu

todo

Dialog

todo

Tabs

todo

Combobox

todo

Date Picker

todo

Chapter 8: Introduces Data Tables

todo