---
title: Lost, forgotten, and unfamiliar HTML
teaser: Automated scans taught me about some web stuff I forgot or never even know.
tags: web,accessibility
author: Dave Iverson
published_on: 2026-05-27
---

I ran [HTML-validate ](https://html-validate.org/)and [Axe core](https://github.com/dequelabs/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](https://html.spec.whatwg.org/multipage/dom.html#custom-data-attribute)? I didn’t.

HTTP headers are also case-insensitive [except in HTTP2](https://datatracker.ietf.org/doc/html/rfc9113#section-8.2.1) where they MUST be lowercase.

## Invalid `id` attributes

I learned that in HTML5, [an `id` can be _anything_ ](https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute)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 `,id`s need to start with a letter](https://www.w3.org/TR/html4/types.html#type-id) 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? `id`s 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!

[W3c explicitly recommends it](https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/search.html):

> If a page includes more than one search landmark, each should have a unique label.

```
<search aria-label="Site-wide">
  <form>
    ...
  </form>
</search>

```

[I filed a bug report.](https://gitlab.com/html-validate/html-validate/-/work_items/359)

## 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-label`s, 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](https://dequeuniversity.com/rules/axe/4.11/scrollable-region-focusable?application=playwright) 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](https://www.w3.org/TR/graphics-aria-1.0/#role_definitions) 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](https://www.w3.org/WAI/WCAG22/Techniques/html/H90#description). 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](https://github.com/ddnexus/pagy) 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](https://www.w3.org/WAI/WCAG22/Techniques/html/H63) 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](https://github.com/thoughtbot/capybara_accessibility_audit) which uses Axe under the hood!
