Accessible Components
Accessible Components
By Chris Ferdinandi
Go Make Things, LLC
v1.0.0
Copyright 2022 Chris Ferdinandi and Go Make Things, LLC. All Rights Reserved.
Table of Contents
1. Intro
A quick word about browser compatibility
Using the code in this guide
Technical editing
2. Accessibility and screen readers
Navigating with a keyboard
A quick note about macOS
Screen Readers
Enabling a screen reader
Using a screen reader
CSS-only components
3. HTML Semantics
Role, name, and state
If an element should be interactive, use something focusable
Buttons and links do different things
Focus Management
ARIA
4. Hiding UI content
The [hidden] attribute, and the display and visibility properties
The [aria-hidden] attribute
The [aria-label] attribute
5. Announcing dynamic content changes
The [aria-live] attribute
The [aria-atomic] attribute
ARIA live region roles
6. Button state
Wrong ways to show active state
The accessible way to show active state
7. Interactive components
A quick aside about the [aria-controls] attribute
8. Disclosure Component
Accessibility Requirements
The Template
Setting up the DOM
Toggling content visibility
9. HTML-Only Disclosure Component
Styling details and summary
Detecting when the content expands or collapses
10. Accordion
Accessibility Requirements
The Template
Setting up the DOM
Toggling content visibility
Styling the buttons
11. Tabs
Accessibility Requirements
The Template
Setting up the DOM
Toggling content visibility when a tab is clicked
Showing the active tab
Styling the buttons
Navigating tabs with arrow keys
12. Notification
Accessibility Requirements
The Template
Creating a utility function
Styling the notifications
Announcing the notification
Automatically removing notifications after a few moments
Dismissing a notification
13. About the Author
Intro
In this guide, you’ll learn:
Technical editing
A technical review of this book was done by the amazing Léonie Watson.
Léonie is a director at TetraLogical, member of the W3C Advisory Board, co-Chair
of the W3C Web Applications and HTML working groups, and member of the BIMA
Inclusive Design Council.
I’m incredibly grateful for her help and guidance in ensuring that the advice and
recommendations in this guide are good!
Accessibility and screen readers
Accessibility is sometimes abbreviated with the numeronym A11Y, with 11
representing the number of letters between the A and Y.
For web developers, the field of accessibility is focused on providing people with
equal access to the things we make, regardless of any physical or neurological
disabilities.
That includes both temporary disabilities (such as losing mobility after breaking an
arm) and long-term or permanent disabilities. It includes physical disabilities, such
as visual impairment or a neuromuscular condition, as well as cognitive
impairments.
As you can imagine, the field of accessibility covers a wide-range of topics, from
HTML structure to color choices to copywriting and more.
Using the tab key, you can jump from one focusable element (such as links and
form fields) to another. Using the enter or return keys, you can activate a link or
submit a form. Using the space bar, you press a button or scroll down on a page.
When building things for the web, it’s important to ensure that the things can be
used with just a keyboard.
Screen Readers
Users who are visually impaired may use a piece of software called a screen reader
to navigate the web.
Screen readers announce the text on a page out loud so that someone who is
visually impaired can use it. They also typically communicate additional
information about the page, such as the heading structure and other landmarks.
Both macOS and Windows 10 and up include free screen reader software. Users on
Linux or older versions of Windows have free third-party options to choose from.
On macOS
On Windows 10
Click Start, then Settings. Click Ease of Access. Toggle the Narrator
button to On. You can alternatively use the Win + Control + Enter
keyboard shortcut.
On Linux
If you’ve never used one before, using one to navigate through your website can be
very enlightening. Open up a site you’ve built, turn on a screen reader, and using
either the Control + Alt + Right Arrow keys on macOS or Down Arrow key
on Windows to navigate through the site’s content.
To see a screen reader in action, you can also watch this video from TetraLogical.
Note: using a screen reader for the first time (and second, and third, and…) is an
awkward, clumsy experience. You don’t have to be an expert. The point right now is to
get a taste for what your website is like for people who can’t see the layout or changes to
it.
CSS-only components
People sometimes build interactive components using only CSS. These are almost
always inaccessible for people who use screen readers and other assistive
technology.
As we’ll see in the coming sections, screen readers expect and require certain
attributes and properties on HTML elements to understand what’s happening in
the UI and communicate those changes to the people who use them. For interactive
components, the values of those properties often change dynamically.
CSS is incredibly powerful, and I love to replace JavaScript with CSS when I can.
But for interactive components, CSS cannot update the attribute values that screen
readers rely on to understand what’s happening in the UI. Additionally, many CSS-
only components assume the use of a mouse, and don’t work (or don’t work as
expected) with keyboard navigation.
Screen readers and other assistive tech not only work just fine with JavaScript, but
often require JS to work properly with interactive components.
HTML Semantics
The HTML elements that you use often convey information to people who use
screen readers, and provide critical functionality to people who navigate the web
with a keyboard.
Role. Communicate the intent or purpose of the element. For example, a link
element (a) has a role of link. An h2 element has a role of heading level 2.
Name. The text that’s read aloud for that element or piece of content. For a
link or heading, for example, that would typically be the text inside the
element.
State. The current state of the element. Some elements, like headings, are
generally stateless. However, a link might have a state of visited. A button
might have a state of pressed.
.btn {
background-color: #e5e5e5;
border: 1px solid #808080;
border-radius: 0.25em;
color: #272727;
font: inherit;
margin-right: 0.5em;
padding: 0.5em 0.85em;
}
.btn:hover {
background-color: #0088cc;
border-color: #0088cc;
color: #ffffff;
}
<!--
This is bad
A span element cannot be focused or accessed with a
keyboard,
and it will not be identified as a link or button by
screen readers
-->
<span class="btn">Activate me, too</span>
A link implies that clicking it will take you to a different location—either another
page, or a different spot on the current one. Buttons imply interactivity—showing
and hiding content, submitting a form, and so on.
Sometimes, you might have a valid link that you progressively enhance into a
button after your JavaScript loads. In that case, you can add [role="button"] to
the link element with your JavaScript.
This is a common pattern with progressively enhanced links and async HTML
loading.
By default, you might have a link that points to another webpage. When your
JavaScript loads, you may instead have it fetch HTML and load it into the current
page instead.
In that case, you would add [role="button"] to the link in the JavaScript file.
You would also need to write some JavaScript to trigger a click event when the
Space bar is prssed with the element is in focus.
<!-- Default -->
<a id="js-async" href="/all-about-merlin">Learn more about
Merlin</a>
// Dispatch event
document.activeElement.dispatchEvent(click);
});
But as a general rule, if it takes you to another page, use a link. If its just for on-
page interactivity, use a button.
Focus Management
Focus order is the order that the different focusable elements on a page will come
into focus if you attempt to navigate to them by pressing the Tab key.
By default, focus order matches the order in which elements appear on a page.
You can change the order that elements come into focus with thetabindex
property.
A value of 0 for tabindex is the default innate value of focusable elements. You
can use other values to change the tab order of elements on the page.
That said, there are a few specific occasions where modifying focus behavioris
required.
For certain interactive components, normal focus order should be removed and
replaced with other keyboard interactions. You might also have an element that’s
not focusable by default, but that you need to bring into focus with JavaScript.
This removes the element from the focus order entirely (if it was focusable
already), and allows you to focus it with JavaScript (if it wasn’t already focusable).
<button tabindex="-1">This will be skipped in the focus
order</button>
<div id="app" tabindex="-1">This isn't a focusable element,
but can now be focused with JavaScript.</div>
ARIA
ARIA (Accessible Rich Internet Applications) is a set of attributes that provide
additional details to screen readers when HTML alone doesn’t convey enough
information.
The ARIA spec includes the [role] attribute, which can be used to convey or
modify the role of an element, and includes some values beyond native HTML.
ARIA also includes special attributes, prefixed with [aria-*].
Generally speaking, you should only use ARIA when the semantics of the
HTML element alone are not enough to convey all of the information about
the UI.
For example, you do not need to to include the [role="button"] attribute with a
button element. The semantics of the element already convey that it’s abutton
to screen readers.
<div hidden>
Nothing inside this element will be displayed in the
UI.
It also won't be read aloud by screen readers.
</div>
Similarly, the display: none CSS property hides an element in the UI, and
prevents screen readers for reading it.
.top-secret {
display: none;
}
<div class="top-secret">
Nothing inside this element will be displayed in the UI
or read aloud, either.
</div>
The visibility: hidden property hides an element visually, but keeps the
element in the layout. Screen readers will read content inside an element hidden
this way.
.invisible {
visibility: hidden;
}
<div class="invisible">
Nothing inside this element will be displayed in the
UI, but it still affects the layout and gets read by screen
readers.
</div>
Imagine that you have a button. To make it a bit more visually interesting, you add
some emoji to it.
While a sighted user might be able to recognize that the emoji are purely
decorative, a visitor using a screen reader is presented with nonsense.
For example, imagine you have a button that contains an SVG icon of a cloud with
an arrow pointing down out of it, but no text. In this app, it’s supposed to mean
“download.”
<button>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16
16">
<path ...>
</svg>
</button>
If you’re not visually impaired, you might be able to figure that out. If you rely on a
screen reader, though, this button will announce, “button.”
<button aria-label="Download">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16
16">
<path ...>
</svg>
</button>
The [aria-label] text is read instead of any content inside the element,
including otherwise accessible text.
In addition to icons, it can be useful when the context of a link or button might be
apparent to sighted users, but confusing for visually impaired users.
For example, a sighted user might be able to infer that a “read more” link is for the
article before it, while a screen reader user tabbing through links might not.
The link below would be announced as “Read more about pirates,” instead of “Read
more…”.
Generally speaking, screen readers will not notice or announce those changes
unless you tell them that they need to.
It’s value can be set to off (the same as not using it at all),assertive (in which
screen readers interrupt user actions to announce changes), and polite (which
tells the screen reader to wait until the user is done to announce updates).
In most cases, you should use polite. Choose assertive when the
announcement should interrupt whatever the user is doing (use with caution).
Note: in order to announce changes, the ARIA live region has to be in the UI before its
content is changed.
The [aria-atomic] attribute
The [aria-atomic] property tells screen readers whether or not to announce the
entire ARIA live region even when only a portion of it has changed.
It will announce all changes when given a value oftrue, and only the changed
content when given a value of false (the same as omitting it altogether).
In this example, only the paragraph text is announced, since that’s the only thing
that changed.
let p = document.querySelector('p');
setTimeout(function () {
p.textContent = 'Good evening!';
}, 5000);
let p = document.querySelector('p');
setTimeout(function () {
p.textContent = 'Good evening!';
}, 5000);
ARIA live region roles
In addition to the [aria-live] attribute, there are specific role values that act
as live regions, and will cause screen readers to announce when content in them is
updated.
You might use it, for example, to announce when some API content has been
loaded into the UI after a user interaction.
<div role="status"></div>
You might use it to, for example, let a user know that their data didn’t save.
<div role="alert"></div>
In apps, it’s common to have buttons that have an on/off or pressed/not pressed
state.
Maybe you have a few simple styles associated with the.fave class, like this.
.fave {
background: transparent;
border: 0;
font-size: 2em;
}
When someone clicks the button, you want to show that it’s “active.”
.fave.is-active {
color: red;
}
While this changes the visual appearance of the button, it does not convey any
information about the new state to screen reader users.
You might think you can solve this by changing the [aria-label] to say
Favorited or something similar.
This attribute lets screen readers know that a button has “state.” When it has a
value of false, the button is not pressed. When it has a value oftrue, it is.
You can change the attribute value using the setAttribute() method.
Don’t remove the attribute if the button is inactive. Toggle it fromtrue to false
// The button is active
btn.setAttribute('aria-pressed', true);
.fave[aria-pressed="true"] {
color: red;
}
With this approach, you should not change the[aria-label] if you’ve used one.
For the rest of this guide, we’re going to look at some common patterns, and create
simple, accessible components that you can use as a starting point for your own
projects.
Disclosure (show/hide)
Accordion
Tabs
Alert
The starter templates and complete project code are included in the source code on
GitHub.
In theory, it’s supposed to create a link between an interactive element and the
content that it controls. In reality, JAWS was the only screen reader to ever
implement it, and the associated announcements it makes are clunky.
Accessibility Requirements
We’ll refer to the thing that causes the content to show or hide as thetrigger, and
the content it reveals as the content.
The Template
For this component, we’ll start off with a button element already in the UI, but
hidden with the [hidden] attribute.
The content is visible by default, and the button to toggle it is hidden. We’re going
to progressively enhance our content into a disclosure after our JavaScript loads.
<div id="content">
<p>Now you see me, now you don't!</p>
</div>
/**
* Show buttons and hide content on page load
*/
function setupDOM () {
// Do stuff...
}
Inside the loop, we’ll use the Element.getAttribute() method to get the ID
selector value from the [data-disclosure] attribute, and pass it into the
document.querySelector() method to get the matching content.
If none is found, we’ll use thecontinue operator to skip to the next disclosure.
// Add ARIA
disclosure.setAttribute('aria-expanded', false);
Finally, we’ll run the setupDOM() method to initialize our script when the page
loads.
One huge benefit of using an actual button element for our trigger is that it will
fire a click event whenever the Enter or Space key are pressed while it has
focus, automatically and by default. It’s part of the semantics of the element.
This means that we can use a single click event listener with the
Element.addEventListener() method instead of multiple listeners.
Since we may have more than one disclosure on the page, we’ll use event
delegation and listen for all clicks on the document. We’ll pass in a
clickHandler() function as our callback.
If the attribute doesn’t exist, we’ll use thereturn operator to end our callback
function immediately.
/**
* Show or hide content on click events
* @param {Event} event The event object
*/
function clickHandler (event) {
Otherwise, we want to show it. We’ll set [aria-expanded] to true, and remove
the [hidden] attribute from the content.
/**
* Show or hide content on click events
* @param {Event} event The event object
*/
function clickHandler (event) {
Now, whenever someone clicks a disclosure button (or presses the Enter or Space
keys while it has focus), our content will show or hide.
HTML-Only Disclosure Component
You can use the details and summary elements to create HTML-only disclosure
components—no JavaScript required!
Put your entire disclosure content inside the details element. The heading that
should act as a toggle goes inside the nested summary element.
When expanded, the details element has the open attribute. You can manually
add it to the element to make your disclosure expanded by default.
<details>
<summary>The toggle</summary>
The content.
</details>
<details open>
<summary>Another toggle</summary>
Expanded by default.
</details>
details[open] {
background-color: #f7f7f7;
padding: 0.5em;
}
summary {
margin-bottom: 0.5em;
}
You can also customize the icon that’s show when the component is expanded or
collapsed using the ::marker pseudo-selector (and ::-webkit-details-
marker for webkit/blink browsers).
In this example, I’m showing a plus symbol (+ ) when the content is collapse, and a
minus symbol (-) when it’s expanded.
/**
* 1. Styling for Firefox and other non-webkit/blink
browsers
* 2. Styling for webkit and blink
*/
summary::marker, /* 1 */
summary::-webkit-details-marker { /* 2 */
display: none;
content: "+ ";
}
By default, this event does not bubble, and has to be attached directly to the
details component. You can use event delegation by setting the useCapture
property to true.
}, true);
Accordion
An accordion component contains a set of headings (and sometimes text snippets)
that can be clicked to reveal additional content. Unlike a disclosure, they typically
contain groups of hidden content, with the header acting as the trigger.
Accessibility Requirements
We’ll refer to the thing that causes the content to show or hide as thetrigger, and
the content it reveals as the content.
The Template
For this component, we’ll start off with a collection of headings and content.
Each piece of content has an ID. Each heading has a[data-accordion] attribute,
with a value equal to the ID selector for the content it should toggle the visibility
of.
The content is visible by default. We’re going to progressively enhance our HTML
into an accordion and hide the content after our JavaScript loads.
/**
* Add buttons and hide content on page load
*/
function setupDOM () {
// Do stuff...
}
/**
* Show buttons and hide content on page load
*/
function setupDOM () {
If none is found, we’ll use thecontinue operator to skip to the next heading.
}
Next, we’ll again use the Element.innerHTML property to wipe out all of the
HTML inside the heading. Then, we’ll use the Element.append() method to
inject the btn into the heading.
// ...
Then, we’ll add an [aria-expanded] property with a value of false to the btn
element.
// Wrap content in a button
for (let heading of headings) {
// ...
// Add ARIA
btn.setAttribute('aria-expanded', false);
Finally, we’ll run the setupDOM() method to initialize our script when the page
loads.
Just like with the disclosure component, one huge benefit of using an actual
button element for our trigger is that it will fire aclick event whenever the
Enter or Space key are pressed while it has focus, automatically and by default.
It’s part of the semantics of the element.
This means that we can use a single click event listener with the
Element.addEventListener() method instead of multiple listeners.
Since we’ll have multiple headings in an accordion, we’ll again use event
delegation and listen for all clicks on the document. We’ll pass in a
clickHandler() function as our callback.
If no matching parent element is found, we’ll use thereturn operator to end our
callback function immediately.
/**
* Show or hide content on click events
* @param {Event} event The event object
*/
function clickHandler (event) {
Otherwise, we want to show it. We’ll set [aria-expanded] to true, and remove
the [hidden] attribute from the content.
/**
* Show or hide content on click events
* @param {Event} event The event object
*/
function clickHandler (event) {
Now, whenever someone clicks an accordion button (or presses the Enter or
Space keys while it has focus), our content will show or hide.
We can use CSS to target button elements inside headings with the [data-
accordion] attribute. We’ll remove any border and background, make the
font match the parent heading, and remove any margin and padding. We’ll also
align the text to the left, and have the button fill the full width of heading.
/**
* Style the accordion buttons to look like headers
*/
[data-accordion] > button {
background: transparent;
border: none;
display: block;
font: inherit;
margin: 0;
padding: 0;
text-align: left;
width: 100%;
}
We can also use CSS to display different icons when the accordion is expanded or
collapsed by targeting the [aria-expanded] attribute on the button.
We’ll use the ::after pseudo-selector with the content property to display a
dash (–) or plus sign (+).
/**
* Show expand/collapse icons
*/
[data-accordion] > button[aria-expanded="true"]::after {
content: " –";
}
Accessibility Requirements
We’ll refer to the thing that causes the content to show or hide as thetrigger, and
the content it reveals as the content.
As you can see, the tab component has a lot more requirements and considerations
than some of the other components we’ve looked at.
The Template
For this component, we’ll start off with a list of anchor links, and a set of anchored
content.
Each piece of content has an ID. The list of anchor links has a[data-tabs]
attribute, indicating that it should be progressively enhanced into tabs after the
JavaScript loads.
<ul data-tabs>
<li><a href="#wizard">Wizard</a></li>
<li><a href="#sorcerer">Sorcerer</a></li>
<li><a href="#druid">Druid</a></li>
</ul>
<div id="wizard">
<!-- ... -->
</div>
<div id="sorcerer">
<!-- ... -->
</div>
<div id="druid">
<!-- ... -->
</div>
/**
* Add ARIA attributes and hide content on page load
*/
function setupDOM () {
// Do stuff...
}
If no tabList is found, we’ll use the return operator to end the function early.
/**
* Add ARIA attributes and hide content on page load
*/
function setupDOM () {
Then, we’ll use a for...of loop to loop through each of the tabs, and add a
[role] of presentation.
/**
* Add ARIA attributes and hide content on page load
*/
function setupDOM () {
// ...
Next, we’ll use the NodeList.forEach() method to loop through each of the
links.
// ...
We’ll assign the returned content element to the tabPane variable. If no tabPane
is found, we’ll use the return operator to skip to the next element in the list.
});
Next, we’ll add a [role] attribute to the link, with a value of tab.
We’ll also add the [aria-selected] attribute. If the index is 0, the link is the
first tab, and we’ll use a value of true. Otherwise, we’ll use a value of false.
});
We want to prevent inactive tab triggers from being focused on with the Tab key.
If the index is greater than 0, we’ll also add a [tabindex] of -1. This will allow
us to shift focus to those elements in response to other keyboard interactions, but
remove them from the normal focus order.
// Add ARIA to the links and content
links.forEach(function (link, index) {
// ...
});
Next, we’ll add a few attributes to thetabPane content. We’ll add a [role] of
tabpanel, and an [aria-labelledby] attribute with the link.id as its value.
Since the link might not have an ID, we’ll check if it does, and if not, we’ll create
one by prefixing the tabPane.id with tab_.
// Add ARIA to the links and content
links.forEach(function (link, index) {
// ...
});
Finally, if the index is greater than 0, the tabPane is not the active one. We’ll add
a [hidden] attribute to hide it.
// Add ARIA to the links and content
links.forEach(function (link, index) {
// ...
});
Now we can run the setupDOM() method to initialize our script when the page
loads.
Let’s start by adding a click event listener. Since there are multiple tabs, we’ll use
event delegation to listen for all clicks on the document, and pass in the
clickHandler() function as our callback.
If not, we’ll use the return operator to end the function. Otherwise, we’ll run the
event.preventDefault() method to prevent the anchor link for updating the
URL or causing the page to jump.
/**
* Show content on click events
* @param {Event} event The event object
*/
function clickHandler (event) {
If so, it’s the currently active tab, so we don’t need to do anything. We can use the
return operator to end the function.
/**
* Show content on click events
* @param {Event} event The event object
*/
function clickHandler (event) {
Since we’ll also need to active a tab when it receives focus, let’s create a
toggleTab() function to actually show the active tab.
If there isn’t one, we’ll use the return operator to end the function.
/**
* Toggle tab visibility
* @param {Node} tab The tab to show
*/
function toggleTab (tab) {
We also want to deactivate the currently active tab, and hide it’s content.
We’ll use the Element.closest() method to find the nearest parent element of
the tab that has the [role="tablist"] attribute. Then, we’ll use the
Element.querySelector() method to find the element in it that currently has
the [aria-selected="true"] attribute.
We’ll assign the active tab to the currentTab variable. Then, we’ll use the
currentTab.hash property to get it’s content element and assign it to the
currentPane variable.
/**
* Toggle tab visibility
* @param {Node} tab The tab to show
*/
function toggleTab (tab) {
Now that we have all of our elements, we’re ready to add, remove, and update
attributes.
We’ll set [aria-selected] to true on the clicked tab, and false on the
currentTab.
We’ll remove the [hidden] attribute from the tabPane, and add it to the
currentPane.
We’ll remove the [tabindex] attribute from the tab so that it can be focused
as part of the normal focus order.
We’ll add the [tabindex] attribute to the currentTab with a value of -1 so
that it’s removed from the normal focus order.
/**
* Toggle tab visibility
* @param {Node} tab The tab to show
*/
function toggleTab (tab) {
// ...
We can hook into ARIA roles so that our styling only applies after the JavaScript
runs.
We’ll give the [role="tab"] links a border, margin, and padding, adjust the
color, and remove the link. We’ll add a light gray background-color on
:hover or when :active.
[role="tablist"] {
list-style: none;
margin: 0 0 2em;
padding: 0;
}
[role="tablist"] li {
display: inline-block;
}
[role="tab"] {
border: 1px solid #808080;
color: #272727;
margin-right: 0.25em;
padding: 0.5em 1em;
text-decoration: none;
}
[role="tab"]:active,
[role="tab"]:hover {
background-color: #e5e5e5;
}
Finally, if the [role="tab"] link has the [aria-selected="true"] attribute
and is the currently active tab, we want to style it differently.
[role="tab"][aria-selected="true"] {
background-color: #0088cc;
border-color: #0088cc;
color: #ffffff;
}
Let’s add a keydown event listener, and pass in a keyHandler() function as our
callback.
We’ll create an array with ArrowLeft and ArrowRight as values, and use the
Array.includes() method to check if the event.code includes one of those
two values.
If not, we’ll use the return operator to end the callback function.
/**
* Update tab content on keyboard events
* @param {Event} event The event object
*/
function keyHandler (event) {
We only want to navigate between tabs with the arrow keys if a tab trigger
currently has focus.
If a tab element isn’t the item that has focus, we’ll use thereturn operator to end
the callback function.
/**
* Update tab content on keyboard events
* @param {Event} event The event object
*/
function keyHandler (event) {
We’ll get the currentTab element’s parent li, and assign that to the listItem
variable.
/**
* Update tab content on keyboard events
* @param {Event} event The event object
*/
function keyHandler (event) {
// ...
Otherwise, we’ll use the Element.querySelector() method to get the tab link
(a) inside the nextListItem, and assign it to the nextTab variable.
/**
* Update tab content on keyboard events
* @param {Event} event The event object
*/
function keyHandler (event) {
// ...
Finally, we’ll pass the nextTab into our toggleTab() function to activate it.
Then, we’ll use the Element.focus() method to shift focus to the nextTab link.
/**
* Update tab content on keyboard events
* @param {Event} event The event object
*/
function keyHandler (event) {
// ...
Now, we have a tab component progressively enhanced from a list of anchor links.
Notification
A notification component is an element that displays important information in the
UI without interrupting the user’s current task. It may remain on screen
permanently, disappear on its own after a period of time, or be manually closed by
the user.
Accessibility Requirements
A notification component is referred to by a variety of names: notification, alert,
status, toast.
Only one of those, alert, has an ARIA spec, and the only requirement is that it
have a [role] attribute of alert.
The Template
For this component, we have an empty div with an ID of #notification where
we’ll be displaying our notification messages.
<div id="notification"></div>
For this component, let’s create a utility function that we can run to add a
notification to the UI when needed. We’ll accept the selector for the element to
show our notification in, and a message to display in the UI.
function notify (selector, message) {
// ...
}
In the notify() function, we’ll first pass the selector into the
document.querySelector() method, and assign the matching element to the
target variable.
If no target is found, we’ll use the return operator to end the function.
We’ll use the Element.append() method to inject the notification into the
target element.
Then, we’ll use the Element.innerHTML property to add the message as its
content.
// ...
// Add message
notification.innerHTML += message;
Now, we can inject a message into the UI by running thenotify() method with
our selector and message.
.notification {
background-color: #f7f7f7;
border: 1px solid #e5e5e5;
margin: 0 0 0.5em;
padding: 0.5em 1em;
}
ARIA live attributes announce changes to the content in an element. When our
notification is appended to the UI, we add the message text at the same time.
As far as screen readers are concerned, this is not achange to the content, but its
initial state.
(Thanks to Heydon Pickering and his Inclusive Components guide for teaching me this
trick.)
// Add message
setTimeout(function () {
notification.innerHTML += message;
}, 1);
Let’s add another parameter, autohide, to the notify() function. If true, we’ll
automatically remove the notification from the UI after some time.
// ...
// Add message
setTimeout(function () {
notification.innerHTML += message;
}, 1);
Dismissing a notification
Notifications can often also be dismissed by a user by clicking a button inside the
notification itself.
Let’s add one more parameter to our notify() function: dismiss. If true, we’ll
add a way for users to dismiss the notification.
After creating the notification, but before injecting into the UI, let’s check if
the dismiss parameter is true.
We’ll use the HTML hex code for a multiplication sign, (×) as the text. We’ll also
give it a class of .notification-close, and an [aria-label] attribute with
a value of Close.
function notify (selector, message, autohide, dismiss) {
// ...
// ...
Next, we’ll add a click event listener to the notification element, and pass in
a named close() function as the callback method.
// ...
// ...
Let’s remove the background and border, and inherit the color and font
from the parent element. We’ll also float it to the right, and add some margin
on the left and bottom.
.notification-close {
background: transparent;
border: none;
color: inherit;
font: inherit;
float: right;
margin: 0 0 0.5em 0.5em;
}
Now, if we pass a fourth argument into the notify() function with a value of
true, users can dismiss the notification.
Hi, I’m Chris Ferdinandi. I believe there’s a simpler, more resilient way to make
things for the web.
I’m the author of the Vanilla JS Pocket Guide series, creator of the Vanilla JS
Academy training program, and host of the Vanilla JS Podcast. My developer tips
newsletter is read by thousands of developers each weekday.
I love pirates, puppies, and Pixar movies, and live near horse farms in rural
Massachusetts.
On my website at GoMakeThings.com.
By email at [email protected].
On Twitter at @ChrisFerdinandi.