I ran HTML-validate and Axe core and a Claude prompt against a new website I’m building, and they caught a bunch of stuff I missed! This gave me a chance to remember the easily overlooked bits of building a website. And I visited a few dark corners of the HTML spec I hadn’t been to yet!
Data attributes should be lowercase
data-dialogOpen is invalid - it should be data-dialogopen.
But did you know that all HTML attribute names get automatically lowercased? I didn’t.
HTTP headers are also case-insensitive except in HTTP2 where they MUST be lowercase.
Invalid id attributes
I learned that in HTML5, an id can be anything as long as it’s 1 character with no whitespace (and it’s unique). id="_0$!11" is totally valid and I think even emojis are ok!.
However, in HTML4 ,ids need to start with a letter and can only contain letters, numbers, and a few punctuation symbols. So it’s probably best not to go too wild. Backwards compatibility is nice.
Oh, and the uniqueness requirement? ids inside iFrames only need to be unique within their document. Otherwise, imagine how tricky it would be to iFrame in an arbitrary page.
Redundant for attributes
A bit of a nitpick: when you label an input by putting it inside a label, the for attribute is redundant. When the input is outside the label, you definitely need that for!
<!-- Rails-style: no `for=""` needed -->
<label>
Username <input type="text" name="username" />
</label>
<!-- non-Rails-style: don't forget the `for=""`! -->
<label for="username>Username</label>
<input type="text" name="username" id="username" />
Some reasons that thoughtbot prefers inputs inside labels:
- it reduces the need for an extra wrapper div
- since the label is clickable, this often results in a bigger click/tap area
- you don’t need to generate unique IDs for inputs
Extra whitespace in a textarea
Claude spotted this one: I accidentally had a blank space inside a textarea.
<textarea name="explain"> </textarea>
An easy mistake to make and kind of annoying to an end user, especially because it will cause the required validation to be skipped. I wish one of my automated scanners had caught it.
False positive: aria-label misuse
HTML-validate told me that using the aria-label attribute on <search> is invalid. Nope - I was using it correctly!
If a page includes more than one search landmark, each should have a unique label.
<search aria-label="Site-wide">
<form>
...
</form>
</search>
iFrames with unique names
I had trouble with this one, but I’m glad Axe caught it because it’s genuinely useful for screen reader users.
Every iFrame needs a title, and those titles should be unique so they can be differentiated. But also, landmarks INSIDE the iFrames must be unique across the entire page, including the parent document.
I had 3 iFrames on a page, all with <main aria-label="Component Example">. Sure enough, when I opened Voiceover it read out 3 of the same landmark:
- Component Example main
- Component Example main
- Component Example main
That’s not a great experience.
First, I tried to fix it by removing the aria-labels, but Axe warns me that the document has multiple <main>s without unique labels. I had to refactor how the iFrames were generated so that each one had both a unique title and <main> label.
Color contrast issues
Automated scanners are the best at finding contrast issues. I happened to have a link state that used a slightly-too-light purple on white. It didn’t pass WCAG’s minimum contrast levels. Easy for me to miss, but troublesome for someone with reduced vision.
Keyboard-accessible overflow scrolling
This was a new one for me! Axe tells me that when a region scrolls using overflow: scroll or similar, it must contain a focusable element. This seems to be a Safari-specific bug.
I tested with Safari and confirmed that it’s true: using the keyboard I was unable to scroll down to see the cut-off content.
The simplest solution is to add tabindex="0" to an element inside the scrolling region.
Forgotten SVGs
I’m constantly forgetting to check that SVGs have the right label and role. With images it’s easy: just make sure you’ve got an alt tag. But inline SVGs can either be decorative or presentational.
Decorative SVGs must use aria-hidden="true" to keep them out of the accessibility tree.
Presentational ones must use role="image and NEED a <title> tag to serve the same function as alt text. And since not all screen readers catch the <title> tag, you usually want to associate it with the <svg> tag using aria-labelledby. And if the SVG contains multiple images, text blocks, or interactivity, there’s even more to consider.
I dug into the WAI-ARIA rabbit hole and learned that maybe some of my SVGs could be role="graphics-symbol"
A graphical object used to convey a simple meaning or category, where the meaning is more important than the particular visual appearance.
Axe missed all this, but Claude caught it. I wonder if there’s an automated scanner that could help me out.
Explain your asterisks
If you’re going to denote require inputs using an asterisk * in the label, you’d better provide a legend that explains it. Even better, replace asterisk with (required).
Oops, thanks for the reminder, Claude. I added an explainer to the form:
<small>* asterisks denote required fields</small>
Punctuation as labels
I built a pagination component that looked like this:
< 1 … 45 46 47 … 104 >
Claude reminded me that when a screen reader reads out those angle brackets and ellipses, it’s going to sound weird. I opened Voiceover and sure enough - it sounds weird.
I followed Pagy’s example: the ellipses get role="separator" and the buttons get aria-label="Next"/aria-label="Previous".
Table header cell scopes
A blind spot for me: I didn’t know about the scope attribute. WCAG recommends using scope="col" on table header <th> cells to associate them with their column. And also using <th scope="row"> for table body cells that identify the subject of the row.
Probably more useful for complex tables than simple ones. I’ll have to remember this.
Thank goodness for automated scanners and the people who maintain them[^2]! The stuff I build is better for it. I was impressed by the bugs Claude caught, even though it surely wasn’t comparable to an accessibility audit by a real person.
[^1] My prompt: “You are an accessibility expert. Please review all the pages on this site and create a table of accessibility and WCAG violations”
[^2] By the way: thoughtbot maintains CapybaraAccessibilityAudit which uses Axe under the hood!