The id
attribute is arguably one of the most popular attributes used in HTML, second only to the class
attribute. It serves a lot of purposes, which means it can easily be misunderstood or misused. Let's take a look at what it's really for, what it's not for, and what alternatives might be better.
When to use the id
attribute
Associate a button with a form
A form's submit button doesn't need to be nested inside the form
element if the form
element is assigned a unique id
and the button
has a form
attribute with the same value.
<form id="nameForm">
<input name="name" />
<input name="surname" />
</form>
...
<button type="submit" form="nameForm">
Submit the form
</button>
Associate a label with an input
Every input
element needs a companion label
input. It's not enough to just render text near the input though, the label must be properly associated with the input so that browsers know they're associated. This doesn't just help screen readers, the label text is part of the focus area for the input so users expect to be able to click on the label text to focus on the input. This UX is provided for free as the input is rendered inside the label or if the input has a unique id
and the label has a for
attribute with the same value.
<label for="givenNameField">
Given name
</label>
<input id="givenNameField" type="text" />
Indicate an element should be the accessible name of another
Using aria-labeledby on an element allows you to indicate that another element's contents should be considered the accessible name for the element. Screen readers use this when announcing an element to the user. This is similar to aria-label but aria-labeledby
takes the highest precedence and should be preferred over aria-label
so long as the appropriate text is rendered somewhere on the page.
The value of aria-labeledby
can be a space-delimited list of IDs, so it's possible for the label to be comprised of multiple elements.
<h2 id="formTitle">Favorite color</h2>
<form aria-labeledby="formTitle">
...
</form>
Indicate an element should be the accessible description of another
Using aria-describedby on an element allows you to indicate that another element's contents describe the element. While it sounds similar to aria-labeledby
, aria-describedby
is intended to be used for longer descriptions. This is similar to aria-description but aria-describedby
takes the highest precedence and should be preferred over aria-label
so long as the appropriate text is rendered somewhere on the page.
The value of aria-describedby
can be a space-delimited list of IDs, so the description can be comprised of multiple elements.
<p id="formDescription">
Let us know what your favorite color is so we can send you some sweet swag!
</p>
<form aria-describedby="formDescription">
...
</form>
Indicate an element contains detailed information about another
Using aria-details on an element allows you to indicate that another element's contents contain additional details about it. This is very similar to aria-describedby
the difference being that aria-details
is intended to be used when the describing element contains more complex and structured content, such as lists, images, links, etc., and aria-describedby
is intended to be used with a text-only description.
The value of aria-details
can be a space-delimited list of IDs, so the detailed description can be comprised of multiple elements.
<button type="submit" aria-details="finePrint">
Purchase
</button>
<div id="finePrint">
<p>Purchasing this means that lorem ipsum whatever...</p>
<h3>Known side effects</h3>
<ul>
<li>...</li>
<li>...</li>
</ul>
<nav aria-label="More information">
<ul>
<li><a>...</a></li>
<li><a>...</a></li>
<li><a>...</a></li>
</ul>
</nav>
</div>
Indicate that interacting with an element affects another
In dynamic web pages, it's common to have buttons with cause-and-effect behavior that affects another element on the page. Clicking a button may make the content change somewhere else on the page, or maybe it makes a dialog appear. You can formalize this relationship by using aria-controls on the trigger element so that screen readers are able to relay this intent to the end user.
<button type="button" aria-controls="infoDialog" onclick="openDialog">
Open dialog
</button>
<dialog id="infoDialog">
...
</dialog>
Indicate that an element contains an error message related to an input
Most custom form designs don't rely on the browser's native error handling. Instead, input error messages are rendered somewhere (hopefully near) the input
it's related to. aria-errormessage should be used on the associated input
element so that screen readers can properly relay this information to the user. Typically this should be used alongside aria-invalid.
<label for="nameInput">Name</label>
<input
type="text"
id="nameInput"
aria-invalid="true"
aria-errormessage="nameError"
/>
<p id="nameError">
Sorry! We don't support names with emoji characters 😭
</p>
Suggest an alternative navigation flow through the page
Sometimes content on the page isn't in sequential order. Sometimes there is no real sequential order and multiple sections on the page could be viewed in any order. You can suggest an alternative flow through a page by using aria-flowto on an element to suggest one or more other elements that could be viewed next. Screen readers that support this won't force the flow onto the user, instead, they will suggest an alternative flow to the user and let the user decide. Support for aria-flowto
isn't great as of right now, but it can be worth the minor effort of providing a great user experience.
<h1 aria-flowto="description details reviews">
Super awesome product
</h1>
<section id="description">
<h2>Description</h2>
...
</section>
<section id="details">
<h2>More details</h2>
...
</section>
<section id="Reviews">
<h2>Reviews</h2>
...
</section>
Indicate that an element should be treated as the parent of another
A parent-child relationship is an important part of the DOM hierarchy. Special design considerations and sometimes limited having limited control of the final DOM hierarchy can mean that you can't always render an element within the element where it should be placed. If this is the case and you need to modify the accessibility tree of a page you can override the interpreted hierarchy by setting aria-owns on the parent element to indicate that another element should be viewed as a direct child of that element. This should only be used when you don't have access to modify the DOM structure as it's preferred you just restructure the DOM if possible.
<dl aria-owns="additionalTerm additionalDescription">
<dt>Term</dt>
<dd>Value</dd>
</dl>
<dt id="additionalTerm">Another term</dt>
<dd id="additionalDescription">Another value</dd>
This example is the equivalent accessibly to:
<dl>
<dt>Term</dt>
<dd>Value</dd>
<dt>Another term</dt>
<dd>Another value</dd>
</dl>
Allow users to navigate to a specific place on a page
Anchor links allow you to link to a specific location within a page. This can be done by setting the fragment identifier in the URL to match the id
of the element you want to link to. Since the fragment identifier is exposed to the end user through the URL, it's a good idea to ensure the target element's id
is readable and intuitive. This can be useful in building a table of contents and allowing users to share a specific part of the page.
When not to use the id
attribute
With so many good use cases for the id
attribute, it's a good idea to avoid using it if doesn't solve one of the use cases listed above to avoid confusing other engineers and potentially creating a worse experience for the user.
Avoid using id
as a CSS selector
The id
can also be used as a CSS selector, like so:
#header {
background: rebeccapurple;
}
While this is valid, it can create some issues down the road that should be considered.
High Specificity
It turns out that the "C" for "cascading" in CSS isn't the final decider regarding what rules apply to what elements. Specificity is also taken into account, and as it turns out id
selectors have the highest specificity. This means that any rules declared using an id selector will have a higher priority over rules declared with other selectors, even if those rules are declared later on.
Low reusability
Since an id
must be unique to each page; any style rules applied using an id
selector can only apply to a single element (and its children) at a time. This doesn't promote reusability as those styles are limited to a single use case.
What to use instead
Prefer using class names or data attributes as CSS selectors.
Avoid using id
as a JavaScript selector
It's possible to select an element in JavaScript by referencing the element's id
like so:
document.getElementById('elementId')
While this is valid, it discourages reusability as the JavaScript code is forced to be tightly coupled to the DOM structure and it's not possible to re-use the code on multiple elements at the same time if necessary.
What to use instead
Prefer using unique data attributes as JavaScript selectors instead. This provides more flexibility and improves code readability as the data attribute can have a clear and descriptive name.
document.querySelectorAll('[data-vue-app]')
document.querySelector('[data-vue-app="myAppName"]')
Avoid using id
as a selector in tests
Quality tests should avoid being tightly coupled to the HTML and DOM structure of the page; they should instead focus on testing the experience from the eyes of the user. It's possible for someone to come along, see an id
on an element, and not see any accessibility advantage to having the id
and remove it. This could break tests they didn't know about despite not making a change that affects the end-user experience.
What to use instead
Most testing frameworks have support for some type of "test id" such as data-testid
. Refer to your testing library's best practices, but use a unique data attribute as a selector to improve code readability.