toggleAttribute and ARIA properties
7 min read
toggleAttribute, a modern method on DOM elements, got added around late 2018 to all modern browsers. It is a convenience function (albeit unchainable) to either add or remove an attribute.
ARIA boolean attributes
Read section ARIA boolean attributesWhen creating an accessible interface, you will have to dynamically modify ARIA attributes. Boolean ARIA attributes (like aria-disabled
) can be set to true
or false
. Any other value (including none at all) are treated as the default value (usually false
). Toggling these attributes is a frequent use case. Here’s how you’d like to do that:
<div id="toggleDisabled" role="button" tabindex="-1">
Custom button
</div>
<script>
toggleDisabled.toggleAttribute('aria-disabled'); toggleDisabled.innerText =
toggleDisabled.getAttribute('aria-disabled') === 'true' ? 'Disabled' : 'Enabled';
</script>
HTML boolean attributes
Read section HTML boolean attributesEnabled
, you say? But I thought I toggled aria-disabled
when it previously did not exist. It should be disabled then, right? Wrong. aria-disabled
must literally be set to true
. This is contrary to HTML boolean attributes, like disabled
on a button
:
<button id="domDisabled">Custom button</button>
<script>
domDisabled.disabled = true; // Using the `disabled` property setter sets this HTML5 boolean attribute to an empty
string, which means `true` domDisabled.innerText = domDisabled.getAttribute('disabled') === '' ? 'Disabled' :
'Enabled';
</script>
When I write <button disabled>
, I of course want a disabled button. <div role="button" aria-disabled>
, surprisingly, represents a semantically enabled button.
All HTML boolean attributes are available as a property on the DOM element which implements them, serving as getters and setters. The reverse is not necessarily true.. toggleAttribute
ends up being quite useless as a result. The only use case I can imagine is to interact with Web Components that do not provide their attributes as properties.
Fixing this is as simple as adding a third argument to toggleAttribute
The second argument to toggleAttribute
is used to force an attribute on or off, like .classList.toggle. The third argument should provide the value for the on
case. Right now, whenever an attribute is toggled on, it is always set to ""
.
ARIA boolean properties
Read section ARIA boolean propertiesSafari implemented the ARIAMixin specification, with Blink (Chrome, etc.) following up early 2020. This specification added additional properties for many aria-
attributes to all DOM elements. To refer to the aria-disabled
attribute, you can use ariaDisabled
:
ARIAMixin properties like ariaDisabled
are not supported by Firefox as of writing. You might have to use another browser to view these examples.
<div id="ariaDisabledToggle" role="button" tabindex="-1">
Custom button
</div>
<script>
ariaDisabledToggle.ariaDisabled = !ariaDisabledToggle.ariaDisabled; ariaDisabledToggle.innerText =
ariaDisabledToggle.getAttribute('aria-disabled') === 'true' ? 'Disabled' : 'Enabled';
</script>
Not so fast. Did you spot the mistake? Committees never learn. ariaDisabled
can be either a string or null (no attribute set). If you set it to a non-string, non-null value, it is coerced to a string. Invalid values (like if you were to do .ariaDisabled = {}
) are treated as false
.
As you would expect, the first time you toggle ariaDisabled
, it becomes 'true'
. Not for the reason you were probably thinking, though. !null
evaluates to true
, so ariaDisabled
is set to String(!null)
(which evaluates to 'true'
). Now, when you toggle it again, look what happens:
<div id="ariaDisabledToggle2" role="button" tabindex="-1">
Custom button
</div>
<script>
ariaDisabledToggle2.ariaDisabled = !ariaDisabledToggle2.ariaDisabled; ariaDisabledToggle2.ariaDisabled =
!ariaDisabledToggle2.ariaDisabled; ariaDisabledToggle2.innerText = ariaDisabledToggle2.getAttribute('aria-disabled')
=== 'true' ? 'Disabled' : 'Enabled';
</script>
Works as expected, right? Glad I was extra careful there to write a unit test to test the “return to default” behavior.
JavaScript strings follow one simple rule when coerced to a boolean: if they are empty (''
), they evaluate to false
. Otherwise, they evaluate to true
:
Boolean('true')
returnstrue
Boolean('false')
returnstrue
Boolean('')
returnsfalse
That means !'true'
evaluates to 'false'
.
Third time’s the charm
Read section Third time’s the charmWhat if we try to toggle ariaDisabled
for a third time, though? Now its value will be 'false'
. !'false'
evaluates to false
. Let’s just see what happens:
<div id="ariaDisabledToggle3" role="button" tabindex="-1">
Custom button
</div>
<script>
ariaDisabledToggle3.ariaDisabled = !ariaDisabledToggle3.ariaDisabled; ariaDisabledToggle3.ariaDisabled =
!ariaDisabledToggle3.ariaDisabled; ariaDisabledToggle3.ariaDisabled = !ariaDisabledToggle3.ariaDisabled;
ariaDisabledToggle3.innerText = ariaDisabledToggle3.getAttribute('aria-disabled') === 'true' ? 'Disabled' :
'Enabled';
</script>
No matter how many times we try to toggle ariaDisabled
using this approach, the button will forever remain enabled.
This is not a JavaScript problem. Implicit type coercions are generally considered to be confusing. It is a good thing that Boolean('false')
does not get any special treatment. The real problem is that boolean ARIA properties coerce to a string. HTML attributes don’t do this. button.disabled
can only ever be true
or false
.
Negating a boolean ARIA property correctly
Read section Negating a boolean ARIA property correctlyWhat you actually want to do here is have ariaDisabled
become true
if it is any value that is not 'true'
, and false
otherwise:
<div id="ariaDisabledInterval" role="button" tabindex="-1">
Custom button
</div>
<script>
function toggleDisabled() {
ariaDisabledInterval.ariaDisabled = ariaDisabledInterval.ariaDisabled !== 'true';
ariaDisabledInterval.innerText = ariaDisabledInterval.ariaDisabled === 'true' ? 'Disabled' : 'Enabled';
}
toggleDisabled();
setInterval(toggleDisabled, 2000);
</script>
This odd behavior is why I used .getAttribute('aria-disabled') === 'true'
in all the examples. To check if an ARIA boolean property is true, you must compare it to the string 'true'
: .ariaDisabled === 'true'