You may not need HTML ID's

Written by Rico Sta. Cruz
(@rstacruz) · 24 Nov 2023 (updated) · First published Dec 2021

I often see ID attributes in HTML for styling elements, attaching JavaScript behaviours, and even using it in tests. Over time, I’ve found that ID’s may not be the best choice for any of these. I learned a few things.

In this article:
  1. ID’s are not unique
  2. ID’s pollute global scope
  3. ID’s break CSS specificity
  4. ID’s can cause confusion
  5. Alternatives
  6. When to use ID’s
  7. Conclusion

ID’s are not unique

It’s possible to have more than one of the same ID in a page. Try this: when querySelectorAll is used with an ID selector, will it return one result, or multiple results?

example.html
<div id="my-id-selector">One</div>
<div id="my-id-selector">Two</div>
<div id="my-id-selector">Three</div>
<div id="my-id-selector">Four</div>
document.querySelectorAll('#my-id-selector')
// Will this have one result, or multiple results?
1 collapsed line
◀ [Node, Node, Node, Node]
Expectation: ids are unique. Reality: multiple elements can have the same ID.

ID’s pollute global scope

If an element has an ID, that ID will be a global variable stored in window.

Firefox dev inspector
The DOM element is available in the global scope as window.drawer.
example.html
<form id="submitform">...</form>
example.js
submitform.submit()
console.log(window.submitform)
Note how submitform is automatically available in JavaScript.

ID’s break CSS specificity

ID’s can cause confusion in CSS specificity. When ID’s are used for styling, it will have a side effect of increasing the specificity of the CSS rules. Let’s start with a simple HTML document with ID’s and classes:

Problem with selecting text
The color: white rule doesn't seem to work in this example. Try this in the Codepen demo.
example.css
#register-page a {
color: green; /* this... */
}
.register-actions a {
color: white; /* ...takes precedence over this */
}
example.html
<div id="register-page">
<h1>Register now</h1>
<p>If you already have an account, ...</p>
<div class="register-actions">
<a href="#">Forgot your password?</a>
</div>
</div>
The first selector (#register-page a) takes precedence over the second selector (.register-actions a) because it uses an ID. As a result, the color doesn't turn to white.
👋
Hey! I write articles about web development and productivity. If you'd like to support me, subscribe to the email list so you don't miss out on updates.

ID’s can cause confusion

ID’s in JavaScript can be deceiving. Behaviour is sometimes attached to ID selectors that pick out a specific element. There are some caveats in this approach:

  • Overloaded semantics. Some projects use ID selectors (eg, #submit-form) to signify style (CSS), behaviour (JavaScript) and semantics (Capybara tests). It’s overloaded, and it’d be hard to disconnect one from the other, such as if you want the same behaviour without the styling.

  • Difficult to test. It can be a bit awkward to test, possibly needing a mock #submit-form element for it.

example.js
let form = document.getElementById('submit-form')
form.addEventListener('click', () => {
let buttons = form.querySelectorAll('[type=submit]')
;[...buttons].forEach((b) => (b.disabled = true))
})
Is the #submit-form ID used for styling, adding behaviours, or test targets? Often: maybe all of the above.

Alternatives

For behaviours: data attributes

If ID’s are used in this way, consider attaching JavaScript behaviours to a data attribute. In the example above, there’s no easy way to attach this same generic behaviour to multiple forms. Instead of querying by ID, we can refactor the above to target a data attribute.

example.html
<body>
.
.
<form data-disable-on-submit>...</form>
.
<body>
The data-disable-on-submit attribute can be added to any form and for it to pick up the behaviour.

For tests: query by role

There are other ways to refer to elements in tests rather than ID’s. One common convention in JavaScript testing is to prefer to choose tests in this order:

For targeting elements in tests, consider querying using:

  1. The [role] attribute
  2. The label text (for form fields)
  3. The placeholder text (for form fields)
  4. The text
  5. The current value (for form elements)
  6. The [alt] text (for images)
  7. The [title] text
  8. The [data-testid] attribute
See: Order of query priorities (testing-library.com), Making your UI tests resilient to change (kentcdodds.com)

When to use ID’s

Despite all these, ID’s are still useful in a number of cases.

  • For anchors. ID’s can be used as text anchors for permalinks.
  • For and aria-labelledby. It’s necessary for attributes like for and aria-labelledby.
example.html
<h1 id="introduction">Introduction</h1>
.
.
<a href="#introduction">Go to introduction</a>
An ID, introduction, lets the link scroll to where the content is.
example.html
<input type="checkbox" id="accept-terms" />
<label for="accept-terms">Accept terms</label>
<button aria-labelledby="my-label">
<span id="my-label">Save</span>
</button>
The attributes for and aria-labelledby uses ID's to refer to another element.

Conclusion

Using ID attributes in HTML for styling elements, attaching JavaScript behaviors, and testing may not be the best choice. When it seems like using an ID is the best solution, it may be good to consider using alternative methods.

I found these to be helpful in untangling tests and JavaScript behaviours. Let me know how it work out for you in the comments below!

Written by Rico Sta. Cruz

I am a web developer helping make the world a better place through JavaScript, Ruby, and UI design. I write articles like these often. If you'd like to stay in touch, subscribe to my list.

Comments

More articles

← More articles