iJS New York – October 2026

Accessibility in the Age of AI

The robots are also blind.

I sent an AI to buy coffee.

It couldn't find the button.

What it saw

Task: "Add an Oat Latte."

Nick Daniel, smiling, in a navy shirt

Hi, I'm Nick –

- img "Nick Daniel"
- heading "Nick Daniel" [level=1]
- text: Richmond, VA
- link "endash.us": the space between
- text: long-time a11y talker and tinkerer
- button "Err while live-coding" [pressed]

Why this talk: the machines started using our UIs. Tree or screenshot, they stumble in the same places screen reader users do.

Accessible design has always been good design.

Five habits. Good for people. Now good for robots too.

Part 1StructureLandmarks, headings, a title.
Part 2ControlsReal elements, real names.
Part 3InteractionsState lives in the tree.
Part 4ErrorsWhich field. What. How.
Part 5RecoverySay what changed. Offer a way back.

Who's reading your UI now

One tree, five readers Your DOM becomes the accessibility tree. Screen readers, voice control, test runners, browser agents, and AI assistants used as sight all read that same tree. Your DOM HTML you already ship Accessibility tree role · name · state built by the browser, for free Screen readersVoiceOver · NVDA · TalkBack Voice control"click Add Oat Latte" Test runnersgetByRole · aria snapshots Browser agentsPlaywright MCP, assistants AI as sighta model reading a screen aloud

The tree in 40 seconds

Role

What is it? button, heading, dialog, textbox. From the element, or from role=.

Name

What is it called? From its text, its <label>, or an ARIA attribute. Placeholder is not a name.

State

What's going on? disabled, expanded, invalid, current, checked. Attributes, not classes.

<button aria-expanded="false" aria-controls="filters">Filters</button>
<!-- becomes, for every reader on the previous slide: -->
button "Filters" [expanded=false]
<!-- and relationships: describedby, labelledby, heading levels -->

Where we still are

94.8% of the top million home pages had detectable WCAG failures. Average: 51 per page. The top six are all tree bugs.

Source: WebAIM Million 2025, webaim.org/projects/million. Share of home pages with at least one instance.

Part 1 – of 5

Clear structure

Landmarks, headings, a title, a language. The map every reader builds before touching anything.

Orient without looking

Task: "What page is this, and where's the menu?"

Structure is navigation

Div soup

<html>
<title>app</title>
<div class="site-header">…</div>
<div class="hero-title">Fuel for…</div>
<div class="h2">Espresso drinks</div>
<div class="cart-panel">…</div>

Same CSS, real structure

<html lang="en">
<title>Menu – n–brew</title>
<header><nav aria-label="Site">…</nav>
<main>
  <h1>Fuel for…</h1>
  <h2>Espresso drinks</h2>…
<aside aria-labelledby="cart-h">…

The rotor, your first Playwright locator, and the agent's first read are all the same outline. Ship one.

Part 2 – of 5

Meaningful controls

A real element with a real name. If it isn't in the tree, it doesn't exist.

Name that control

Task: "Add an Oat Latte."

Four plus buttons. Click one.

+
<div class="add-btn" onclick="add()">+</div>
<button>+</button>
<button aria-label="Add">+</button>
<button><span aria-hidden>+</span><span class="sr-only">Add Oat Latte to cart, $5.00</span></button>
Click or Tab to a button. This panel shows the node the browser builds for it.

Name everything interactive. In this order.

1 · Visible text

<button>
  Add Oat Latte
</button>
  • One string for eyes, ears, voice control and agents
  • Translates. Never drifts from the design

2 · Hidden text

<button>
  <span aria-hidden>+</span>
  <span class="sr-only">
    Add Oat Latte
  </span>
</button>
  • For icon-only controls
  • Real DOM text: translates, testable, in the name

3 · aria-label

<button
  aria-label=
    "Add Oat Latte">
  +
</button>
  • Last resort. Invisible, often untranslated
  • Overrides content; rots quietly

Never placeholder, never title alone, never a <div onclick>.

Part 3 – of 5

Predictable interactions

State lives in the tree. Focus goes where the action is. Escape means escape.

Open checkout. Then get out.

Task: "Open checkout, then close it."

Put state in the tree

Looks right

<button class="btn disabled-look">
  Checkout
</button>
<div class="tab active">Menu</div>
<div class="overlay">
  <div class="modal-card">…</div>
</div>

Is right

<button class="btn" disabled>
  Checkout
</button>
<button aria-current="page">
  Menu
</button>
<dialog aria-labelledby="order-h">
  <h2 id="order-h">Your order</h2>…
