Here's a heading and some content! It's important to focus on me.
Content, content, content...lorem ipsum, etc.
IT'S A PUPPY!
<details>
<summary>OOOH, WHAT'S IN THERE?</summary>
<p>Lots of stuff and things.</p>
</details>
details {
border: 1px solid #aaa;
border-radius: 4px;
padding: .5em .5em 0;
}
summary {
font-weight: bold;
margin: -.5em -.5em 0;
padding: .5em;
}
details[open] {
padding: .5em;
}
details[open] summary {
border-bottom: 1px solid #aaa;
margin-bottom: .5em;
}
The DETAILS element supports the TOGGLE event, so adding more functionality to it is pretty straightforward.
details.addEventListener("toggle", event => {
if (details.open) {
/* the element was toggled open */
//do awesome thing!
} else {
/* the element was toggled closed */
// undo awesome thing
}
});
Tested using | Firefox with JAWS | Chrome | Safari iOS with Voiceover | Edge |
---|
A tooltip provides extra information about a form field, a link, a button, or other focusable element. It must be triggered by both focus and hover events and remains on the screen as long as the trigger has the focus. The focus does not move to the tooltip.
So, what functionality do we want from a tooltip?
In this example, we’re going to use the tooltip text as an accessible label. The tooltip trigger itself will be a <button>
. When this button is hovered over with the mouse or tabbed to using the keyboard, the tooltip text will be read aloud to a screen reader. We’ll use CSS to hide / show the tooltip on hover and focus, and the aria-labelledby
attribute to link the tooltip text to the trigger button.
<button class="notifications" aria-labelledby="tooltip-label">
<!-- Your SVG, IMG, icon here! -->
</button>
<div class="arrow_box" role="tooltip" id="tooltip-label">Hi, I'm tooltip text! Hopefully, something useful and brief.</div>
[role="tooltip"] {
display: none;
border: 2px solid black;
padding: 10px;
border-radius: 5px;
width: 40%;
}
.arrow_box {
position: relative;
background: #fff;
border: 2px solid #000;
margin-top: 15px;
}
.arrow_box:after, .arrow_box:before {
bottom: 100%;
left: 11%;
border: solid transparent;
content: "";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.arrow_box:after {
border-color: rgba(255, 255, 255, 0);
border-bottom-color: #fff;
border-width: 20px;
margin-left: -20px;
}
.arrow_box:before {
border-color: rgba(0, 0, 0, 0);
border-bottom-color: #000;
border-width: 23px;
margin-left: -23px;
}
button:hover + [role="tooltip"],
button:focus + [role="tooltip"] {
display: block;
}
button {
font-size: 1.25rem;
border-radius: 0.33em;
font-family: inherit;
width: 120px;
height: 120px;
color: #fefefe;
padding: 0.75rem;
border: 0;
background: #fff;
}
svg {
width: 100%;
}
<span class="toggletip-container">
<button type="button" aria-label="more info" data-toggletip-content="Hi! I'm the toggletip text.">i</button>
<span role="status"></span>
</span>
.toggletip-container {
position: relative;
display: inline-block;
}
/* the bubble element, added inside the toggletip live region */
.toggletip-bubble {
display: inline-block;
position: absolute;
left: 100%;
top: 0;
width: 10em;
padding: 0.5rem;
background: #000;
color: #fff;
}
button {
width: 2em;
height: 2em;
border-radius: 50%;
border: 0;
background: #000;
font-family: serif;
font-weight: bold;
color: #fff;
}
button:focus {
outline: none;
box-shadow: 0 0 0 0.25rem skyBlue;
}
/* boilerplate; nothing really to see here */
html {
font-size: 150%;
font-family: sans-serif;
}
* {
font-size: inherit;
}
(function() {
// Get all the toggletip buttons
var toggletips = demo.querySelectorAll('[data-toggletip-content]');
// Iterate over them
Array.prototype.forEach.call(toggletips, function (toggletip) {
// Get the message from the data-content element
var message = toggletip.getAttribute('data-toggletip-content');
// Get the live region element
var liveRegion = toggletip.nextElementSibling;
// Toggle the message
toggletip.addEventListener('click', function () {
liveRegion.innerHTML = '';
window.setTimeout(function() {
liveRegion.innerHTML = '<span class="toggletip-bubble">'+ message +'</span>';
}, 100);
});
// Close on outside click
demo.addEventListener('click', function (e) {
if (toggletip !== e.target) {
liveRegion.innerHTML = '';
}
});
// Remove toggletip on ESC
toggletip.addEventListener('keydown', function (e) {
if ((e.keyCode || e.which) === 27)
liveRegion.innerHTML = '';
});
// Remove on blur
toggletip.addEventListener('blur', function (e) {
liveRegion.innerHTML = '';
});
});
}());
Tested using | Firefox with NVDA | Chrome | Safari iOS with Voiceover | Edge |
---|
Accordion menus are everywhere we look on the web. With such ubiquity, you’d expect that there would be a pretty well defined standard for constructing these components. Well, there’s not. So when it comes to making accordion menus accessible, things can get tricky. While the functionality and HTML markup for accordions is pretty straightforward, some implementations are overly complex. Let’s take a look at the intended functionality of an accordion menu before we look under the hood.
Accordion menus and collapsible regions are great ways to conserve screen space on your website. It is a design element that expands in place to reveal hidden information. They allow the user to get a quick overview of the content on the page. This is especially helpful on mobile devices, where screen size is limited. Accordion menus can become unwieldy very fast, as they can push other content out of view as they expand.
In this implementation, one panel of the accordion is always expanded, and only one panel may be expanded at a time. Adapted from WAI Aria Code Patterns
<div class="demo-block">
<!-- Accordion Configuration Options
data-allow-toggle
Allow for each toggle to both open and close its section. Makes it possible for all sections to be closed. Assumes only one section may be open.
data-allow-multiple
Allow for multiple accordion sections to be expanded at the same time. Assumes data-allow-toggle otherwise the toggle on open sections would be disabled.
__________
Ex:
<div id="accordionGroup" class="Accordion" data-allow-multiple>
<div id="accordionGroup" class="Accordion" data-allow-toggle>
-->
<div id="accordionGroup" class="Accordion">
<h3>
<button aria-expanded="true"
class="Accordion-trigger"
aria-controls="sect1"
id="accordion1id">
<span class="Accordion-title">
Personal Information
<span class="Accordion-icon"></span>
</span>
</button>
</h3>
<div id="sect1"
role="region"
aria-labelledby="accordion1id"
class="Accordion-panel">
<div>
<!-- Variable content within section, may include any type of markup or interactive widgets. -->
<fieldset>
<p>
<label for="cufc1">
Name
<span aria-hidden="true">
*
</span>
:
</label>
<input type="text"
value=""
name="Name"
id="cufc1"
class="required"
aria-required="true">
</p>
<p>
<label for="cufc2">
Email
<span aria-hidden="true">
*
</span>
:
</label>
<input type="text"
value=""
name="Email"
id="cufc2"
aria-required="true">
</p>
<p>
<label for="cufc3">
Phone:
</label>
<input type="text"
value=""
name="Phone"
id="cufc3">
</p>
<p>
<label for="cufc4">
Extension:
</label>
<input type="text"
value=""
name="Ext"
id="cufc4">
</p>
<p>
<label for="cufc5">
Country:
</label>
<input type="text"
value=""
name="Country"
id="cufc5">
</p>
<p>
<label for="cufc6">
City/Province:
</label>
<input type="text"
value=""
name="City_Province"
id="cufc6">
</p>
</fieldset>
</div>
</div>
<h3>
<button aria-expanded="false"
class="Accordion-trigger"
aria-controls="sect2"
id="accordion2id">
<span class="Accordion-title">
Billing Address
<span class="Accordion-icon"></span>
</span>
</button>
</h3>
<div id="sect2"
role="region"
aria-labelledby="accordion2id"
class="Accordion-panel"
hidden="">
<div>
<fieldset>
<p>
<label for="b-add1">
Address 1:
</label>
<input type="text"
name="b-add1"
id="b-add1">
</p>
<p>
<label for="b-add2">
Address 2:
</label>
<input type="text"
name="b-add2"
id="b-add2">
</p>
<p>
<label for="b-city">
City:
</label>
<input type="text"
name="b-city"
id="b-city">
</p>
<p>
<label for="b-state">
State:
</label>
<input type="text"
name="b-state"
id="b-state">
</p>
<p>
<label for="b-zip">
Zip Code:
</label>
<input type="text"
name="b-zip"
id="b-zip">
</p>
</fieldset>
</div>
</div>
<h3>
<button aria-expanded="false"
class="Accordion-trigger"
aria-controls="sect3"
id="accordion3id">
<span class="Accordion-title">
Shipping Address
<span class="Accordion-icon"></span>
</span>
</button>
</h3>
<div id="sect3"
role="region"
aria-labelledby="accordion3id"
class="Accordion-panel"
hidden="">
<div>
<fieldset>
<p>
<label for="m-add1">
Address 1:
</label>
<input type="text"
name="m-add1"
id="m-add1">
</p>
<p>
<label for="m-add2">
Address 2:
</label>
<input type="text"
name="m-add2"
id="m-add2">
</p>
<p>
<label for="m-city">
City:
</label>
<input type="text"
name="m-city"
id="m-city">
</p>
<p>
<label for="m-state">
State:
</label>
<input type="text"
name="m-state"
id="m-state">
</p>
<p>
<label for="m-zip">
Zip Code:
</label>
<input type="text"
name="m-zip"
id="m-zip">
</p>
</fieldset>
</div>
</div>
</div>
</div>
.Accordion {
margin: 0;
padding: 0;
border: 2px solid hsl(0, 0%, 82%);
border-radius: 7px;
width: 20em;
}
.Accordion h3 {
margin: 0;
padding: 0;
}
.Accordion.focus {
border-color: hsl(216, 94%, 73%);
}
.Accordion.focus h3 {
background-color: hsl(0, 0%, 97%);
}
.Accordion > * + * {
border-top: 1px solid hsl(0, 0%, 82%);
}
.Accordion-trigger {
background: none;
color: hsl(0, 0%, 13%);
display: block;
font-size: 1rem;
font-weight: normal;
margin: 0;
padding: 1em 1.5em;
position: relative;
text-align: left;
width: 100%;
outline: none;
}
.Accordion-trigger:focus,
.Accordion-trigger:hover {
background: hsl(216, 94%, 94%);
}
.Accordion *:first-child .Accordion-trigger {
border-radius: 5px 5px 0 0;
}
button {
border-style: none;
}
.Accordion button::-moz-focus-inner {
border: 0;
}
.Accordion-title {
display: block;
pointer-events: none;
border: transparent 2px solid;
border-radius: 5px;
padding: 0.25em;
outline: none;
}
.Accordion-trigger:focus .Accordion-title {
border-color: hsl(216, 94%, 73%);
}
.Accordion-icon {
border: solid hsl(0, 0%, 62%);
border-width: 0 2px 2px 0;
height: 0.5rem;
pointer-events: none;
position: absolute;
right: 2em;
top: 50%;
transform: translateY(-60%) rotate(45deg);
width: 0.5rem;
}
.Accordion-trigger:focus .Accordion-icon,
.Accordion-trigger:hover .Accordion-icon {
border-color: hsl(216, 94%, 73%);
}
.Accordion-trigger[aria-expanded="true"] .Accordion-icon {
transform: translateY(-50%) rotate(-135deg);
}
.Accordion-panel {
margin: 0;
padding: 1em 1.5em;
}
/* For Edge bug https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4806035/ */
.Accordion-panel[hidden] {
display: none;
}
fieldset {
border: 0;
margin: 0;
padding: 0;
}
input {
border: 1px solid hsl(0, 0%, 62%);
border-radius: 0.3em;
display: block;
font-size: inherit;
padding: 0.3em 0.5em;
}
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* Simple accordion pattern example
*/
'use strict';
Array.prototype.slice.call(document.querySelectorAll('.Accordion')).forEach(function (accordion) {
// Allow for multiple accordion sections to be expanded at the same time
var allowMultiple = accordion.hasAttribute('data-allow-multiple');
// Allow for each toggle to both open and close individually
var allowToggle = (allowMultiple) ? allowMultiple : accordion.hasAttribute('data-allow-toggle');
// Create the array of toggle elements for the accordion group
var triggers = Array.prototype.slice.call(accordion.querySelectorAll('.Accordion-trigger'));
var panels = Array.prototype.slice.call(accordion.querySelectorAll('.Accordion-panel'));
accordion.addEventListener('click', function (event) {
var target = event.target;
if (target.classList.contains('Accordion-trigger')) {
// Check if the current toggle is expanded.
var isExpanded = target.getAttribute('aria-expanded') == 'true';
var active = accordion.querySelector('[aria-expanded="true"]');
// without allowMultiple, close the open accordion
if (!allowMultiple && active && active !== target) {
// Set the expanded state on the triggering element
active.setAttribute('aria-expanded', 'false');
// Hide the accordion sections, using aria-controls to specify the desired section
document.getElementById(active.getAttribute('aria-controls')).setAttribute('hidden', '');
// When toggling is not allowed, clean up disabled state
if (!allowToggle) {
active.removeAttribute('aria-disabled');
}
}
if (!isExpanded) {
// Set the expanded state on the triggering element
target.setAttribute('aria-expanded', 'true');
// Hide the accordion sections, using aria-controls to specify the desired section
document.getElementById(target.getAttribute('aria-controls')).removeAttribute('hidden');
// If toggling is not allowed, set disabled state on trigger
if (!allowToggle) {
target.setAttribute('aria-disabled', 'true');
}
}
else if (allowToggle && isExpanded) {
// Set the expanded state on the triggering element
target.setAttribute('aria-expanded', 'false');
// Hide the accordion sections, using aria-controls to specify the desired section
document.getElementById(target.getAttribute('aria-controls')).setAttribute('hidden', '');
}
event.preventDefault();
}
});
// Bind keyboard behaviors on the main accordion container
accordion.addEventListener('keydown', function (event) {
var target = event.target;
var key = event.which.toString();
var isExpanded = target.getAttribute('aria-expanded') == 'true';
var allowToggle = (allowMultiple) ? allowMultiple : accordion.hasAttribute('data-allow-toggle');
// 33 = Page Up, 34 = Page Down
var ctrlModifier = (event.ctrlKey && key.match(/33|34/));
// Is this coming from an accordion header?
if (target.classList.contains('Accordion-trigger')) {
// Up/ Down arrow and Control + Page Up/ Page Down keyboard operations
// 38 = Up, 40 = Down
if (key.match(/38|40/) || ctrlModifier) {
var index = triggers.indexOf(target);
var direction = (key.match(/34|40/)) ? 1 : -1;
var length = triggers.length;
var newIndex = (index + length + direction) % length;
triggers[newIndex].focus();
event.preventDefault();
}
else if (key.match(/35|36/)) {
// 35 = End, 36 = Home keyboard operations
switch (key) {
// Go to first accordion
case '36':
triggers[0].focus();
break;
// Go to last accordion
case '35':
triggers[triggers.length - 1].focus();
break;
}
event.preventDefault();
}
}
});
// These are used to style the accordion when one of the buttons has focus
accordion.querySelectorAll('.Accordion-trigger').forEach(function (trigger) {
trigger.addEventListener('focus', function (event) {
accordion.classList.add('focus');
});
trigger.addEventListener('blur', function (event) {
accordion.classList.remove('focus');
});
});
// Minor setup: will set disabled state, via aria-disabled, to an
// expanded/ active accordion which is not allowed to be toggled close
if (!allowToggle) {
// Get the first expanded/ active accordion
var expanded = accordion.querySelector('[aria-expanded="true"]');
// If an expanded/ active accordion is found, disable
if (expanded) {
expanded.setAttribute('aria-disabled', 'true');
}
}
});
In this implementation, multiple panels of the accordion can be expanded and / or collapsed at a time. Adapted from WAI Aria Code Patterns
<div class="demo-block">
<div id="accordionGroup" class="Accordion" data-allow-multiple>
<h3>
<button aria-expanded="true"
class="Accordion-trigger"
aria-controls="sect1"
id="accordion1id">
<span class="Accordion-title">
Personal Information
<span class="Accordion-icon"></span>
</span>
</button>
</h3>
<div id="sect1"
role="region"
aria-labelledby="accordion1id"
class="Accordion-panel">
<div>
<!-- Variable content within section, may include any type of markup or interactive widgets. -->
<fieldset>
<p>
<label for="cufc1">
Name
<span aria-hidden="true">
*
</span>
:
</label>
<input type="text"
value=""
name="Name"
id="cufc1"
class="required"
aria-required="true">
</p>
<p>
<label for="cufc2">
Email
<span aria-hidden="true">
*
</span>
:
</label>
<input type="text"
value=""
name="Email"
id="cufc2"
aria-required="true">
</p>
<p>
<label for="cufc3">
Phone:
</label>
<input type="text"
value=""
name="Phone"
id="cufc3">
</p>
<p>
<label for="cufc4">
Extension:
</label>
<input type="text"
value=""
name="Ext"
id="cufc4">
</p>
<p>
<label for="cufc5">
Country:
</label>
<input type="text"
value=""
name="Country"
id="cufc5">
</p>
<p>
<label for="cufc6">
City/Province:
</label>
<input type="text"
value=""
name="City_Province"
id="cufc6">
</p>
</fieldset>
</div>
</div>
<h3>
<button aria-expanded="false"
class="Accordion-trigger"
aria-controls="sect2"
id="accordion2id">
<span class="Accordion-title">
Billing Address
<span class="Accordion-icon"></span>
</span>
</button>
</h3>
<div id="sect2"
role="region"
aria-labelledby="accordion2id"
class="Accordion-panel"
hidden="">
<div>
<fieldset>
<p>
<label for="b-add1">
Address 1:
</label>
<input type="text"
name="b-add1"
id="b-add1">
</p>
<p>
<label for="b-add2">
Address 2:
</label>
<input type="text"
name="b-add2"
id="b-add2">
</p>
<p>
<label for="b-city">
City:
</label>
<input type="text"
name="b-city"
id="b-city">
</p>
<p>
<label for="b-state">
State:
</label>
<input type="text"
name="b-state"
id="b-state">
</p>
<p>
<label for="b-zip">
Zip Code:
</label>
<input type="text"
name="b-zip"
id="b-zip">
</p>
</fieldset>
</div>
</div>
<h3>
<button aria-expanded="false"
class="Accordion-trigger"
aria-controls="sect3"
id="accordion3id">
<span class="Accordion-title">
Shipping Address
<span class="Accordion-icon"></span>
</span>
</button>
</h3>
<div id="sect3"
role="region"
aria-labelledby="accordion3id"
class="Accordion-panel"
hidden="">
<div>
<fieldset>
<p>
<label for="m-add1">
Address 1:
</label>
<input type="text"
name="m-add1"
id="m-add1">
</p>
<p>
<label for="m-add2">
Address 2:
</label>
<input type="text"
name="m-add2"
id="m-add2">
</p>
<p>
<label for="m-city">
City:
</label>
<input type="text"
name="m-city"
id="m-city">
</p>
<p>
<label for="m-state">
State:
</label>
<input type="text"
name="m-state"
id="m-state">
</p>
<p>
<label for="m-zip">
Zip Code:
</label>
<input type="text"
name="m-zip"
id="m-zip">
</p>
</fieldset>
</div>
</div>
</div>
</div>
.Accordion {
margin: 0;
padding: 0;
border: 2px solid hsl(0, 0%, 82%);
border-radius: 7px;
width: 20em;
}
.Accordion h3 {
margin: 0;
padding: 0;
}
.Accordion.focus {
border-color: hsl(216, 94%, 73%);
}
.Accordion.focus h3 {
background-color: hsl(0, 0%, 97%);
}
.Accordion > * + * {
border-top: 1px solid hsl(0, 0%, 82%);
}
.Accordion-trigger {
background: none;
color: hsl(0, 0%, 13%);
display: block;
font-size: 1rem;
font-weight: normal;
margin: 0;
padding: 1em 1.5em;
position: relative;
text-align: left;
width: 100%;
outline: none;
}
.Accordion-trigger:focus,
.Accordion-trigger:hover {
background: hsl(216, 94%, 94%);
}
.Accordion *:first-child .Accordion-trigger {
border-radius: 5px 5px 0 0;
}
button {
border-style: none;
}
.Accordion button::-moz-focus-inner {
border: 0;
}
.Accordion-title {
display: block;
pointer-events: none;
border: transparent 2px solid;
border-radius: 5px;
padding: 0.25em;
outline: none;
}
.Accordion-trigger:focus .Accordion-title {
border-color: hsl(216, 94%, 73%);
}
.Accordion-icon {
border: solid hsl(0, 0%, 62%);
border-width: 0 2px 2px 0;
height: 0.5rem;
pointer-events: none;
position: absolute;
right: 2em;
top: 50%;
transform: translateY(-60%) rotate(45deg);
width: 0.5rem;
}
.Accordion-trigger:focus .Accordion-icon,
.Accordion-trigger:hover .Accordion-icon {
border-color: hsl(216, 94%, 73%);
}
.Accordion-trigger[aria-expanded="true"] .Accordion-icon {
transform: translateY(-50%) rotate(-135deg);
}
.Accordion-panel {
margin: 0;
padding: 1em 1.5em;
}
/* For Edge bug https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4806035/ */
.Accordion-panel[hidden] {
display: none;
}
fieldset {
border: 0;
margin: 0;
padding: 0;
}
input {
border: 1px solid hsl(0, 0%, 62%);
border-radius: 0.3em;
display: block;
font-size: inherit;
padding: 0.3em 0.5em;
}
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* Simple accordion pattern example
*/
'use strict';
Array.prototype.slice.call(document.querySelectorAll('.Accordion')).forEach(function (accordion) {
// Allow for multiple accordion sections to be expanded at the same time
var allowMultiple = accordion.hasAttribute('data-allow-multiple');
// Allow for each toggle to both open and close individually
var allowToggle = (allowMultiple) ? allowMultiple : accordion.hasAttribute('data-allow-toggle');
// Create the array of toggle elements for the accordion group
var triggers = Array.prototype.slice.call(accordion.querySelectorAll('.Accordion-trigger'));
var panels = Array.prototype.slice.call(accordion.querySelectorAll('.Accordion-panel'));
accordion.addEventListener('click', function (event) {
var target = event.target;
if (target.classList.contains('Accordion-trigger')) {
// Check if the current toggle is expanded.
var isExpanded = target.getAttribute('aria-expanded') == 'true';
var active = accordion.querySelector('[aria-expanded="true"]');
// without allowMultiple, close the open accordion
if (!allowMultiple && active && active !== target) {
// Set the expanded state on the triggering element
active.setAttribute('aria-expanded', 'false');
// Hide the accordion sections, using aria-controls to specify the desired section
document.getElementById(active.getAttribute('aria-controls')).setAttribute('hidden', '');
// When toggling is not allowed, clean up disabled state
if (!allowToggle) {
active.removeAttribute('aria-disabled');
}
}
if (!isExpanded) {
// Set the expanded state on the triggering element
target.setAttribute('aria-expanded', 'true');
// Hide the accordion sections, using aria-controls to specify the desired section
document.getElementById(target.getAttribute('aria-controls')).removeAttribute('hidden');
// If toggling is not allowed, set disabled state on trigger
if (!allowToggle) {
target.setAttribute('aria-disabled', 'true');
}
}
else if (allowToggle && isExpanded) {
// Set the expanded state on the triggering element
target.setAttribute('aria-expanded', 'false');
// Hide the accordion sections, using aria-controls to specify the desired section
document.getElementById(target.getAttribute('aria-controls')).setAttribute('hidden', '');
}
event.preventDefault();
}
});
// Bind keyboard behaviors on the main accordion container
accordion.addEventListener('keydown', function (event) {
var target = event.target;
var key = event.which.toString();
var isExpanded = target.getAttribute('aria-expanded') == 'true';
var allowToggle = (allowMultiple) ? allowMultiple : accordion.hasAttribute('data-allow-toggle');
// 33 = Page Up, 34 = Page Down
var ctrlModifier = (event.ctrlKey && key.match(/33|34/));
// Is this coming from an accordion header?
if (target.classList.contains('Accordion-trigger')) {
// Up/ Down arrow and Control + Page Up/ Page Down keyboard operations
// 38 = Up, 40 = Down
if (key.match(/38|40/) || ctrlModifier) {
var index = triggers.indexOf(target);
var direction = (key.match(/34|40/)) ? 1 : -1;
var length = triggers.length;
var newIndex = (index + length + direction) % length;
triggers[newIndex].focus();
event.preventDefault();
}
else if (key.match(/35|36/)) {
// 35 = End, 36 = Home keyboard operations
switch (key) {
// Go to first accordion
case '36':
triggers[0].focus();
break;
// Go to last accordion
case '35':
triggers[triggers.length - 1].focus();
break;
}
event.preventDefault();
}
}
});
// These are used to style the accordion when one of the buttons has focus
accordion.querySelectorAll('.Accordion-trigger').forEach(function (trigger) {
trigger.addEventListener('focus', function (event) {
accordion.classList.add('focus');
});
trigger.addEventListener('blur', function (event) {
accordion.classList.remove('focus');
});
});
// Minor setup: will set disabled state, via aria-disabled, to an
// expanded/ active accordion which is not allowed to be toggled close
if (!allowToggle) {
// Get the first expanded/ active accordion
var expanded = accordion.querySelector('[aria-expanded="true"]');
// If an expanded/ active accordion is found, disable
if (expanded) {
expanded.setAttribute('aria-disabled', 'true');
}
}
});
Modals are intended to be used as a quick and simple way to capture an interaction from a user. They trap the user’s focus (visual and navigational) in a window that is separated from the rest of the page content, blocking access to the contents on the main page until the modal is closed by the user. These modal windows are overlaid over the main page content, trapping keyboard focus in their windows, and blurring out or dimming the main page content.
<!--NOTE: Example using Bootstrap 4.4.1 -->
<!-- Button trigger modal -->
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
</head>
<div class="container-fluid">
<div class="row main-content">
<div class="col-md-6 offset-md-3">
<button id="launchModal" type="button" class="btn btn-light btn-lg btn-block">
Launch Modal
</button>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Accessible Modal Example</h5>
<button type="button" class="close closeModal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p tabindex="0">Set a tabindex on this P element to make sure first element in modal gets focus. Normally non-focusable elements that are the first element in a modal need to recieve focus.</p>
<form>
<div class="form-row">
<div class="form-group col-md-6">
<label for="inputEmail4">Email</label>
<input type="email" class="form-control" id="inputEmail4" placeholder="Email" title="Please enter your email." required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="inputCity">City</label>
<input type="text" class="form-control" id="inputCity" title="Please enter your city." required>
</div>
<div class="form-group col-md-4">
<label for="inputState">State</label>
<select id="inputState" class="form-control">
<option selected>Choose...</option>
<option>OR</option>
<option>CA</option>
<option>NY</option>
</select>
</div>
<div class="form-group col-md-2">
<label for="inputZip">Zip</label>
<input type="text" class="form-control" id="inputZip" title="Please enter your zip code." required>
</div>
</div>
<div>
</div>
<p>Make sure that error messages have been added to all inputs.</p>
</div>
<div class="modal-footer">
<input role="button" type="submit" class="btn btn-primary closeModal" value="Submit">
</form>
</div>
</div>
</div>
</div>
<div class="modal-overlay"></div>
body {
background: rgb(59, 45, 63);
background: radial-gradient(
circle,
rgba(59, 45, 63, 0.5186449579831933) 0%,
rgba(76, 64, 77, 1) 100%
);
}
#launchModal {
margin-top: 150px;
}
.modal-overlay {
width: 100%;
height: 100%;
z-index: 2; /* places the modalOverlay between the main page and the modal dialog */
background-color: #000;
opacity: 0.5;
position: fixed;
top: 0;
left: 0;
display: none;
margin: 0;
padding: 0;
}
.main-content {
height: 60vh;
}
// Will hold previously focused element before modal was opened
let beforeModalOpenedFocus;
// Find the modal and its overlay
let modal = document.querySelector(".modal");
let modalOverlay = document.querySelector(".modal-overlay");
let openModalBtn = document.querySelector("#launchModal");
openModalBtn.addEventListener("click", openModal);
function openModal() {
// Save current focus
beforeModalOpenedFocus = document.activeElement;
// Listen for and trap the keyboard
modal.addEventListener("keydown", trapTabKey);
// change aria-hidden state
modal.setAttribute("aria-hidden", "false");
// Listen for indicators to close the modal
modalOverlay.addEventListener("click", closeModal);
// Sign-Up button
const closeModalBtn = modal.querySelector(".closeModal");
closeModalBtn.addEventListener("click", closeModal);
// Find all of the focusable children / elements
let focusableElementsString =
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
let focusableElements = modal.querySelectorAll(focusableElementsString);
// Convert NodeList to Array
focusableElements = Array.prototype.slice.call(focusableElements);
const firstTabStop = focusableElements[0];
const lastTabStop = focusableElements[focusableElements.length - 1];
// Show the modal and overlay
modal.style.display = "block";
modalOverlay.style.display = "block";
// Focus first child
firstTabStop.focus();
function trapTabKey(e) {
// Check for TAB key press
if (e.keyCode === 9) {
// SHIFT + TAB
if (e.shiftKey) {
if (document.activeElement === firstTabStop) {
e.preventDefault();
lastTabStop.focus();
}
// TAB
} else {
if (document.activeElement === lastTabStop) {
e.preventDefault();
firstTabStop.focus();
}
}
}
// ESCAPE
if (e.keyCode === 27) {
closeModal();
}
}
}
function closeModal() {
// Hide the modal and overlay
modal.style.display = "none";
modalOverlay.style.display = "none";
// change aria-hidden state
modal.setAttribute("aria-hidden", "true");
// Set focus back to element that had it before the modal was opened
beforeModalOpenedFocus.focus();
}
Here is an example of the menu button being activated on the click event.
<nav>
<button id="menu-btn-example1" aria-expanded="false" aria-haspopup="true">Press me!</button>
<ul role="menu" class="menu-btn-example-ul" hidden>
<li>
<a href="#" role="menuitem">Option 1</a>
</li>
<li>
<a href="#" role="menuitem">Option 2</a>
</li>
<li>
<a href="#" role="menuitem">Option 3</a>
</li>
</ul>
</nav>
button {
font-size: 1.25rem;
border-radius: 0.33em;
font-family: inherit;
background: #111;
color: #fefefe;
padding: 0.75rem;
border: 0;
}
ul {
list-style: none;
width: 150px;
margin-top: 0px;
padding-left: 0px;
border-bottom-left-radius: 0.33em;
border-bottom-right-radius: 0.33em;
border-top-right-radius: 0.33em;
border: 1px solid black;
}
ul > li {
font-size: 1.25rem;
font-family: inherit;
background: #fff;
color: #000;
padding: 0.75rem;
border: 1px solid black;
}
ul > li a {
width: 100%;
}
ul > li:hover {
background-color: aliceblue;
}
ul > li a:focus {
background-color: aliceblue;
}
const menuButton = document.getElementById('menu-btn-example');
menuButton.addEventListener('click', function(){
let expanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', !expanded);
let exampleMenu = this.nextElementSibling;
exampleMenu.hidden = !exampleMenu.hidden;
})
Alerts are an important type of ARIA live region which provide screen readers with ways to announce meaningful information to a user. These alerts are triggered programmatically, usually based on a user interaction or timer. They announce text to the use without moving keyboard focus. Since they are intended to stand out from other content on the page, they should be styled and positioned on the page close to the user’s mouse or element that has keyboard focus.
Alert! There's something very wrong here.
<div class="alertGroup" >
<div id="alertBox" class="demo-hidden" role="alert" aria-live="polite"></div>
<button id="yass">Success!</button>
<button id="error">Error</button>
<button id="clear">Clear alert</button>
<button id="launchModal">
Alert Dialog
</button>
</div>
<!-- Modal -->
<div class="modal" aria-modal="true" id="exampleModal" tabindex="-1" role="alertdialog" aria-labelledby="launchModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<p tabindex="0">Alert! There's something very wrong here.</p>
<button type="button" class="close closeModal" aria-label="Close">
Close
</button>
</div>
</div>
<div class="modal-overlay"></div>
.demo-hidden {
display: none;
visibility: hidden;
}
#alertBox {
padding: 20px;
}
.yes {
color: green;
border: 2px solid green;
border-radius: 10px;
}
.no {
color: red;
border: 2px solid red;
border-radius: 10px;
}
.modal {
position: fixed;
top: 30%;
left: 50%;
z-index: 5;
display: none;
}
.modal-dialog {
border: 2px solid black;
border-radius: 20px;
padding: 10px;
text-align: center;
}
.modal-overlay {
width: 100vw;
height: 100vh;
z-index: 2; /* places the modalOverlay between the main page and the modal dialog */
background-color: #000;
opacity: 0.5;
position: fixed;
top: 0;
left: 0;
display: none;
margin: 0;
padding: 0;
}```
const alert = document.getElementById("alertBox");
const goodJob = document.getElementById("yass");
const badJob = document.getElementById("error");
const clear = document.getElementById("clear");
badJob.addEventListener("click", () => {
activateAlert("Whoa, there's something wrong here...", "no" );
});
goodJob.addEventListener("click", () => {
activateAlert("Success! Such a good job!", "yes" );
});
clear.addEventListener("click", () => {
alert.classList.add("demo-hidden");
});
function activateAlert(alertMsg, alertClass){
alert.className = alertClass;
alert.innerHTML = "";
alert.classList.remove("demo-hidden");
alert.innerHTML = alertMsg;
}
//for alertdialog
// Will hold previously focused element before modal was opened
let beforeModalOpenedFocus;
// Find the modal and its overlay
let modal = document.querySelector(".modal");
let modalOverlay = document.querySelector(".modal-overlay");
let openModalBtn = document.querySelector("#launchModal");
openModalBtn.addEventListener("click", openModal);
function openModal() {
// Save current focus
beforeModalOpenedFocus = document.activeElement;
// Listen for and trap the keyboard
modal.addEventListener("keydown", trapTabKey);
// change aria-hidden state
modal.setAttribute("aria-hidden", "false");
// Listen for indicators to close the modal
modalOverlay.addEventListener("click", closeModal);
// Sign-Up button
const closeModalBtn = modal.querySelector(".closeModal");
closeModalBtn.addEventListener("click", closeModal);
// Find all of the focusable children / elements
let focusableElementsString =
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
let focusableElements = modal.querySelectorAll(focusableElementsString);
// Convert NodeList to Array
focusableElements = Array.prototype.slice.call(focusableElements);
const firstTabStop = focusableElements[0];
const lastTabStop = focusableElements[focusableElements.length - 1];
// Show the modal and overlay
modal.style.display = "block";
modalOverlay.style.display = "block";
// Focus first child
firstTabStop.focus();
function trapTabKey(e) {
// Check for TAB key press
if (e.keyCode === 9) {
// SHIFT + TAB
if (e.shiftKey) {
if (document.activeElement === firstTabStop) {
e.preventDefault();
lastTabStop.focus();
}
// TAB
} else {
if (document.activeElement === lastTabStop) {
e.preventDefault();
firstTabStop.focus();
}
}
}
// ESCAPE
if (e.keyCode === 27) {
closeModal();
}
}
}
function closeModal() {
// Hide the modal and overlay
modal.style.display = "none";
modalOverlay.style.display = "none";
// change aria-hidden state
modal.setAttribute("aria-hidden", "true");
// Set focus back to element that had it before the modal was opened
beforeModalOpenedFocus.focus();
}
Tested using | Firefox with JAWS | Chrome | Safari iOS with Voiceover | IE with JAWS | Edge with Narrator |
---|
Accessible content slider / carousel, ported from Inclusive Components.
Accessible datepicker, ported from Inclusive Dates.
Interactive tables are powerful widgets that can provide many different layers of fucntionality. They provide a variety of different fucntions, which can be as simple as sorting rows or as complex as duplicating spreadsheet functions.
Date | Type | Description | Category | Amount | Balance |
---|---|---|---|---|---|
01-Jan-16 | Deposit |
Cash Deposit
|
|
$1,000,000.00 | $1,000,000.00 |
02-Jan-16 | Debit |
Down Town
Grocery
|
|
$250.00 | $999,750.00 |
03-Jan-16 | Debit |
Hot Coffee
|
|
$9.00 | $999,741.00 |
04-Jan-16 | Debit |
The Filling
Station
|
|
$88.00 | $999,653.00 |
05-Jan-16 | Debit |
Tinker's
Hardware
|
|
$3,421.00 | $996,232.00 |
06-Jan-16 | Debit |
Cutey's Salon
|
|
$700.00 | $995,532.00 |
07-Jan-16 | Debit |
My Chocolate
Shop
|
|
$41.00 | $995,491.00 |
<h4 id="grid2Label">
Transactions January 1 through January 7
</h4>
<table role="grid"
aria-labelledby="grid2Label"
class="data">
<tbody>
<tr>
<th aria-sort="ascending">
<span tabindex="-1" role="button">
Date
</span>
</th>
<th tabindex="-1">
Type
</th>
<th tabindex="-1">
Description
</th>
<th tabindex="-1">
Category
</th>
<th aria-sort="none">
<span tabindex="-1" role="button">
Amount
</span>
</th>
<th tabindex="-1">
Balance
</th>
</tr>
<tr>
<td tabindex="-1">
01-Jan-16
</td>
<td tabindex="-1">
Deposit
</td>
<td>
<div class="editable-text">
<span class="edit-text-button"
tabindex="-1"
role="button">
Cash Deposit
</span>
<input class="edit-text-input hidden"
tabindex="-1"
value="">
</div>
</td>
<td class="menu-wrapper">
<button tabindex="-1"
aria-haspopup="true"
aria-controls="menu1">
Income
</button>
<ul role="menu" id="menu1">
<li role="menuitem">
Income
</li>
<li role="menuitem">
Groceries
</li>
<li role="menuitem">
Dining Out
</li>
<li role="menuitem">
Auto
</li>
<li role="menuitem">
Household
</li>
<li role="menuitem">
Beauty
</li>
</ul>
</td>
<td tabindex="-1">
$1,000,000.00
</td>
<td tabindex="-1">
$1,000,000.00
</td>
</tr>
<tr>
<td tabindex="-1">
02-Jan-16
</td>
<td tabindex="-1">
Debit
</td>
<td>
<div class="editable-text">
<span class="edit-text-button"
tabindex="-1"
role="button">
Down Town
Grocery
</span>
<input class="edit-text-input hidden"
tabindex="-1"
value="">
</div>
</td>
<td class="menu-wrapper">
<button tabindex="-1"
aria-haspopup="true"
aria-controls="menu2">
Groceries
</button>
<ul role="menu" id="menu2">
<li role="menuitem">
Income
</li>
<li role="menuitem">
Groceries
</li>
<li role="menuitem">
Dining Out
</li>
<li role="menuitem">
Auto
</li>
<li role="menuitem">
Household
</li>
<li role="menuitem">
Beauty
</li>
</ul>
</td>
<td tabindex="-1">
$250.00
</td>
<td tabindex="-1">
$999,750.00
</td>
</tr>
<tr>
<td tabindex="-1">
03-Jan-16
</td>
<td tabindex="-1">
Debit
</td>
<td>
<div class="editable-text">
<span class="edit-text-button"
tabindex="-1"
role="button">
Hot Coffee
</span>
<input class="edit-text-input hidden"
tabindex="-1"
value="">
</div>
</td>
<td class="menu-wrapper">
<button tabindex="-1"
aria-haspopup="true"
aria-controls="menu3">
Dining Out
</button>
<ul role="menu" id="menu3">
<li role="menuitem">
Income
</li>
<li role="menuitem">
Groceries
</li>
<li role="menuitem">
Dining Out
</li>
<li role="menuitem">
Auto
</li>
<li role="menuitem">
Household
</li>
<li role="menuitem">
Beauty
</li>
</ul>
</td>
<td tabindex="-1">
$9.00
</td>
<td tabindex="-1">
$999,741.00
</td>
</tr>
<tr>
<td tabindex="-1">
04-Jan-16
</td>
<td tabindex="-1">
Debit
</td>
<td>
<div class="editable-text">
<span class="edit-text-button"
tabindex="-1"
role="button">
The Filling
Station
</span>
<input class="edit-text-input hidden"
tabindex="-1"
value="">
</div>
</td>
<td class="menu-wrapper">
<button tabindex="-1"
aria-haspopup="true"
aria-controls="menu4">
Auto
</button>
<ul role="menu" id="menu4">
<li role="menuitem">
Income
</li>
<li role="menuitem">
Groceries
</li>
<li role="menuitem">
Dining Out
</li>
<li role="menuitem">
Auto
</li>
<li role="menuitem">
Household
</li>
<li role="menuitem">
Beauty
</li>
</ul>
</td>
<td tabindex="-1">
$88.00
</td>
<td tabindex="-1">
$999,653.00
</td>
</tr>
<tr>
<td tabindex="-1">
05-Jan-16
</td>
<td tabindex="-1">
Debit
</td>
<td>
<div class="editable-text">
<span class="edit-text-button"
tabindex="-1"
role="button">
Tinker's
Hardware
</span>
<input class="edit-text-input hidden"
tabindex="-1"
value="">
</div>
</td>
<td class="menu-wrapper">
<button tabindex="-1"
aria-haspopup="true"
aria-controls="menu5">
Household
</button>
<ul role="menu" id="menu5">
<li role="menuitem">
Income
</li>
<li role="menuitem">
Groceries
</li>
<li role="menuitem">
Dining Out
</li>
<li role="menuitem">
Auto
</li>
<li role="menuitem">
Household
</li>
<li role="menuitem">
Beauty
</li>
</ul>
</td>
<td tabindex="-1">
$3,421.00
</td>
<td tabindex="-1">
$996,232.00
</td>
</tr>
<tr>
<td tabindex="-1">
06-Jan-16
</td>
<td tabindex="-1">
Debit
</td>
<td>
<div class="editable-text">
<span class="edit-text-button"
tabindex="-1"
role="button">
Cutey's Salon
</span>
<input class="edit-text-input hidden"
tabindex="-1"
value="">
</div>
</td>
<td class="menu-wrapper">
<button tabindex="-1"
aria-haspopup="true"
aria-controls="menu6">
Beauty
</button>
<ul role="menu" id="menu6">
<li role="menuitem">
Income
</li>
<li role="menuitem">
Groceries
</li>
<li role="menuitem">
Dining Out
</li>
<li role="menuitem">
Auto
</li>
<li role="menuitem">
Household
</li>
<li role="menuitem">
Beauty
</li>
</ul>
</td>
<td tabindex="-1">
$700.00
</td>
<td tabindex="-1">
$995,532.00
</td>
</tr>
<tr>
<td tabindex="-1">
07-Jan-16
</td>
<td tabindex="-1">
Debit
</td>
<td>
<div class="editable-text">
<span class="edit-text-button"
tabindex="-1"
role="button">
My Chocolate
Shop
</span>
<input class="edit-text-input hidden"
tabindex="-1"
value="">
</div>
</td>
<td class="menu-wrapper">
<button tabindex="-1"
aria-haspopup="true"
aria-controls="menu7">
Dining Out
</button>
<ul role="menu" id="menu7">
<li role="menuitem">
Income
</li>
<li role="menuitem">
Groceries
</li>
<li role="menuitem">
Dining Out
</li>
<li role="menuitem">
Auto
</li>
<li role="menuitem">
Household
</li>
<li role="menuitem">
Beauty
</li>
</ul>
</td>
<td tabindex="-1">
$41.00
</td>
<td tabindex="-1">
$995,491.00
</td>
</tr>
</tbody>
</table>
.annotate {
font-style: italic;
color: #366ed4;
}
.hidden {
display: none !important;
}
[role="button"] {
cursor: pointer;
}
[aria-sort="ascending"] {
position: relative;
}
[aria-sort="ascending"]::after {
content: " ";
border-bottom: 0.4em solid black;
border-left: 0.4em solid transparent;
border-right: 0.4em solid transparent;
position: absolute;
right: 1em;
top: 0.8em;
}
[aria-sort="descending"] {
position: relative;
}
[aria-sort="descending"]::after {
content: " ";
border-left: 0.4em solid transparent;
border-right: 0.4em solid transparent;
border-top: 0.4em solid black;
position: absolute;
right: 1em;
top: 0.8em;
}
.edit-text-button {
color: #360;
display: block;
position: relative;
}
.edit-text-button::after {
background-image: url('../imgs/pencil-icon.png');
background-position: center;
background-repeat: no-repeat;
background-size: 44px;
content: ' ';
height: 17px;
opacity: 0.6;
position: absolute;
right: -24px;
top: 0;
width: 20px;
}
.edit-text-button:hover,
.edit-text-button:focus {
color: black;
}
.edit-text-button:hover::after,
.edit-text-button:focus::after {
opacity: 1;
}
[role="gridcell"]:focus,
[role="gridcell"] *:focus,
[role="grid"] [tabindex="0"]:focus {
outline: #005a9c;
outline-style: dotted;
outline-width: 3px;
}
#arrow-keys-indicator {
bottom: 10px;
left: 0;
position: fixed;
height: 65px;
width: 85px;
background: url('../imgs/black_keys.png') no-repeat;
background-size: contain;
}
@media screen and (max-width: 1000px) {
#arrow-keys-indicator {
display: none;
}
}
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*/
/**
* @namespace aria
*/
var aria = aria || {};
/**
* @desc
* Values for aria-sort
*/
aria.SortType = {
ASCENDING: 'ascending',
DESCENDING: 'descending',
NONE: 'none'
};
/**
* @desc
* DOM Selectors to find the grid components
*/
aria.GridSelector = {
ROW: 'tr, [role="row"]',
CELL: 'th, td, [role="gridcell"]',
SCROLL_ROW: 'tr:not([data-fixed]), [role="row"]',
SORT_HEADER: 'th[aria-sort]',
TABBABLE: '[tabindex="0"]'
};
/**
* @desc
* CSS Class names
*/
aria.CSSClass = {
HIDDEN: 'hidden'
};
/**
* @constructor
*
* @desc
* Grid object representing the state and interactions for a grid widget
*
* Assumptions:
* All focusable cells initially have tabindex="-1"
* Produces a fully filled in mxn grid (with no holes)
*
* @param gridNode
* The DOM node pointing to the grid
*/
aria.Grid = function (gridNode) {
this.navigationDisabled = false;
this.gridNode = gridNode;
this.paginationEnabled = this.gridNode.hasAttribute('data-per-page');
this.shouldWrapCols = this.gridNode.hasAttribute('data-wrap-cols');
this.shouldWrapRows = this.gridNode.hasAttribute('data-wrap-rows');
this.shouldRestructure = this.gridNode.hasAttribute('data-restructure');
this.topIndex = 0;
this.keysIndicator = document.getElementById('arrow-keys-indicator');
aria.Utils.bindMethods(this,
'checkFocusChange', 'checkPageChange', 'checkRestructureGrid',
'delegateButtonHandler', 'focusClickedCell', 'restructureGrid',
'showKeysIndicator', 'hideKeysIndicator');
this.setupFocusGrid();
this.setFocusPointer(0, 0);
if (this.paginationEnabled) {
this.setupPagination();
}
else {
this.perPage = this.grid.length;
}
this.registerEvents();
};
/**
* @desc
* Creates a 2D array of the focusable cells in the grid.
*/
aria.Grid.prototype.setupFocusGrid = function () {
this.grid = [];
Array.prototype.forEach.call(
this.gridNode.querySelectorAll(aria.GridSelector.ROW),
(function (row) {
var rowCells = [];
Array.prototype.forEach.call(
row.querySelectorAll(aria.GridSelector.CELL),
(function (cell) {
var focusableSelector = '[tabindex]';
if (aria.Utils.matches(cell, focusableSelector)) {
rowCells.push(cell);
}
else {
var focusableCell = cell.querySelector(focusableSelector);
if (focusableCell) {
rowCells.push(focusableCell);
}
}
}).bind(this)
);
if (rowCells.length) {
this.grid.push(rowCells);
}
}).bind(this)
);
if (this.paginationEnabled) {
this.setupIndices();
}
};
/**
* @desc
* If possible, set focus pointer to the cell with the specified coordinates
*
* @param row
* The index of the cell's row
*
* @param col
* The index of the cell's column
*
* @returns
* Returns whether or not the focus could be set on the cell.
*/
aria.Grid.prototype.setFocusPointer = function (row, col) {
if (!this.isValidCell(row, col)) {
return false;
}
if (this.isHidden(row, col)) {
return false;
}
if (!isNaN(this.focusedRow) && !isNaN(this.focusedCol)) {
this.grid[this.focusedRow][this.focusedCol].setAttribute('tabindex', -1);
}
this.grid[row][col]
.removeEventListener('focus', this.showKeysIndicator);
this.grid[row][col]
.removeEventListener('blur', this.hideKeysIndicator);
// Disable navigation if focused on an input
this.navigationDisabled = aria.Utils.matches(this.grid[row][col], 'input');
this.grid[row][col].setAttribute('tabindex', 0);
this.focusedRow = row;
this.focusedCol = col;
this.grid[row][col]
.addEventListener('focus', this.showKeysIndicator);
this.grid[row][col]
.addEventListener('blur', this.hideKeysIndicator);
return true;
};
/**
* @param row
* The index of the cell's row
*
* @param col
* The index of the cell's column
*
* @returns
* Returns whether or not the coordinates are within the grid's boundaries.
*/
aria.Grid.prototype.isValidCell = function (row, col) {
return (
!isNaN(row) &&
!isNaN(col) &&
row >= 0 &&
col >= 0 &&
this.grid &&
this.grid.length &&
row < this.grid.length &&
col < this.grid[row].length
);
};
/**
* @param row
* The index of the cell's row
*
* @param col
* The index of the cell's column
*
* @returns
* Returns whether or not the cell has been hidden.
*/
aria.Grid.prototype.isHidden = function (row, col) {
var cell = this.gridNode.querySelectorAll(aria.GridSelector.ROW)[row]
.querySelectorAll(aria.GridSelector.CELL)[col];
return aria.Utils.hasClass(cell, aria.CSSClass.HIDDEN);
};
/**
* @desc
* Clean up grid events
*/
aria.Grid.prototype.clearEvents = function () {
this.gridNode.removeEventListener('keydown', this.checkFocusChange);
this.gridNode.removeEventListener('keydown', this.delegateButtonHandler);
this.gridNode.removeEventListener('click', this.focusClickedCell);
this.gridNode.removeEventListener('click', this.delegateButtonHandler);
if (this.paginationEnabled) {
this.gridNode.removeEventListener('keydown', this.checkPageChange);
}
if (this.shouldRestructure) {
window.removeEventListener('resize', this.checkRestructureGrid);
}
this.grid[this.focusedRow][this.focusedCol]
.removeEventListener('focus', this.showKeysIndicator);
this.grid[this.focusedRow][this.focusedCol]
.removeEventListener('blur', this.hideKeysIndicator);
};
/**
* @desc
* Register grid events
*/
aria.Grid.prototype.registerEvents = function () {
this.clearEvents();
this.gridNode.addEventListener('keydown', this.checkFocusChange);
this.gridNode.addEventListener('keydown', this.delegateButtonHandler);
this.gridNode.addEventListener('click', this.focusClickedCell);
this.gridNode.addEventListener('click', this.delegateButtonHandler);
if (this.paginationEnabled) {
this.gridNode.addEventListener('keydown', this.checkPageChange);
}
if (this.shouldRestructure) {
window.addEventListener('resize', this.checkRestructureGrid);
}
};
/**
* @desc
* Focus on the cell in the specified row and column
*
* @param row
* The index of the cell's row
*
* @param col
* The index of the cell's column
*/
aria.Grid.prototype.focusCell = function (row, col) {
if (this.setFocusPointer(row, col)) {
this.grid[row][col].focus();
}
};
aria.Grid.prototype.showKeysIndicator = function () {
if (this.keysIndicator) {
aria.Utils.removeClass(this.keysIndicator, 'hidden');
}
};
aria.Grid.prototype.hideKeysIndicator = function () {
if (this.keysIndicator &&
this.grid[this.focusedRow][this.focusedCol].tabIndex === 0) {
aria.Utils.addClass(this.keysIndicator, 'hidden');
}
};
/**
* @desc
* Triggered on keydown. Checks if an arrow key was pressed, and (if possible)
* moves focus to the next valid cell in the direction of the arrow key.
*
* @param event
* Keydown event
*/
aria.Grid.prototype.checkFocusChange = function (event) {
if (!event || this.navigationDisabled) {
return;
}
this.findFocusedItem(event.target);
var key = event.which || event.keyCode;
var rowCaret = this.focusedRow;
var colCaret = this.focusedCol;
var nextCell;
switch (key) {
case aria.KeyCode.UP:
nextCell = this.getNextVisibleCell(0, -1);
rowCaret = nextCell.row;
colCaret = nextCell.col;
break;
case aria.KeyCode.DOWN:
nextCell = this.getNextVisibleCell(0, 1);
rowCaret = nextCell.row;
colCaret = nextCell.col;
break;
case aria.KeyCode.LEFT:
nextCell = this.getNextVisibleCell(-1, 0);
rowCaret = nextCell.row;
colCaret = nextCell.col;
break;
case aria.KeyCode.RIGHT:
nextCell = this.getNextVisibleCell(1, 0);
rowCaret = nextCell.row;
colCaret = nextCell.col;
break;
case aria.KeyCode.HOME:
if (event.ctrlKey) {
rowCaret = 0;
}
colCaret = 0;
break;
case aria.KeyCode.END:
if (event.ctrlKey) {
rowCaret = this.grid.length - 1;
}
colCaret = this.grid[this.focusedRow].length - 1;
break;
default:
return;
}
if (this.paginationEnabled) {
if (rowCaret < this.topIndex) {
this.showFromRow(rowCaret, true);
}
if (rowCaret >= this.topIndex + this.perPage) {
this.showFromRow(rowCaret, false);
}
}
this.focusCell(rowCaret, colCaret);
event.preventDefault();
};
/**
* @desc
* Reset focused row and col if it doesn't match focusedRow and focusedCol
*
* @param focusedTarget
* Element that is currently focused by browser
*/
aria.Grid.prototype.findFocusedItem = function (focusedTarget) {
var focusedCell = this.grid[this.focusedRow][this.focusedCol];
if (focusedCell === focusedTarget ||
focusedCell.contains(focusedTarget)) {
return;
}
for (var i = 0; i < this.grid.length; i++) {
for (var j = 0; j < this.grid[i].length; j++) {
if (this.grid[i][j] === focusedTarget ||
this.grid[i][j].contains(focusedTarget)) {
this.setFocusPointer(i, j);
return;
}
}
}
};
/**
* @desc
* Triggered on click. Finds the cell that was clicked on and focuses on it.
*
* @param event
* Keydown event
*/
aria.Grid.prototype.focusClickedCell = function (event) {
var clickedGridCell = this.findClosest(event.target, '[tabindex]');
for (var row = 0; row < this.grid.length; row++) {
for (var col = 0; col < this.grid[row].length; col++) {
if (this.grid[row][col] === clickedGridCell) {
this.setFocusPointer(row, col);
if (!aria.Utils.matches(clickedGridCell, 'button[aria-haspopup]')) {
// Don't focus if it's a menu button (focus should be set to menu)
this.focusCell(row, col);
}
return;
}
}
}
};
/**
* @desc
* Triggered on click. Checks if user clicked on a header with aria-sort.
* If so, it sorts the column based on the aria-sort attribute.
*
* @param event
* Keydown event
*/
aria.Grid.prototype.delegateButtonHandler = function (event) {
var key = event.which || event.keyCode;
var target = event.target;
var isClickEvent = (event.type === 'click');
if (!target) {
return;
}
if (
target.parentNode &&
target.parentNode.matches('th[aria-sort]') &&
(
isClickEvent ||
key === aria.KeyCode.SPACE ||
key === aria.KeyCode.RETURN
)
) {
event.preventDefault();
this.handleSort(target.parentNode);
}
if (
aria.Utils.matches(target, '.editable-text, .edit-text-button') &&
(
isClickEvent ||
key === aria.KeyCode.RETURN
)
) {
event.preventDefault();
this.toggleEditMode(
this.findClosest(target, '.editable-text'),
true,
true
);
}
if (
aria.Utils.matches(target, '.edit-text-input') &&
(
key === aria.KeyCode.RETURN ||
key === aria.KeyCode.ESC
)
) {
event.preventDefault();
this.toggleEditMode(
this.findClosest(target, '.editable-text'),
false,
key === aria.KeyCode.RETURN
);
}
};
/**
* @desc
* Toggles the mode of an editable cell between displaying the edit button
* and displaying the editable input.
*
* @param editCell
* Cell to toggle
*
* @param toggleOn
* Whether to show or hide edit input
*
* @param updateText
* Whether or not to update the button text with the input text
*/
aria.Grid.prototype.toggleEditMode = function (editCell, toggleOn, updateText) {
var onClassName = toggleOn ? 'edit-text-input' : 'edit-text-button';
var offClassName = toggleOn ? 'edit-text-button' : 'edit-text-input';
var onNode = editCell.querySelector('.' + onClassName);
var offNode = editCell.querySelector('.' + offClassName);
if (toggleOn) {
onNode.value = offNode.innerText;
}
else if (updateText) {
onNode.innerText = offNode.value;
}
aria.Utils.addClass(offNode, aria.CSSClass.HIDDEN);
aria.Utils.removeClass(onNode, aria.CSSClass.HIDDEN);
offNode.setAttribute('tabindex', -1);
onNode.setAttribute('tabindex', 0);
onNode.focus();
this.grid[this.focusedRow][this.focusedCol] = onNode;
this.navigationDisabled = toggleOn;
};
/**
* @desc
* Sorts the column below the header node, based on the aria-sort attribute.
* aria-sort="none" => aria-sort="ascending"
* aria-sort="ascending" => aria-sort="descending"
* All other headers with aria-sort are reset to "none"
*
* Note: This implementation assumes that there is no pagination on the grid.
*
* @param headerNode
* Header DOM node
*/
aria.Grid.prototype.handleSort = function (headerNode) {
var columnIndex = headerNode.cellIndex;
var sortType = headerNode.getAttribute('aria-sort');
if (sortType === aria.SortType.ASCENDING) {
sortType = aria.SortType.DESCENDING;
}
else {
sortType = aria.SortType.ASCENDING;
}
var comparator = function (row1, row2) {
var row1Text = row1.children[columnIndex].innerText;
var row2Text = row2.children[columnIndex].innerText;
var row1Value = parseInt(row1Text.replace(/[^0-9\.]+/g, ''));
var row2Value = parseInt(row2Text.replace(/[^0-9\.]+/g, ''));
if (sortType === aria.SortType.ASCENDING) {
return row1Value - row2Value;
}
else {
return row2Value - row1Value;
}
};
this.sortRows(comparator);
this.setupFocusGrid();
Array.prototype.forEach.call(
this.gridNode.querySelectorAll(aria.GridSelector.SORT_HEADER),
function (headerCell) {
headerCell.setAttribute('aria-sort', aria.SortType.NONE);
}
);
headerNode.setAttribute('aria-sort', sortType);
};
/**
* @desc
* Sorts the grid's rows according to the specified compareFn
*
* @param compareFn
* Comparison function to sort the rows
*/
aria.Grid.prototype.sortRows = function (compareFn) {
var rows = this.gridNode.querySelectorAll(aria.GridSelector.ROW);
var rowWrapper = rows[0].parentNode;
var dataRows = Array.prototype.slice.call(rows, 1);
dataRows.sort(compareFn);
dataRows.forEach((function (row) {
rowWrapper.appendChild(row);
}).bind(this));
};
/**
* @desc
* Adds aria-rowindex and aria-colindex to the cells in the grid
*/
aria.Grid.prototype.setupIndices = function () {
var rows = this.gridNode.querySelectorAll(aria.GridSelector.ROW);
for (var row = 0; row < rows.length; row++) {
var cols = rows[row].querySelectorAll(aria.GridSelector.CELL);
rows[row].setAttribute('aria-rowindex', row + 1);
for (var col = 0; col < cols.length; col++) {
cols[col].setAttribute('aria-colindex', col + 1);
}
}
};
/**
* @desc
* Determines the per page attribute of the grid, and shows/hides rows
* accordingly.
*/
aria.Grid.prototype.setupPagination = function () {
this.onPaginationChange = this.onPaginationChange || function () {};
this.perPage = parseInt(this.gridNode.getAttribute('data-per-page'));
this.showFromRow(0, true);
};
aria.Grid.prototype.setPaginationChangeHandler = function (onPaginationChange) {
this.onPaginationChange = onPaginationChange;
};
/**
* @desc
* Check if page up or page down was pressed, and show the next page if so.
*
* @param event
* Keydown event
*/
aria.Grid.prototype.checkPageChange = function (event) {
if (!event) {
return;
}
var key = event.which || event.keyCode;
if (key === aria.KeyCode.PAGE_UP) {
event.preventDefault();
this.movePageUp();
}
else if (key === aria.KeyCode.PAGE_DOWN) {
event.preventDefault();
this.movePageDown();
}
};
aria.Grid.prototype.movePageUp = function () {
var startIndex = Math.max(this.perPage - 1, this.topIndex - 1);
this.showFromRow(startIndex, false);
this.focusCell(startIndex, this.focusedCol);
};
aria.Grid.prototype.movePageDown = function () {
var startIndex = this.topIndex + this.perPage;
this.showFromRow(startIndex, true);
this.focusCell(startIndex, this.focusedCol);
};
/**
* @desc
* Scroll the specified row into view in the specified direction
*
* @param startIndex
* Row index to use as the start index
*
* @param scrollDown
* Whether to scroll the new page above or below the row index
*/
aria.Grid.prototype.showFromRow = function (startIndex, scrollDown) {
var dataRows =
this.gridNode.querySelectorAll(aria.GridSelector.SCROLL_ROW);
var reachedTop = false;
var firstIndex = -1;
var endIndex = -1;
if (startIndex < 0 || startIndex >= dataRows.length) {
return;
}
for (var i = 0; i < dataRows.length; i++) {
if (
(
scrollDown &&
i >= startIndex &&
i < startIndex + this.perPage) ||
(
!scrollDown &&
i <= startIndex &&
i > startIndex - this.perPage
)
) {
aria.Utils.removeClass(dataRows[i], aria.CSSClass.HIDDEN);
if (!reachedTop) {
this.topIndex = i;
reachedTop = true;
}
if (firstIndex < 0) {
firstIndex = i;
}
endIndex = i;
}
else {
aria.Utils.addClass(dataRows[i], aria.CSSClass.HIDDEN);
}
}
this.onPaginationChange(firstIndex, endIndex);
};
/**
* @desc
* Throttle restructuring to only happen every 300ms
*/
aria.Grid.prototype.checkRestructureGrid = function () {
if (this.waitingToRestructure) {
return;
}
this.waitingToRestructure = true;
setTimeout(this.restructureGrid, 300);
};
/**
* @desc
* Restructure grid based on the size.
*/
aria.Grid.prototype.restructureGrid = function () {
this.waitingToRestructure = false;
var gridWidth = this.gridNode.offsetWidth;
var cells = this.gridNode.querySelectorAll(aria.GridSelector.CELL);
var currentWidth = 0;
var focusedElement = this.gridNode.querySelector(aria.GridSelector.TABBABLE);
var shouldRefocus = (document.activeElement === focusedElement);
var focusedIndex = (this.focusedRow * this.grid[0].length + this.focusedCol);
var newRow = document.createElement('div');
newRow.setAttribute('role', 'row');
this.gridNode.innerHTML = '';
this.gridNode.append(newRow);
cells.forEach(function (cell, index) {
var cellWidth = cell.offsetWidth;
if (currentWidth > 0 && currentWidth >= (gridWidth - cellWidth)) {
newRow = document.createElement('div');
newRow.setAttribute('role', 'row');
this.gridNode.append(newRow);
currentWidth = 0;
}
newRow.append(cell);
currentWidth += cellWidth;
});
this.setupFocusGrid();
this.focusedRow = Math.floor(focusedIndex / this.grid[0].length);
this.focusedCol = focusedIndex % this.grid[0].length;
if (shouldRefocus) {
this.focusCell(this.focusedRow, this.focusedCol);
}
};
/**
* @desc
* Get next cell to the right or left (direction) of the focused
* cell.
*
* @param currRow
* Row index to start searching from
*
* @param currCol
* Column index to start searching from
*
* @param directionX
* X direction for where to check for cells. +1 to check to the right, -1 to
* check to the left
*
* @return
* Indices of the next cell in the specified direction. Returns the focused
* cell if none are found.
*/
aria.Grid.prototype.getNextCell = function (
currRow,
currCol,
directionX,
directionY
) {
var row = currRow + directionY;
var col = currCol + directionX;
var rowCount = this.grid.length;
var isLeftRight = directionX !== 0;
if (!rowCount) {
return false;
}
var colCount = this.grid[0].length;
if (this.shouldWrapCols && isLeftRight) {
if (col < 0) {
col = colCount - 1;
row--;
}
if (col >= colCount) {
col = 0;
row++;
}
}
if (this.shouldWrapRows && !isLeftRight) {
if (row < 0) {
col--;
row = rowCount - 1;
if (this.grid[row] && col >= 0 && !this.grid[row][col]) {
// Sometimes the bottom row is not completely filled in. In this case,
// jump to the next filled in cell.
row--;
}
}
else if (row >= rowCount || !this.grid[row][col]) {
row = 0;
col++;
}
}
if (this.isValidCell(row, col)) {
return {
row: row,
col: col
};
}
else if (this.isValidCell(currRow, currCol)) {
return {
row: currRow,
col: currCol
};
}
else {
return false;
}
};
/**
* @desc
* Get next visible column to the right or left (direction) of the focused
* cell.
*
* @param direction
* Direction for where to check for cells. +1 to check to the right, -1 to
* check to the left
*
* @return
* Indices of the next visible cell in the specified direction. If no visible
* cells are found, returns false if the current cell is hidden and returns
* the current cell if it is not hidden.
*/
aria.Grid.prototype.getNextVisibleCell = function (directionX, directionY) {
var nextCell = this.getNextCell(
this.focusedRow,
this.focusedCol,
directionX,
directionY
);
if (!nextCell) {
return false;
}
var rowCount = this.grid.length;
var colCount = this.grid[nextCell.row].length;
while (this.isHidden(nextCell.row, nextCell.col)) {
var currRow = nextCell.row;
var currCol = nextCell.col;
nextCell = this.getNextCell(currRow, currCol, directionX, directionY);
if (currRow === nextCell.row && currCol === nextCell.col) {
// There are no more cells to try if getNextCell returns the current cell
return false;
}
}
return nextCell;
};
/**
* @desc
* Show or hide the cells in the specified column
*
* @param columnIndex
* Index of the column to toggle
*
* @param isShown
* Whether or not to show the column
*/
aria.Grid.prototype.toggleColumn = function (columnIndex, isShown) {
var cellSelector = '[aria-colindex="' + columnIndex + '"]';
var columnCells = this.gridNode.querySelectorAll(cellSelector);
Array.prototype.forEach.call(
columnCells,
function (cell) {
if (isShown) {
aria.Utils.removeClass(cell, aria.CSSClass.HIDDEN);
}
else {
aria.Utils.addClass(cell, aria.CSSClass.HIDDEN);
}
}
);
if (!isShown && this.focusedCol === (columnIndex - 1)) {
// If focus was set on the hidden column, shift focus to the right
var nextCell = this.getNextVisibleCell(1, 0);
if (nextCell) {
this.setFocusPointer(nextCell.row, nextCell.col);
}
}
};
/**
* @desc
* Find the closest element matching the selector. Only checks parent and
* direct children.
*
* @param element
* Element to start searching from
*
* @param selector
* Index of the column to toggle
*/
aria.Grid.prototype.findClosest = function (element, selector) {
if (aria.Utils.matches(element, selector)) {
return element;
}
if (aria.Utils.matches(element.parentNode, selector)) {
return element.parentNode;
}
return element.querySelector(selector);
};
Skip links are a common accessibility feature on websites. They are shortcuts to important parts of the webpage that makes it easier and quicker for some users – especially users with disabilities – to find their way around. They care commonly placed before the main navigation menu on the page, but can be used anywhere there is a chunk of content.
Skip links are usually hidden visually by default and appear when users navigate to them using the tab key on their keyboard.
There are multiple approaches to creating skip links, but the main idea is to hide the link from view until it receives keyboard focus. When the link is pressed, focus is moved to the section that is skipped to.
Content, content, content...lorem ipsum, etc.
<a class="hidden" id="skipLink" href="#demomain">Skip to content</a>
<div id="content">
<nav>
<ul>
<li><a href="#">Main</a></li>
<li><a href="#">Things</a></li>
<li><a href="#">Stuff</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
<div id="demomain">
<h1 tabindex="0" id="demoHead">Here's a heading and some content! It's important to focus on me.</h1>
<p>Content, content, content...lorem ipsum, etc.</p>
<button id="demoBtn">Push me</button>
</div>
</div>
*:focus {
border: 2px solid blue;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 500px;
}
a {
color: #333;
text-decoration: none;
padding: 0.5rem;
}
button {
font-size: 1.25rem;
border-radius: 0.33em;
font-family: inherit;
background: #111;
color: #fefefe;
padding: 0.75rem;
border: 0;
text-align: center;
margin-top: 20vh;
margin-left: 25%;
width: 50%;
}
ul {
list-style: none;
margin-top: 0px;
padding-left: 0px;
background-color: lightgreen;
}
ul > li {
font-size: 1.25rem;
font-family: inherit;
color: #000;
padding: 0.75rem;
display: inline-block;
width: 24%;
text-align: center;
margin: 0px;
}
ul > li a {
width: 100%;
}
ul > li:hover {
background-color: aliceblue;
}
ul > li a:focus {
background-color: aliceblue;
}
.hidden {
padding: 0.75rem;
padding-bottom: 1.25rem;
position: absolute;
background: #000;
color: #fff;
left: 50%;
height: 50px;
transform: translateY(-100%);
transition: transform 0.3s;
opacity: 0;
}
.hidden:focus {
transform: translateY(0%);
opacity: 1;
border: 2px solid aliceblue;
}
#content {
height: 400px;
background-color: #009FD4;
}
p {
color: white;
padding-left: 20px;
}
document.getElementById('skipLink').addEventListener('click', function(e) {
e.preventDefault();
var target = demo.getElementById('demoHead');
target.focus();
});
The objective of this technique is to associate each data cell (in a data table) with the appropriate headers. This technique adds a headers attribute to each data cell (td element). It also adds an id attribute to any cell used as a header for other cells. The headers attribute of a cell contains a list of the id attributes of the associated header cells. If there is more than one id, they are separated by spaces.
This technique is used when data cells are associated with more than one row and/or one column header. This allows screen readers to speak the headers associated with each data cell when the relationships are too complex to be identified using the th element alone or the th element with the scope attribute. Using this technique also makes these complex relationships perceivable when the presentation format changes.
This technique is not recommended for layout tables since its use implies a relationship between cells that is not meaningful when tables are used for layout.
<table>
<tr>
<th rowspan="2" id="h">Homework</th>
<th colspan="3" id="e">Exams</th>
<th colspan="3" id="p">Projects</th>
</tr>
<tr>
<th id="e1" headers="e">1</th>
<th id="e2" headers="e">2</th>
<th id="ef" headers="e">Final</th>
<th id="p1" headers="p">1</th>
<th id="p2" headers="p">2</th>
<th id="pf" headers="p">Final</th>
</tr>
<tr>
<td headers="h">15%</td>
<td headers="e e1">15%</td>
<td headers="e e2">15%</td>
<td headers="e ef">20%</td>
<td headers="p p1">10%</td>
<td headers="p p2">10%</td>
<td headers="p pf">15%</td>
</tr>
</table>
Homework | Exams | Projects | ||||
---|---|---|---|---|---|---|
1 | 2 | Final | 1 | 2 | Final | |
15% | 15% | 15% | 20% | 10% | 10% | 15% |
Poet Training Tool - Exhaustive training tool that details best practices for creating alternate text for images.
Writing Alt Text for Data Visualization - Writing Alt Text for Data Visualization
Writing alt text for complex images - WAI tutorial for writing alt text for complex images
By using the IMG element instead of the SVG element to insert SVGs, you can alternate text using the IMG element’s ALT attribute.
The SVG desc element allows
Axess Lab - What are skip links? - Exhaustive training tool that details best practices for creating alternate text for images.
Dynamic search bar - Don’t want it to update to frequently. Should be ARIA-LIVE=“polite”
Toasts - High risk of going wrong for many users – not just SR. Bad for zoom text users also.
Feed and chat messages - role=“log” plus aria-live=“assertive/polite” aria-atomic=“false” – create seperate live region that updates with new messages. Wrapping whole region makes SR read out whole region instead of new messages.
Plugins, apps, and other tools to test
ARIA is a way for websites and web apps to communicate with screen readers. You can think of ARIA as an API for communicating information about an interactive HTML element / widget to a screen reader.
Here is how the accessible name of an element is calculated:
<button>This is a button</button>
Authoring tools are software and services that “authors” (web developers, designers, writers, etc.) use to produce web content (static web pages, dynamic web applications, etc.).