Last week, we looked at an accessible accordion component based on Dave Rupert's Nutrition Cards for Accessible Components. This week, we're going to look at an accessible button component.

The HTML

We're going to have two buttons: a standard button and a toggle button.

The two sections of content can be thought of as self-contained compositions, so we wrap each one in an article element. This is called semantic HTML.

Both buttons have an id attribute which we'll use as a selector in our script. The buttons don't do anything by default—we're adding their behaviour with JavaScript—so we set their type attribute to the value 'button'.

The standard button

<article>
<h2>
Standard button
</h2>
<p>
This is a standard button. It shows an alert when activated.
</p>
<p>
<button id="alert" type="button">
Show alert...
</button>
</p>
</article>

The toggle button

Because the second button is a toggle button, we set its aria-pressed attribute to the initial value 'false'. The WAI-ARIA Authoring Practices explain why:

Toggle button: A two-state button that can be either off (not pressed) or on (pressed). To tell assistive technologies that a button is a toggle button, specify a value for the attribute aria-pressed. For example, a button labelled mute in an audio player could indicate that sound is muted by setting the pressed state true. Important: it is critical the label on a toggle does not change when its state changes. In this example, when the pressed state is true, the label remains "Mute" so a screen reader would say something like "Mute toggle button pressed". Alternatively, if the design were to call for the button label to change from "Mute" to "Unmute," the aria-pressed attribute would not be needed.

The button's child SVG element is a heart icon from Font Awesome. Because this is only decorative, its aria-hidden attribute is set to 'true', which hides it from assistive technologies. This improves the experience by hiding the extraneous content; only the text label 'Favourite' will be announced.

<article>
<h2>
Toggle button
</h2>
<p>
This is a toggle button with an on/off state.
</p>
<p>
<button id="toggle" type="button" aria-pressed="false">
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="12" height="12">
<path fill="currentColor" d="M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z"></path>
</svg>
Favourite
</button>
</p>
</article>

The CSS

All of our styles are for the button elements. They reference a number of custom properties I have defined in a global stylesheet:

:root {
--black: #000;
--near-black: #222;
--white: #fff;
--firebrick: #b22222;
--orange-red: #ff4500;
--blue: #00f;
--deep-sky-blue: #00bfff;
--purple: #800080;
--orchid: #da70d6;
}

Default button styles (light mode)

First, the default button styles. In light mode, they have black text and a black border. On hover, they get white text and a black background. We also offset the focus outline from all buttons, just to make it more visible around their coloured borders and backgrounds.

button {
padding: 0.5em 1em;
border: 0.25em solid var(--black);
color: var(--black);
background: transparent;
font: inherit;
}

button:focus {
outline-offset: 0.25em;
}

button:hover {
color: var(--white);
background: var(--black);
}

Toggle button styles (light mode)

Then we style the toggle button. In light mode, it has firebrick red text and a firebrick red border. When the button is activated, it gets white text and a firebrick red background.

[aria-pressed] {
border-color: var(--firebrick);
}

[aria-pressed='false'],
[aria-pressed='false']:hover
{
color: var(--firebrick);
}

[aria-pressed='false']:hover {
background: transparent;
}

[aria-pressed='true'],
[aria-pressed='true']:hover
{
color: var(--white);
background: var(--firebrick);
}

Default button styles (dark mode)

All of our dark mode styles go inside a prefers-color-scheme media query:

@media (prefers-color-scheme: dark) {
/* ... */
}

In dark mode, the default button styles are reversed; they have white text and a white border. On hover, they get black text and a white background.

button {
border-color: var(--white);
color: var(--white);
}

button:hover {
color: var(--black);
background: var(--white);
}

Toggle button styles (dark mode)

In dark mode, our toggle button starts with orange-red text and an orange-red border. On hover, it gets an orange-red background. The text turns black instead of white, since this has better contrast.

[aria-pressed] {
border-color: var(--orange-red);
}

[aria-pressed='false'],
[aria-pressed='false']:hover
{
color: var(--orange-red);
}

[aria-pressed='true'],
[aria-pressed='true']:hover
{
color: var(--black);
background: var(--orange-red);
}

The JavaScript

Now we just need to write our script to make the buttons work!

Setting our constants

The first thing we do is get the main element, which is the closest common ancestor of the button elements. This is because we'll be using a technique called event delegation.

We also save the message we'll show each time the 'Show alert...' button is clicked. We use the backslash character (\) to separate the string across multiple lines. This doesn't insert a line break into the actual string; it just maintains the common 80-character line length limit.

// Get the main element
const main = document.querySelector('main');

// The message for the alert
const message = 'You just activated a native HTML button element. It has the \
required button role, focus outline, and keyboard functionality by default.'
;

Handling the toggle button

As I mentioned earlier, the toggle button has an aria-pressed attribute with an initial value of 'false'.

We need a function to toggle the button's aria-pressed state between 'true' and 'false'. Let's start with an empty function declaration. The function has a single parameter, button, which will be an instance of the HTMLButtonElement interface:

/**
* Toggle a button's pressed state.
* @param {HTMLButtonElement} button
*/

function toggle(button) {
// ...
}

Within our function, the first thing we need to do is get the current (old) state of the button. We can do this by calling the getAttribute() method and saving the result to the oldState constant:

// Get the old pressed state
const oldState = button.getAttribute('aria-pressed');

Now we work out what the new state should be. If the value of oldState is 'true', the new value should be 'false', and vice versa. We save the result to the newState variable:

// Get the new pressed state
let newState;
if (oldState === 'true') {
newState = 'false';
} else if (oldState === 'false') {
newState = 'true';
}

Finally, we call the setAttribute() method, passing in the value of our newState variable:

// Update the pressed state
button.setAttribute('aria-pressed', newState);

Handling click events

Now let's wire up our click event listener. It has a single parameter, event, which refers to the Event interface:

/**
* Handle click events.
* @param {Event} event
*/

function handleClick(event) {
// ...
}

Firstly, we need to get the closest button to the element that was clicked. We can do this using the closest() method. If there is no such button, we use an empty return statement to stop executing the function.

The reason we need to do this is because, if the user clicks on the little heart icon inside the toggle button, then they element they clicked will actually be the svg or path element—not the button element itself. The closest() method ensures we get the actual button element. And because this method first checks the element itself, it will still work for the alert button, which has no child elements.

// Get the closest button
const button = event.target.closest('button');
if (!button) return;

Next, we use a switch statement to check the ID of the button that was clicked, and act accordingly. Some developers don't like switch statements, but they're ideal when you need to check multiple permutations of the same value.

  • If the alert button was clicked, we show an alert with our message.
  • If the toggle button was clicked, we toggle its pressed state.
  • Otherwise, we just quit the function.
switch (button.id) {
// If the alert button was clicked, show an alert
case 'alert':
window.alert(message);
break;

// If the toggle button was clicked, toggle its pressed state
case 'toggle':
toggle(button);
break;

// Otherwise, do nothing
default:
return;
}

Attaching the event listener

All that remains is to attach our event listener. We use our handleClick() function as the callback function for the addEventListener() method:

// Handle click events
main.addEventListener('click', handleClick);

I hope you've found this tutorial informative! You can view the demo and check the source code on GitHub. Stay tuned for next week, when we'll look at an accessible disclosure (show/hide) component.