cantclickthis.dev Screen reader labs Cheat sheet (PDF)

You can't click this

Six ways websites lock people out, each one live and broken on this page — then fixed, in the same breath, with the markup that did it.

Read the transcript

Intro

[record scratch]
Yeah… Check the DOM.
Check the tree.
You can't tab through that.

Chorus

You can't click this.
[record scratch]
You can't click this.
You you you you you you you.
You can't click this.
Yah.
Sema, sema, se-man
You can't click this.
Stop!
Semantic time!

Verse

My, my, my, my markup hits so bad,
Makes the screen reader user straight-up mad.
Thank you for blessing me
With a clickable div that I just can't see!
No role, no focus state,
Hit the Tab key once, now I gotta wait.
Buttons look real, but they have no tags,
Accessibility backlog drags.

Chorus

Can't tab this.
Look, man, you can't click this.
No aria-label, can't click this.
Stop! Semantic time!

Outro

Just use a button.
CantClickThis.dev.
Stop! Semantic time!

Greg Miller, Shrinkray Interactive Track: “Can't Click This” by toledogeeks

01 of 06

The clickable div

It looks like a button. It is styled like a button. The mouse agrees. Nothing else does.

  • WCAG 4.1.2 Name, Role, Value
  • WCAG 2.1.1 Keyboard
Open in lab: The clickable div

example.com

Put focus in the demo and press Tab. In the broken state there is nothing to land on.

What breaks it

 (flagged, see note 3) <div class="btn" onclick="addToCart()">
  Add to cart
</div>
  1. No role, so a screen reader announces this as plain text — or skips it entirely.
  2. Not focusable: a div has no tabindex, so Tab walks straight past it.
  3. A click handler alone ignores Enter and Space, which is how buttons are operated without a mouse.

What fixes it

 (flagged, see note 2) <button type="button" class="btn">
  Add to cart
</button>
  1. Role, focusability, Enter and Space handling, and the disabled state all arrive free with the element.
  2. type="button" stops it submitting a surrounding form by accident.

If it looks like a button, it has to be a button. Screen readers and keyboards only know what the code says, never what it looks like.

02 of 06

Alt text that says nothing

Every image gets an alt attribute. What goes inside it depends on whether the image carries information or just decorates.

  • WCAG 1.1.1 Non-text Content
Open in lab: Alt text that says nothing

example.com

Tab through the example. The line under the image is the alt a screen reader would read — a filename, then decoration.

What breaks it

 (flagged, see note 1) <img src="hero.png" alt="IMG_2847_final_v3.png">
 
 (flagged, see note 2) <img src="swirl.svg"
     alt="decorative swirl divider graphic image">
  1. A filename describes your export settings, not the picture. It tells the listener nothing.
  2. Decoration announced out loud is clutter. "Image" is also redundant — the screen reader already said "graphic".

What fixes it

<img src="hero.png"
 (flagged, see note 1)      alt="Blue high-top sneaker with a white sole, side view">
 
 (flagged, see note 2) <img src="swirl.svg" alt="">
  1. Describes what a sighted person gets from the image, in the length of a caption.
  2. An empty alt is a decision, not an omission: it tells assistive tech to skip this one on purpose.

If you deleted the image, what sentence would you write in its place? That sentence is your alt text. If you would write nothing, use alt="".

03 of 06

Levels of heading

Nearly 30 million headings on a million home pages — 29.9 each, up 20.4% in a year. More headings only help if the outline is true.

  • WCAG 1.3.1 Info and Relationships
  • WCAG 2.4.6 Headings and Labels
Open in lab: Levels of heading

example.com

Tab through the example. The fake title is skipped — it is a paragraph. Then the outline jumps 4, 6, 3. A screen reader headings list (VoiceOver rotor, NVDA Insert+F7) shows the same gaps.

What breaks it

 (flagged, see note 1) <p class="hero">DevFest 2026</p>
 
 (flagged, see note 2) <h4>Schedule</h4>
 
 (flagged, see note 3) <h6>Venue</h6>
 
<h3>Get tickets</h3>
  1. Looks like the page title. The headings list never sees it — it is a paragraph.
  2. h4 because it looked medium-small. Nothing sits above it, so the outline skips two levels.
  3. h6 as a size token. Almost every home page that uses one also skips levels — this is type size, not a sixth level of structure.

What fixes it

 (flagged, see note 1) <h2>DevFest 2026</h2>
 (flagged, see note 2) <h3>Schedule</h3>
<h3>Venue</h3>
<h3>Get tickets</h3>
  1. A real heading. Screen reader users jump here first, the way sighted users look at the big type.
  2. Siblings share a level. Do not pick h4 or h6 because they look smaller — that is CSS, not structure.

Headings are the map, not the type scale. If it looks like a heading, it has to be a heading, at the next level down — never the one that happens to look the right size.

04 of 06

The invisible cursor

Someone removed the focus outline because it looked ugly on one button. Now nobody navigating by keyboard can tell where they are.

  • WCAG 2.4.7 Focus Visible
  • WCAG 2.4.11 Focus Not Obscured
Open in lab: The invisible cursor

example.com

Tab through this row in both states. Same tab order, same elements — only one of them tells you anything.

What breaks it

a,
button {
 (flagged, see note 2)   outline: none;
}
  1. Nothing replaces it, so keyboard focus becomes invisible across the entire page.
  2. This is usually copied in to hide the ring on mouse click — :focus-visible already does that for you.

What fixes it

 (flagged, see note 1) a:focus-visible,
button:focus-visible {
  outline: 3px solid #ffffff;
  outline-offset: 2px;
 (flagged, see note 2)   box-shadow: 0 0 0 6px #0e1420;
}
  1. :focus-visible fires for keyboard focus and stays quiet for mouse clicks — the behaviour people actually wanted.
  2. A second ring in the opposite tone keeps the indicator visible on light and dark backgrounds alike.

Never remove a focus outline without replacing it with something at least as visible. If you cannot see where you are, neither can the person using only a keyboard.

06 of 06

A form with no labels

Placeholder text looks like a label until you start typing and it vanishes — taking the only instruction with it.

  • WCAG 1.3.1 Info and Relationships
  • WCAG 3.3.2 Labels or Instructions
Open in lab: A form with no labels

example.com

Click or Tab into a field. The placeholder vanishes, and nothing is left to say what the field is for. A screen reader announces "edit text" with no name.

What breaks it

 (flagged, see note 1) <input type="text" placeholder="Full name">
 
 (flagged, see note 2) <input type="text" placeholder="Email">
  1. Placeholder is the only instruction, and it disappears the moment you click or Tab in — which is when you need it.
  2. Nothing is associated with the field, so a screen reader announces "edit text" with no name.

What fixes it

 (flagged, see note 1) <label for="email">Email</label>
<input type="email" id="email" name="email"
 (flagged, see note 2)        autocomplete="email"
 (flagged, see note 3)        aria-describedby="email-hint" required>
 
<p id="email-hint">We send one confirmation
   and nothing else.</p>
  1. for and id pair them up, so the label is announced with the field and clicking the label focuses it.
  2. autocomplete lets the browser fill it in — a genuine accessibility win under 1.3.5, not just convenience.
  3. required is announced as a state, and aria-describedby attaches the hint without crowding the label.

Every input needs a real label a screen reader can announce. Placeholder text is a hint, not a label, and it disappears the moment it is needed.