</dialog>
// dialog.showModal();

A class is a rumor. An attribute is a fact. <dialog> does the rest for free.

Part 4 – of 5

Useful errors

Which field. What's wrong. How to fix it. Attached to the field, not floating near it.

An error the robot can read

Task: "Place the order." The email is already wrong.

An error answers three questions

Color and vibes

<input class="err" placeholder="Email">
…
showToast("Something went wrong")

Which · what · how

<label for="em">Email</label>
<input id="em" type="email"
       aria-invalid="true"
       aria-describedby="em-err">
<p id="em-err">Enter an email with @,
   like nick@endash.us</p>

Then move focus to the first invalid field, and put a summary in a role="alert". Same words on screen and in the tree.

Part 5 – of 5

Paths to recovery

Say what changed. Offer a way back.

Did that work?

Task: "Remove the croissant. Then put it back."

Announce the change. Forgive the click.

One polite announcer

<p id="status" role="status"></p>

status.textContent =
  "Croissant removed.";
status.append(undo); // a way back

After the big one

<h2 id="done" tabindex="-1">
  Order placed</h2>
<p>Ready in 5 minutes.</p>
<button>Back to menu</button>

done.focus(); // then hand over

Agents wait for an announcement the same way screen reader users do. Silence reads as failure to both.

The whole order, without looking

Task: "Order an Oat Latte and a Croissant. Receipt to nick@endash.us."

Same commit. Two users.

Three, if you count your test suite.

And then –

Make the robots work for you

The same machine that stalls on your UI is the cheapest accessibility engineer you'll ever hire. Give it three things.

1 · Give your agent the rules

# .claude/skills/a11y-first/SKILL.md    (or AGENTS.md, CLAUDE.md, .cursorrules)
1. Use the element that means it: button, a[href], label, dialog, h1–h6, nav.
2. Name every control with visible text. Icon only? Hidden text. aria-label last.
3. State is an attribute, never a class: disabled, aria-current, aria-invalid.
4. Errors: aria-invalid + aria-describedby, focus the first bad field, role=alert.
5. Off-screen changes go through one role="status" region. Big moves move focus.
Before you say "done": run `npm test -- a11y` and paste the aria snapshot.

One file in the repo. Every agent that touches your UI reads it first. The five habits, as a lint rule the robot applies to itself.

2 · Ask it the screen reader question

“Read this screen the way a screen reader would. List every control by role and name. Flag anything unnamed, any state that lives only in a class, and any change that isn't announced. Then give me the smallest diff.”

Paste it with a component, a PR, or a staging URL. Claude Code, Cursor, Copilot, a chat window. It's the agent log from the demos, on demand.

3 · Let the tests do the checking

test.beforeEach(({ page }) => page.goto("/"));
test("the menu, as the robot reads it", async ({ page }) => {
  await expect(page.getByRole("main")).toMatchAriaSnapshot(`
    - heading "Fuel for the space between" [level=1]
    - list:
      - listitem:
        - button /Add Oat Latte to cart/
  `);
});
test("no axe violations", async ({ page }) => {
  expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]);
});

Snapshot for names and structure, @axe-core/playwright for the WebAIM six, a Tab walk for reach. Green means a person and a robot can both use it.

4 · Point an agent at staging

agent task: check out
found: button "Checkout" → clicking
something appeared, but the tree shows no dialog. Focus never moved.
trying Escape… still there. Esc means nothing here.
✗ task technically done. I never knew I was in a modal.
  A keyboard user would have tabbed into the page behind it.

Playwright MCP or any browser agent, plus the prompt, on a Friday. Every stall is a bug report written in plain English, filed by your cheapest tester, and the fix is a commit that also helps a person.

This week

Build it for the person.
The robot comes free.

Before you go – three quick asks

Say hi

Questions after the session, or tell me what you're making accessible.

nick@endash.us

Find me

Writing and tools, including everything from this talk.

endash.us

Take it with you

This deck, the café, the skill file and the tests.

endash.us/toolkit

Thanks, iJS. Now let's talk. · nick@endash.us

We ask for your feedback. Please vote now in the devmio app.
00:00 1 / 1

Keyboard shortcuts

→ Space PgDnNext slide
← PgUpPrevious slide
Home / EndFirst / last slide
RRun the agent on a demo slide
MCycle the demo mode (soup → real elements → fixed)
NSpeaker notes
EscOverview grid
FFullscreen
TReset timer
PPrint view (PDF export)

Slide changes are announced through a polite live region and focus follows the deck. The café inside the demos is a real app: Tab into it.

Speaker notes