Chapter 4. Performing Actions
This whole chapter focuses on a single HTML element, the <button>
. There are two reasons I give buttons so much space.
First, the button is a common element; you’ll find it on most pages: in a contact or newsletter sign-up form, to toggle a mobile navigation, or to close a pop-up dialog.
Second, although web developers need buttons so often, they’re bad at implementing them correctly. That’s not just based on years of my experience auditing sites others have built; there’s also data to confirm it.
That’s why this chapter focuses on the most essential requirements of the button element and how to fulfill them.
4.1 Pick the Right Element
Problem
If a button doesn’t meet basic requirements, you may exclude one or more groups of users from being able to access it or understand its purpose.
The discussion section of this recipe provides more detail, but the essential accessibility criteria for a button are:
-
It must convey its semantic button role programmatically.
-
It has a concise and straightforward accessible name (visible text or a text alternative).
-
It communicates its state (pressed, expanded, etc.) if necessary.
-
It’s recognizable as a button.
-
Its colors must have enough contrast.
-
It must be focusable and allow activation via click, touch, and key events.
Solution
Use the native button element and HTML and ARIA attributes to adjust its features according to the requirements, as shown in Examples 4-1, 4-2 and 4-3.
Example 4-1. A native submit button used in a form to submit data
<
form
>
<
label
for
=
"email"
>
Email address</
label
>
<
input
type
=
"email"
id
=
"email"
name
=
"email"
>
<
button
>
Sign up</
button
>
</
form
>
Example 4-2. A button that executes JavaScript
<
button
type
=
"button"
onclick
=
"print()"
>
Print</
button
>
Example 4-3. A button communicating its pressed state
<
button
type
=
"button"
aria-pressed
=
"true"
>
Mute</
button
>
Discussion
The button element has two main use cases: submitting a form and running JavaScript when a user interacts with it.
A button becomes a submit button when you set its type to submit or in the context of a form, as shown in Example 4-1. You usually use a submit button to send form data to a server.
If you set the type to button, the button does nothing. You do that if you want to run JavaScript when the user activates the button. For example, when you click the button in Example 4-2, it opens your browser’s print dialog. Other common uses are toggling the visibility of other elements (see Chapter 8), opening modal dialogs (see Recipe 11.3), and running other JavaScript functions.
According to the WCAG, a button must meet at least six specific requirements to be accessible and provide great UX. Let’s look at each one in turn:
- It must convey its semantic button role programmatically.
-
Your buttons must convey their semantic roles. The safest and most reliable option is to use the
<button>
element, because it has an implicitbutton
role.A screen reader will announce the button’s role alongside its name; for example, “Print, button.”
- It has an accessible name (visible text or a text alternative).
-
Regardless of whether a button contains text, it must have an accessible name. There are no exceptions to this rule. If you don’t label buttons, screen reader and voice users won’t know what to do with them.
There are different ways of labeling buttons. Recipe 4.2 describes some of them, but the best thing you can do in most cases is include text that is visible to everyone.
- It communicates its state (pressed, expanded, etc.) if necessary.
-
A button may have different states or control another element’s state: If so, it must convey its state or relationship using ARIA attributes. The button in Example 4-3 conveys that some media is muted. You can find more examples in Recipe 4.5.
- It’s recognizable as a button.
-
According to Jakob’s law, users spend most of their time on other sites, which means that users prefer your site to work the same way as all the other sites they already know. For buttons, this means they should meet the user’s visual expectations of a button. If a button looks like a button, it makes it easier for users, especially those with cognitive disabilities, to understand its purpose. That also applies to its accessible name. If identical functions have different accessible names on different pages, the site will be more challenging to use.
- Its colors must have enough contrast.
-
Just like most of the text or images of text on a web page, buttons must also meet minimum contrast requirements. The text color in a button must have a contrast ratio of at least 4.5:1 for normal text or 3:1 for large-scale or bold text.
Other colors used in the button, like the background color or colors of focus indicators, should also meet requirements for minimum contrast ratio against adjacent colors. In other words, the button’s background color or the outline of a focus indicator or border should also have a contrast ratio of at least 3:1 against the background, which typically is a parent component or the page itself. Low contrast controls are more difficult to perceive and may be completely missed by people with low vision.
- It must be tabbable and allow activation via click, touch, and key events
-
A button is an interactive element, which means that if you can click it, you must also be able to perform the same action using the keyboard by pressing
Enter
orSpace
. For that, it must be tabbable (reachable via theTab
key), which is true by default for the<button>
element.
Most websites contain buttons. Try to follow the best practices in this chapter and avoid several common alternative solutions that don’t meet the requirements, listed in Example 4-4. It’s possible to re-create the native button’s default functionality using a different element, like the <div>
, but usually it’s not worth the effort because the <button>
comes with most of the features described in this recipe.
Example 4-4. Bad practices: Common inaccessible button alternatives you should avoid
<!-- Div with a click event -->
<
div
onclick
=
"[JS function]"
>
O'Reilly books and videos</
div
>
<!-- Non-focusable button -->
<
div
role
=
"button"
onclick
=
"[JS function]"
>
O'Reilly books and videos</
div
>
<!-- Link with a click event -->
<
a
href
=
"javascript: void(0)"
onclick
=
"[JS function]"
>
Menu</
a
>
<!-- Link with a click event -->
<
a
href
=
"#"
onclick
=
"[JS function]"
>
Menu</
a
>
4.2 Label Buttons Clearly
Solution
First, if the button contains text, that text serves as its label, as shown in Example 4-5.
Example 4-5. Button with the accessible name coming from its content
<
button
type
=
"button"
>
Download</
button
>
Second, if the button contains an image, the image’s alt
attribute should provide the label, as in Example 4-6.
Example 4-6. Button with the accessible name coming from the alt
attribute of the image
<
button
type
=
"button"
>
<
img
src
=
"/images/download.svg"
alt
=
"Download"
width
=
"26"
>
</
button
>
Instead of an img
, you can also use a an SVG with a <title>
element. For cross-browser support, use aria-labelledby
on the SVG and create a reference to the title, as shown in Example 4-7.
Example 4-7. Button with the accessible name coming from the SVG
<
button
type
=
"button"
>
<
svg
viewBox
=
"0 0 39 44"
aria-labelledby
=
"title"
role
=
"img"
width
=
"26"
>
<
title
id
=
"title"
>
Download</
title
>
<
path
d
=
"M19.5 36.5 1.6 26.1v-3.6l16.3 9.4V1.5h3.2v30.4l16.3-9.4v3.6z"
/>
<
path
d
=
"M1 41.5h37"
style
=
"stroke:#000;stroke-width:3;"
/>
</
svg
>
</
button
>
You can also remove the graphic from the accessibility tree by defining an empty alt
attribute on the img
or aria-hidden="true"
on the SVG. If you do that, the button still needs a text alternative, which you can provide with visually hidden text (see Example 4-8), aria-labelledby
, or aria-label
(see Example 4-9).
Example 4-8. The visually hidden text labels the button
<
button
type
=
"button"
>
<
span
class
=
"visually-hidden"
>
Download</
span
>
<
img
src
=
"/images/download.svg"
alt
=
""
width
=
"26"
>
</
button
>
<!-- or -->
<
button
type
=
"button"
>
<
span
class
=
"visually-hidden"
>
Download</
span
>
<
svg
viewBox
=
"0 0 39 44"
aria-hidden
=
"true"
width
=
"26"
>
<
path
d
=
"M19.5 36.5 1.6 26.1v-3.6l16.3 9.4V1.5h3.2v30.4l16.3-9.4v3.6z"
/>
<
path
d
=
"M1 41.5h37"
style
=
"stroke:#000;stroke-width:3;"
/>
</
svg
>
</
button
>
<
style
>
.visually-hidden
{
clip-path
:
inset
(
50%
);
height
:
1px
;
overflow
:
hidden
;
position
:
absolute
;
white-space
:
nowrap
;
width
:
1px
;
}
</style>
Example 4-9. The aria-label
attribute labels the button
<
button
type
=
"button"
aria-label
=
"Save"
>
<
svg
aria-hidden
=
"true"
viewBox
=
"0 0 39 44"
width
=
"26"
>
<
path
d
=
"M19.5 36.5 1.6 26.1v-3.6l16.3 9.4V1.5h3.2v30.4l16.3-9.4v3.6z"
/>
<
path
d
=
"M1 41.5h37"
style
=
"stroke:#000;stroke-width:3;"
/>
</
svg
>
</
button
>
Removing nested graphics from the accessibility tree is also helpful when you combine an icon with text because the text eliminates the need for an extra label for the icon, as shown in Example 4-10.
Example 4-10. Combination of text and icon
<
button
type
=
"button"
>
Save<
img
src
=
"/images/download.svg"
alt
=
""
width
=
"26"
>
</
button
>
<
button
type
=
"button"
>
Save<
svg
aria-hidden
=
"true"
viewBox
=
"0 0 39 44"
width
=
"26"
>
<
path
d
=
"M19.5 36.5 1.6 26.1v-3.6l16.3 9.4V1.5h3.2v30.4l16.3-9.4v3.6z"
/>
<
path
d
=
"M1 41.5h37"
style
=
"stroke:#000;stroke-width:3;"
/>
</
svg
>
</
button
>
Discussion
According to the WebAIM Million 2024 report, which is the result of a yearly automated accessibility evaluation of the top 1 million websites, 28.2% of tested websites contained empty buttons, which makes it one of the top five issues the study found. An empty button is either a button with no children or a button that contains unlabeled graphics and with no other source that labels it.
There are different methods for labeling buttons. Which one you choose depends on the requirements. When you have multiple options, I recommend following the priority order Adrian Roselli describes in “My Priority of Methods for Labeling a Control”:
-
Native HTML techniques
-
aria-labelledby
pointing at existing visible text -
Visibly hidden content
-
aria-label
If the button contains only text or a combination of text and icons, put the label as text between the element’s start and end tag, as shown in Example 4-5. If there’s an icon that doesn’t provide additional information, remove it from the accessibility tree to avoid redundancy (see Example 4-10).
If there’s no visible text but only an image or icon, then its alternative text can serve as the name for the button (see Examples 4-6 and 4-7). These kinds of images are called functional images. Their alternative text should describe not what they show, but their purpose. Alternatively, you can use visually hidden text (Example 4-8), aria-labelledby
, or the aria-label
attribute (Example 4-9).
Missing accessible names are probably the most common issue, but wrong ones can be problematic, too. Sometimes they’re in the wrong natural language, like English on a French site. Sometimes they contain unresolved variables or placeholder text. These issues usually arise with icon-only buttons. You can easily miss visually hidden text or labels provided via attributes like aria-label
since they’re visible only in the code, not the rendered UI. You should favor buttons with visible text because they are universally understandable and less error-prone.
The label should be informative and concise. Don’t label a button “Click here to open or close the navigation.” “Navigation” is sufficient.
4.3 Remove Default Button Styles
Problem
Even when a button doesn’t look like a button, it must still meet most of the requirements described in Recipe 4.1. If it doesn’t, it might not be accessible to keyboard and screen reader users. If you pick a generic element with fewer default styles instead of the <button>
element, users might not be able to access the fake button or understand its purpose.
Solution
If you want to use a button but don’t want to make it look like a button, you should still use the <button>
element but remove the default button styles. CSS offers three efficient strategies for removing default button styles, as shown in Examples 4-11, 4-12, and 4-13.
Example 4-11. Removing or resetting properties manually
button
{
background
:
none
;
border
:
0.1em
solid
transparent
;
font
:
inherit
;
padding
:
0
;
}
Example 4-12. Resetting all button properties to their initial value
button
{
all
:
initial
;
}
button
:focus-visible
{
outline
:
0.1em
solid
;
outline-offset
:
0.1em
;
}
Example 4-13. Resetting all button properties to their initial value except for inheritable properties
button
{
all
:
unset
;
}
button
:focus-visible
{
outline
:
0.1em
solid
;
outline-offset
:
0.1em
;
}
Discussion
Button elements in operating systems and web pages have a particular default shape and styling: a rectangle with a border, a background color, and some padding between the text and the border. When you style a page, you usually stick to these default characteristics. You change the default values and maybe add properties, as shown in Example 4-14. You might also have versions of that button that vary in size, color, and shape.
Example 4-14. Custom styles for buttons
button
{
--
_l
:
0
.
47
;
background
:
oklch
(
var
(
--
_l
)
0
.
05
195
.
6
);
color
:
#fff
;
font-size
:
1.2rem
;
font-family
:
inherit
;
padding
:
0.4em
0.9em
;
border-radius
:
3px
;
border
:
0
;
min
-
inline
-
size
:
7rem
;
}
button
:is
(
:hover
,
:focus-visible
)
{
--
_l
:
0
.
27
;
}
Some buttons don’t look like buttons because they consist of only an icon. How you implement such a button is crucial. One of the most common accessibility issues on most websites I audit is fake buttons: many developers assume that if a control doesn’t look like a button, it doesn’t have to be a <button>
element. Their reasoning is: if there are no button styles in the first place, you don’t have to remove them. That often results in code like that in Example 4-15, which looks harmless but makes a considerable difference to accessibility.
Example 4-15. Bad practice: A fake div
button
<
div
class
=
"button"
aria-label
=
"Navigation"
>
<
svg
width
=
"24"
height
=
"24"
aria-hidden
=
"true"
>
<
path
d
=
"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"
>
</
svg
>
</
div
>
<
script
>
const
button
=
document
.
querySelector
(
".button"
);
button
.
addEventListener
(
"click"
,
(
e
)
=>
{
console
.
log
(
"do something"
);
});
</
script
>
There are several issues with this “button:”
-
It’s not focusable via keyboard.
-
Even if it was focusable, activation via Enter or Space wouldn’t work.
-
Screen readers don’t announce it as a button.
-
Some screen readers don’t announce it at all.
-
You shouldn’t use
aria-label
on generic elements because it’s invalid to name them.
This “button” not being focusable affects keyboard and screen reader users, but the latter can use the virtual cursor to access the fake button. If a click event listener is attached to the div
, some screen readers will hint that you can click it. Nevertheless, if you look at Table 4-1, you can see that using a div
or any other noninteractive element isn’t reliable. If you have need to remove default button styles, sticking to the <button>
element and resetting styles using CSS is the safest choice.
Screen reader | Browser | Screen reader narration |
---|---|---|
NVDA |
Firefox |
Navigation |
JAWS |
Chrome |
N/A |
Narrator |
Edge |
N/A |
TalkBack |
Chrome |
Navigation |
VoiceOver macOS |
Safari |
Navigation, empty group |
VoiceOver iOS |
Safari |
N/A |
It wouldn’t be viable to use a generic element instead of a button or put event listeners on noninteractive elements. You need an interactive element if the user can interact with a component.
The four lines of CSS you see in Example 4-11 are all you need to remove buttons’ default user-agent styles—assuming you haven’t added more rules, like we did in Example 4-14. In that case, you would have to reset those styles as well. That’s the problem with this manual approach: you must keep track of changes to your custom default styles and adjust the reset styles accordingly, or else reset every possible property in advance.
The approach illustrated in Examples 4-12 and 4-13 is much more efficient. You can use the all
property in CSS to reset all properties of an element, except unicode-bidi
, direction
, and CSS Custom Properties. Depending on your needs, you can set its value to initial
or unset
(see Figure 4-1).
The initial
keyword sets all property values to their initial value. Each property has an initial value, defined in the property’s definition table. For example, if you look at the color property in the specification, the defined initial value in the definition table is CanvasText
. Note that the initial value is not the default property value defined in the user agent.
The unset
keyword resets a property to its inherited value, if the property naturally inherits from its parent and to its initial value if not. That can be useful if you want to keep specific properties like font-family
or color
.
Either way, you may have to bring back focus
and hover
styles, because all
resets those, too. The gist is that even when a button doesn’t look like a button, it must behave like one.
4.4 Add States and Properties
Problem
When screen reader users use buttons to control other elements on the page or settings for the site, those buttons must provide as much information as possible. That may include the type of button, the type of associated element, or the state of a button or controlled element. If that information is missing, it’s much more complicated—or sometimes even impossible—to tell if the user has activated the button successfully and what happens when they do.
Solution
A button that toggles the visibility of another element needs to communicate whether the element is expanded, as shown in Example 4-16.
Example 4-16. The aria-expanded
attribute on the button communicating whether the navigation is visible
<
nav
>
<
button
aria-expanded
=
"false"
aria-controls
=
"main_nav"
>
Navigation
<
/
button
>
<
ul
id
=
"main_nav"
>
…
<
/
ul
>
<
/
nav
>
<
style
>
[
aria-expanded
=
"false"
]
+
ul
{
display
:
none
;
}
</style>
<
script
>
const
button
=
document
.
querySelector
(
"button"
)
;
button
.
addEventListener
(
"click"
,
(
e
)
=>
{
const
isExpanded
=
button
.
getAttribute
(
"aria-expanded"
)
===
"true"
;
button
.
setAttribute
(
"aria-expanded"
,
!
isExpanded
)
;
}
)
;
<
/
script
>
aria-expanded
must be on the button and not the expandable element.The list is hidden, depending on the value of the attribute.
Click event on the button toggles the value of the
aria-expanded
attribute.
A button that turns a setting on or off must communicate whether it’s active, as shown in Example 4-17.
Example 4-17. A button communicating its pressed state
<
button
type
=
"button"
aria-pressed
=
"true"
>
Add to favourites
<
/
button
>
<
script
>
const
button
=
document
.
querySelector
(
"button"
)
;
button
.
addEventListener
(
"click"
,
(
e
)
=>
{
const
isPressed
=
button
.
getAttribute
(
"aria-pressed"
)
===
"true"
;
button
.
setAttribute
(
"aria-pressed"
,
!
isPressed
)
;
}
)
;
<
/
script
>
A button that toggles a setting must communicate whether it’s active. Figure 4-2 and Example 4-18 show a switch button communicating its state using the aria-checked
attribute.
Example 4-18. A switch button
<
button
role
=
"switch"
aria-checked
=
"false"
>
Functional cookies
<
/
button
>
<
script
>
const
button
=
document
.
querySelector
(
"button"
)
;
button
.
addEventListener
(
"click"
,
(
e
)
=>
{
const
isChecked
=
button
.
getAttribute
(
"aria-checked"
)
===
"true"
;
button
.
setAttribute
(
"aria-checked"
,
!
isChecked
)
;
}
)
;
<
/
script
>
<
style
>
button
{
--toggle-offset
:
0.125em
;
--toggle-height
:
1.6em
;
--toggle-background
:
oklab
(
0
.
82
0
0
);
all
:
unset
;
align-items
:
center
;
display
:
flex
;
gap
:
0.5em
;
position
:
relative
;
}
button
::before
{
background
:
var
(
--
toggle
-
background
);
border-radius
:
4em
;
content
:
""
;
display
:
inline-block
;
height
:
var
(
--
toggle
-
height
);
transition
:
background
0.3s
,
box-shadow
0.3s
;
width
:
3em
;
}
button
::after
{
--
_size
:
calc
(
var
(
--
toggle
-
height
)
-
(
var
(
--
toggle
-
offset
)
*
2
));
background
:
#FFF
;
border-radius
:
50%
;
content
:
""
;
height
:
var
(
--
_size
);
left
:
var
(
--
toggle
-
offset
);
position
:
absolute
;
transition
:
translate
0.3s
;
top
:
var
(
--
toggle
-
offset
);
width
:
var
(
--
_size
);
}
button
:focus-visible
::before
{
outline
:
2px
solid
;
outline-offset
:
2px
;
}
button
:is
(
:focus-visible
,
:hover
)
::before
{
box-shadow
:
0px
0px
3px
1px
rgb
(
0
0
0
/
0
.
3
);
}
[
aria-checked
=
"true"
]
{
--toggle-background
:
oklab
(
0
.
7
-0
.
18
0
.
17
);
}
button
[
aria-checked
=
"true"
]
::after
{
translate
:
100%
0
;
}
</style>
Resets the default button styles.
Pseudoelement for the background of the switch.
Pseudoelement for the movable indicator of the switch.
Adds custom focus styles.
Shows a box shadow on
:hover
and:focus-visible
.Turns the background color of the switch to green when active.
Moves the indicator to the end of the switch when active.
A button can communicate its state and what kind of element it controls, as shown in Example 4-19.
Example 4-19. A button controlling a menu
<
button
type
=
"button"
aria-haspopup
=
"menu"
aria-expanded
=
"false"
id
=
"button_settings"
>
Settings
<
/
button
>
<
ul
role
=
"menu"
aria-labelledby
=
"button_settings"
hidden
>
<
li
role
=
"none"
>
<
button
role
=
"menuitem"
>
<
/
button
>
<
/
li
>
<
li
role
=
"none"
>
<
button
role
=
"menuitem"
>
Save
<
/
button
>
<
/
li
>
<
/
ul
>
<
style
>
[
aria-expanded
=
"true"
]
+
ul
{
display
:
block
;
}
</style>
<
script
>
const
button
=
document
.
querySelector
(
"button"
)
;
button
.
addEventListener
(
"click"
,
(
e
)
=>
{
const
isExpanded
=
button
.
getAttribute
(
"aria-expanded"
)
===
"true"
;
button
.
setAttribute
(
"aria-expanded"
,
!
isExpanded
)
;
}
)
;
<
/
script
>
Discussion
Many attributes in the ARIA specification provide elements with states or properties. When you build custom JavaScript widgets, you’ll use some of them often. The arial-label
property, for example, gives an element an accessible name, while the aria-hidden
state removes an element from the accessibility tree.
This recipe focuses on four attributes commonly used with buttons.
The expanded state
You use the aria-expanded
attribute on a button element to indicate whether a grouping element it controls is expanded or collapsed.
Note
The controlled element can be pretty much any element, but it’s usually a grouping element like <div>
, <p>
, or <ul>
.
In Example 4-16, you can see that the button has the attribute and communicates that the associated element is expanded. Users should understand from the button text which element the button controls. That’s why you should avoid generic text like “show/hide.” The attribute can be useful for fly-in navigations (see Recipe 7.5), for submenus in nested navigations (see Recipe 7.7), and for disclosure widgets (see Recipe 8.3).
The button’s job is to communicate whether the controlled element is expanded. You must follow three essential rules when you apply it:
-
You set the attribute on the element that does the controlling (the button), not the controlled element (the group).
-
The sheer presence of the attribute is not enough; you must set it to “true” or “false.”
-
The attribute must be present and set before the user interacts with the button. If you set the value to “false,” it means that the controlled element is collapsed. If you don’t set the attribute, the button doesn’t control anything.
As you can see in Example 4-16, the button has another ARIA attribute: aria-controls
.
The controls property
The aria-controls
attribute identifies the element the button controls. The value is a list of one or more id
references. With the attribute present, a screen reader can identify a relationship between a button and another element. JAWS doesn’t automatically announce this relationship, but you can use the JAWSKEY
+ ALT
+ M
shortcut to jump directly to the controlled element.
For disclosure widgets, this attribute isn’t well supported (JAWS is the only screen reader that uses it), and it’s unclear how much importance screen reader vendors plan to attach to it. However, it does no harm. For NVDA, this has been an open issue since 2018; an open discussion in the ARIA GitHub repository dates to 2019. Whether you use it is up to you. Until there’s an official recommendation for or against it, I’m using it in the following chapters.
Pressed state
The aria-pressed
attribute indicates the current “pressed” state of toggle buttons.
A toggle button is similar to a checkbox but not quite the same. Aside from the styling, the most significant difference is that a checkbox conveys only a state (checked/unchecked/mixed), whereas a button performs an action. When users click a toggle button, they expect something to happen. Pressing the button in Example 4-17 toggles the value of the aria-pressed
attribute and changes the state of the Add to favorites button. Client-side changes like that require you to work in an environment that relies on JavaScript because the pressed state must change on click. If that’s not the case and you want to enhance the control progressively, use a checkbox instead. Adrian Roselli describes the differences between checkboxes and toggle buttons in depth in “Under-Engineered Toggles” and “Under-Engineered Toggles Too”.
Checked state
The aria-checked
attribute indicates the current “checked” state of checkboxes, radio buttons, and other widgets.
You’re not supposed to use aria-checked
on a button with the role button
, but you can use it on a switch (see Example 4-18). A switch is a type of checkbox that represents on/off values instead of checked/unchecked/mixed values. It provides approximately the same functionality as a checkbox or toggle button, but you can distinguish between them for screen readers in a fashion consistent with its on-screen appearance. Switches are a problematic pattern in terms of user experience. I talk a bit about why in Recipe 9.1. If you decide to use switches, test them thoroughly with users, including those who use screen readers.
haspopup property
The aria-haspopup
attribute indicates that a button controls another interactive pop-up element. Most screen readers also announce the type of pop-up element. The attribute supports seven values: true, false, menu, dialog, grid, listbox, and tree, indicating that the referenced element has the respective role. true is the same as menu.
Depending on the screen reader software you’re using, if you focus the button in Example 4-19, it will announce something like “Settings, button, menu” (JAWS) or “Settings, pop-up button, menu pop-up” (VoiceOver). The value you use must keep its promise: if the value is menu, the role of the controlled element should also be menu and function accordingly. JAWS, for example, will also announce appropriate instructions when an attribute with a specific value is present. For true and menu, it announces, “Press Space to activate the menu. Then navigate with arrow keys.” For listbox, tree, and grid: “To activate, press Enter.”
The values true and menu are well supported in all screen readers. However, TalkBack and Narrator don’t support grid, dialog, listbox, or tree.
4.5 Don’t Disable Buttons
Problem
Disabling buttons can cause more problems for users than benefits.
Missing feedback
When you click a disabled button, nothing happens. The button doesn’t explain what’s wrong or help you fix the problem. It provides no helpful feedback. If the user thinks their answers are correct, not providing feedback can make the UI feel broken.
Low contrast
WCAG’s minimum contrast rule doesn’t apply to disabled controls, but they’re often hard to read, especially for people with low vision.
Deception
It’s not always apparent that buttons are disabled. Some users will try to click them; if nothing happens, they can feel irritated, confused, or disappointed. That may occur because the design isn’t distinct or because disabled buttons usually contain call-to-action words like “submit,” “send,” or “order,” which lure users into clicking them.
Solution
Don’t disable buttons. Users should always be able to interact with them and get feedback.
Discussion
The point of disabling buttons is to avoid premature clicks and to make it difficult for users to make mistakes when filling out forms. Developers use this technique to indicate that something important is wrong or missing, and must be fixed before the user can continue to the next step. That sounds good, but a disabled button is not the best solution. It’s a flawed pattern. A button can be disabled for many reasons, but using it forces the user to figure out what went wrong.
Instead of disabling buttons, there are several measures you can take to avoid errors up front:
-
Use clear labels for your input fields.
-
Add hints and descriptions when the label alone isn’t clear enough.
-
Split complex forms into multiple steps or pages to reduce cognitive load.
-
Always enable buttons and validate input on submit.
-
Give clear error messages.
When the user presses the button, provide them with a list of errors that point to the respective field, or move focus to the erroneous field if there’s only one (see Recipe 9.4 for details).
See Also
-
“Buttons and the Baader–Meinhof phenomenon” by Manuel Matuzović
-
“The problem with disabled buttons and what to do instead” by Adam Silver
-
“Usability Pitfalls of Disabled Buttons, and How To Avoid Them” by Vitaly Friedman
-
“Making Disabled Buttons More Inclusive” by Sandrina Pereira
-
“Perceived affordances and the functionality mismatch” by Léonie Watson
Get Web Accessibility Cookbook now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.