curriculum/challenges/english/blocks/lecture-understanding-aria-expanded-aria-live-and-common-aria-states/672a54f29d783890d1f94740.md
Semantic form control elements like input, select, textarea, button, and fieldset have built-in states that are conveyed to assistive technologies.
For example, you could use the disabled attribute to disable a button or the checked attribute to indicate that a checkbox is checked.
But if you are creating a custom control element, you need to use ARIA attributes to convey the state of the control to assistive technologies.
In this lesson, we will discuss a few common ARIA states that you can use on custom control elements.
The first ARIA state we will discuss is aria-selected. This state is used to indicate that an element is selected. You can use this state on custom controls like a tabbed interface, a listbox, or a grid.
Here is an example of how you can use aria-selected on a custom tab control:
:::interactive_editor
<link rel="stylesheet" href="styles.css">
<div role="tablist">
<button role="tab" aria-selected="true">Tab 1</button>
<button role="tab" aria-selected="false">Tab 2</button>
<button role="tab" aria-selected="false">Tab 3</button>
</div>
<script src="index.js"></script>
[role="tablist"] {
display: flex;
border-bottom: 2px solid #ddd;
gap: 0.25rem;
font-family: system-ui, sans-serif;
}
[role="tab"] {
appearance: none;
border: none;
background: none;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: 1rem;
color: #444;
border-radius: 4px 4px 0 0;
transition: background-color 0.2s, color 0.2s;
}
[role="tab"]:hover {
background-color: #f3f3f3;
}
[role="tab"][aria-selected="true"] {
background-color: #fff;
color: #0078d4;
border: 2px solid #0078d4;
border-bottom: 2px solid #fff;
font-weight: 600;
position: relative;
z-index: 1;
}
[role="tab"]:focus {
outline: 2px solid #0078d4;
outline-offset: 2px;
}
document.addEventListener("click", (event) => {
const clickedTab = event.target.closest('[role="tab"]');
if (!clickedTab) return;
const tablist = clickedTab.closest('[role="tablist"]');
const tabs = tablist.querySelectorAll('[role="tab"]');
tabs.forEach((tab) => {
const isSelected = tab === clickedTab;
tab.setAttribute("aria-selected", isSelected);
tab.tabIndex = isSelected ? 0 : -1;
});
});
document.addEventListener("keydown", (event) => {
const activeTab = document.activeElement;
if (activeTab.getAttribute("role") !== "tab") return;
const tablist = activeTab.closest('[role="tablist"]');
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
const index = tabs.indexOf(activeTab);
let newIndex = index;
if (event.key === "ArrowRight") newIndex = (index + 1) % tabs.length;
if (event.key === "ArrowLeft") newIndex = (index - 1 + tabs.length) % tabs.length;
if (newIndex !== index) {
tabs[newIndex].focus();
tabs[newIndex].click();
}
});
:::
Tabs are used to display multiple panels of content in a limited space. The aria-selected state is used to indicate which tab is currently selected.
When the user selects a tab, the aria-selected state of the selected tab is set to true, and the aria-selected state of the other tabs is set to false.
Another common ARIA state is aria-disabled. This state is used to indicate that an element is disabled only to people using assistive technologies, such as screen readers. It is important to note that aria-disabled does not actually disable the element. It is up to you, the developer, to make it look and act like a disabled element. This attribute is also commonly used on native HTML elements in place of the disabled attribute. Which one you choose will depend on the context the button is being used.
Here is an example of how you can use aria-disabled on a custom edit button:
:::interactive_editor
<link rel="stylesheet" href="styles.css">
<div role="button" tabindex="-1" aria-disabled="true">Edit</div>
[role="button"] {
display: inline-block;
background-color: #0078d4;
color: #fff;
padding: 0.5rem 1rem;
border-radius: 6px;
font-family: system-ui, sans-serif;
font-size: 1rem;
text-align: center;
cursor: pointer;
user-select: none;
transition: background-color 0.2s, transform 0.1s, opacity 0.2s;
}
[role="button"]:not([aria-disabled="true"]):hover {
background-color: #005fa3;
}
[role="button"]:not([aria-disabled="true"]):focus {
outline: 2px solid #005fa3;
outline-offset: 2px;
}
[role="button"]:not([aria-disabled="true"]):active {
transform: scale(0.97);
}
[role="button"][aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
background-color: #b0b0b0;
color: #f2f2f2;
}
:::
The aria-disabled attribute is used to tell screen reader users that the edit button is disabled and cannot be interacted with. Again, it does not actually disable the button. When using aria-disabled,you will need to apply styling and JavaScript to make the control look and behave like a disabled button.
In most cases, you will probably use the native button element, but there are cases where you might need to use a custom control. So, it is essential to know how to convey the state of the control to assistive technologies.
The next ARIA state we will discuss is aria-haspopup. This state is used to indicate that an interactive element will trigger a popup element when activated. You can only use the aria-haspopup attribute when the popup has one of the following roles: menu, listbox, tree, grid, or dialog. The value of aria-haspopup must be either one of these roles or true, which defaults to the menu role.
Here is an example of a file editor menu that uses aria-haspopup:
:::interactive_editor
<link rel="stylesheet" href="styles.css">
<button id="menubutton" aria-haspopup="menu" aria-controls="filemenu" aria-expanded="false">File</button>
<ul id="filemenu" role="menu" aria-labelledby="menubutton" hidden>
<li role="menuitem" tabindex="-1">Open</li>
<li role="menuitem" tabindex="-1">New</li>
<li role="menuitem" tabindex="-1">Save</li>
<li role="menuitem" tabindex="-1">Delete</li>
</ul>
#menubutton {
background-color: #0078d4;
color: #fff;
border: none;
padding: 8px 14px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
position: relative;
}
#menubutton:hover,
#menubutton:focus {
background-color: #005ea2;
outline: none;
}
#filemenu {
list-style: none;
padding: 4px 0;
margin: 4px 0 0;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #fff;
width: 160px;
position: absolute;
z-index: 1000;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#filemenu[hidden] {
display: none;
}
#filemenu [role="menuitem"] {
display: block;
padding: 8px 12px;
font-size: 14px;
color: #333;
cursor: pointer;
}
#filemenu [role="menuitem"]:hover,
#filemenu [role="menuitem"]:focus {
background-color: #e5f1fb;
outline: none;
}
#filemenu [role="menuitem"]:focus-visible {
box-shadow: inset 0 0 0 2px #0078d4;
}
:::
The aria-haspopup state is used to indicate that the File menu button will open a popup menu when activated. Screen reader users may hear this additional information when they navigate to the button.
You will need to use JavaScript to show and hide the popup menu, and to implement proper keyboard support for interacting with the menu. Also, please note that the ARIA menu role refers to a very specific type of menu. It generally refers to a list of actions that the user can invoke, similar to a menu on a desktop application. It does not include more common uses of what we typically refer to as "menus", such as navigation menus. Realistically, most "menus" you create on the web will not be ARIA menus and you will not use aria-haspopup with them.
The next ARIA state we will discuss is aria-required. The aria-required attribute is used to indicate that a field needs to be filled out before the form is submitted.
Here is an example of working with the aria-required attribute for a custom form control.
:::interactive_editor
<link rel="stylesheet" href="styles.css">
<div id="name-label">Full Name*</div>
<div role="textbox" contenteditable aria-labelledby="name-label" aria-required="true" id="name"></div>
#name-label {
font-family: system-ui, sans-serif;
font-size: 0.95rem;
font-weight: 600;
margin-bottom: 0.25rem;
color: #333;
}
[role="textbox"] {
display: block;
width: 100%;
min-height: 2rem;
padding: 0.5rem 0.75rem;
border: 1.5px solid #ccc;
border-radius: 4px;
font-family: system-ui, sans-serif;
font-size: 1rem;
color: #222;
background-color: #fff;
line-height: 1.4;
transition: border-color 0.2s, box-shadow 0.2s;
}
[role="textbox"]:hover {
border-color: #999;
}
[role="textbox"]:focus {
outline: none;
border-color: #0078d4;
box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.25);
}
[role="textbox"]:empty::before {
content: attr(data-placeholder);
color: #aaa;
pointer-events: none;
}
[role="textbox"][aria-required="true"] {
border-left: 3px solid #e81123;
padding-left: calc(0.75rem - 3px);
}
:::
We need to use the contenteditable attribute so users can type in their input. We are also using the aria-required attribute set to true to indicate that this custom form control is required.
To make the form control look like a normal form control, you would need to add CSS. You would also need to add JavaScript to prevent the form from being submitted without content.
If the label already has the word required, then you should omit the aria-required attribute. This ensures that screen readers only announce the word required once.
In most cases, you will probably use the native label and form elements with the required attribute. But if you need to create a custom form control, then it is important to add the aria-required attribute when necessary.
Additionally, the aria-required attribute can also be used on native form inputs, such as the input, textarea, and select elements. This is often preferred to the native required attribute, since the required attribute may have potential usability and accessibility concerns, particularly with the default error handling provided by the browser. Ultimately, you will need to test in order to determine which attribute is best for your situation.
The last ARIA state we will discuss is aria-checked. This attribute is used to indicate whether an element is in the checked state. It is most commonly used when creating custom checkboxes, radio buttons, switches, and listboxes.
Here is an example of how you can use aria-checked on a custom checkbox control:
:::interactive_editor
<link rel="stylesheet" href="styles.css">
<div role="checkbox" aria-checked="true" tabindex="0">Checkbox</div>
[role="checkbox"] {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: system-ui, sans-serif;
font-size: 1rem;
cursor: pointer;
user-select: none;
color: #222;
}
[role="checkbox"]::before {
content: "";
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid #666;
border-radius: 4px;
background-color: #fff;
transition: all 0.2s ease;
box-sizing: border-box;
}
[role="checkbox"]:hover::before {
border-color: #0078d4;
}
[role="checkbox"]:focus::before {
outline: 2px solid #0078d4;
outline-offset: 2px;
}
[role="checkbox"][aria-checked="true"]::before {
background-color: #0078d4;
border-color: #0078d4;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'><path fill='white' d='M5.2 10.4L2 7.2l1.1-1.1 2.1 2.1L10.9 2.5 12 3.6z'/></svg>");
background-repeat: no-repeat;
background-position: center;
}
[role="checkbox"][aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
:::
Native checkbox elements have a built-in checked state that is conveyed to assistive technologies. But if you are creating a custom checkbox control, you will need to use the aria-checked attribute to indicate its state.
When the user interacts with the custom checkbox control, you will need to use the aria-checked state to reflect the new state of the checkbox. When the checkbox is checked, the aria-checked attribute is set to true. When it is unchecked, it is set to false.
Native elements typically have better support and built-in accessibility features.
However, if you must create custom controls, using ARIA attributes is essential to convey the state of these controls to assistive technologies effectively.
As always, test your work to ensure that ARIA attributes are applied correctly and that the custom control functions in a way that is both accessible and user-friendly.
What ARIA state would you use to indicate that a tab is currently selected?
aria-disabled
This state shows which tab is active.
aria-selected
aria-haspopup
This state shows which tab is active.
aria-checked
This state shows which tab is active.
2
In the context of custom controls, which ARIA state is used to indicate that an element is currently disabled?
aria-checked
This state shows that an element cannot be interacted with.
aria-disabled
aria-selected
This state shows that an element cannot be interacted with.
aria-haspopup
This state shows that an element cannot be interacted with.
2
When you want to indicate that a menu item has a submenu, which ARIA state should be used?
aria-checked
One of the state heavily suggests that a popup is present.
aria-disabled
One of the state heavily suggests that a popup is present.
aria-haspopup
aria-selected
One of the state heavily suggests that a popup is present.
3