Here’s something you’ll spot in the wild:
Custom Button
This is one of those code smells that makes me stop in my tracks because we know there’s a semantic element that we can use instead. There’s a whole other thing about conflating anchors (e.g., ) and buttons, but that’s not exactly what we’re talking about here, and we have a great guide on it.
A semantic element makes a lot more sense than reaching for a
off the top of my head:- Interactive states
- Focus indicators
- Keyboard support
But I find myself unable to explicitly define those benefits. They’re more like talking points I’ve retained than clear arguments for using over
is a best practice.Let’s compare the two approaches:
Did you know that you can inspect the semantics of these directly in DevTools? I’m ashamed to admit that I didn’t before watching Sara’s course.


There’s clearly a difference between the two “buttons” and it’s more than visual. Notice a few things:
- The
gets exposed as abuttonrole while theis agenericrole. We already knew that.- The
gets an accessible label that’s equal to its content.- The
is focusable and gets a click listener right out of the box.I’m not sure exactly why someone would reach for a
over a. But if I had to wager a guess, it’s probably because stylingis tougher that styling a. You’ve got to reset all those user agent styles which feels like an extra step in the process when acomes with no styling opinions whatsoever, save for it being a block-level element as far as document flow goes.I don’t get that reasoning when all it take to reset a button’s styles is a CSS one-liner:
From here, we can use the exact same class to get the exact same appearance:
What seems like more work is the effort it takes to re-create the same built-in benefits we get from a semantic
specifically for a. Sara’s course has given me the exact language to put words to the code smells:- The div does not have
Tabfocus by default. It is not recognized by the browser as an interactive element, even after giving it a button role. The role does not add behavior, only how it is presented to screen readers. We need to give it atabindex. - But even then, we can’t operate the button on
SpaceorReturn. We need to add that interactive behavior as well, likely using a JavaScript listener for a button press to fire a function. - Did you know that the
SpaceandReturnkeys do different things? Adrian Roselli explains it nicely, and it was a big TIL moment for me. Probably need different listeners to account for both interactions. - And, of course, we need to account for a
disabledstate. All it takes is a single HTML attribute on a, but aprobably needs yet another function that looks for some sort of data-attribute and then setsdisabledon it.Oh, but hey, we can slap
on there, right? It’s super tempting to go there, but all that does is expose theas a button to assistive technology. It’s announced as a button, but does nothing to recreate the interactions needed for the complete user experience adoes. And no amount of styling will fix those semantics, either. We can make alook like a button, but it’s not one despite its appearances.Anyway, that’s all I wanted to share. Using semantic elements where possible is one of those “best practice” statements we pick up along the way. I teach it to my students, but am guilty of relying on the high-level “it helps accessibility” reasoning that is just as generic as a
. Now I have specific talking points for explaining why that’s the case, as well as a “new-to-me” weapon in my DevTools arsenal to inspect and confirm those points.Thanks, Sara! This is merely the tip of the iceberg as far as what I’m learning (and will continue to learn) from the course.
<![CDATA[
/* */
]]><![CDATA[
/* */
]]><![CDATA[
/* */
]]><![CDATA[
/* */
]]> {
const consent = truste.cma.callApi("getGDPRConsentDecision","css-tricks.com");// Google Analytics and Segment are functional
if (consent.consentDecision.includes(2) || (consent.source === 'implied' && truste.eu.bindMap.behaviorManager !== 'eu')) {
// Google Analytics
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-68528-29', 'auto');if (typeof articleYear !== "undefined") {
ga('set', 'dimension1', articleYear);
}
if (typeof articleAuthor !== "undefined") {
ga('set', 'dimension2', articleAuthor);
}
if (typeof articleType !== "undefined") {
ga('set', 'dimension3', articleType);
}ga('send', 'pageview');
// Segment Analytics
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e
- The
