From Nicholas C. Zakas’ newsletter:
The QA manager for my team at Yahoo stopped by my cube one day. “We need you to stop changing CSS classes in the HTML,” she said. I found this request odd. After all, we use CSS classes to apply styles to different HTML elements. I couldn’t imagine why QA would care about that. “Well,” I replied, “that’s not realistic for us. We are in rapid development of the site and need to be able to change both HTML and CSS as we go along to do that. What is the concern?”
As it turned out, the QA team had created a bunch of automated tests using Selenium. Those tests were looking for specific CSS classes in order to click on different areas of the page and test that things were changing appropriately. Naturally, when those CSS classes were changed or removed, it broke the tests.
Their fix, at the time, was to use prefixed class names like qa-*
. That’s a perfectly good solution, but I’ve taken a slightly different approach for similar reasons: data attributes.
End-to-end tests are one example where fragile classes are a pain in the ass. Another for us is an app we use at CodePen called Appcues which, among other things, displays popout messages that are positioned next to specific DOM elements. Appcues needs CSS selectors to do this. Using class names is better than having to :nth-child
or the like to find the right element, but they are still fragile as hell. A broken selector means the entire “flow”, as Appcues calls them, is broken.
I’ve preferred the extra clarity of a data-*
attributes in these situations.
<nav data-appcues="user-menu" class="user-menu">
<img src="avatar.jpg" alt="User Avatar for User" />
<ul>
<li>
<a href="/dashboard">Dashboard</a>
</li>
<li>
<a href="/logout" data-testing="log-out">Log Out</a>
</li>
</ul>
</nav>
Code language: HTML, XML (xml)
Above, Appcues might need to target a User Menu, but rather than use .user-menu
as a selector, which signals to developers it’s used for CSS styling, I use [data-appcues="user-menu"]
so that nobody, including me, will mess with it. Lower, a Cypress test, for instance, could locate and click the [data-testing="log-out"]
link without issue.
Nobody is going to accidentally change attributes like that, as they clearly have some purpose. Changes to them will also be quite clear in a code review.
I wonder if it’s too far to consider using data-*
attributes for any non-styling reason. If JavaScript needs to discover this element, use an data-js
attribute.
Just don’t make up attributes, that always makes me cringe.
I’ve routinely used data attributes as selectors for JS. We’ve still got a bunch of applications using jQuery. If I’m making a discrete interface element then I’ll give it a data-ui attribute and give elements and controls within that block other data attributes that the scoped jQuery method will use for applying handlers. Much less fragile than using classes as you say.
What about using IDs for this? IDs shouldn’t change anyway, as they are part of the public API (can be referenced by URL), and they could contain a semantic description of the element, that can be used for all kinds of purposes (including testing). I just feel like creating an artificial API to test against feels kind of wrong…