AI Writes Divs by Default: Reviewing Generated Markup for Accessibility
Generated UI code looks polished and renders beautifully — and its accessibility tree is often empty. Here are the four smells I hunt for when reviewing AI-assisted frontend PRs.
Last month I reviewed a settings page that the author cheerfully described as "mostly generated, I just wired up the data." And credit where due: it looked terrific. Responsive, dark-mode aware, consistent spacing, nice hover states. Then I opened the browser's accessibility inspector to see what the page looked like to assistive technology, and the answer was: almost nothing. Where I saw a navigation sidebar, a settings form, and a row of action buttons, the accessibility tree saw generic, generic, generic, image, generic. A whole interface, visually complete and semantically vacant.
This is the defining texture of AI-generated frontend code right now, and I want to talk about it without the hand-wringing. The tools are genuinely useful — I use them daily. But they've moved the accessibility work from the keyboard to the review, and if you're the human approving generated UI, there's a specific, learnable set of things to look for.
Why models write divs
It helps to understand that this isn't a bug in any particular tool — it's a mirror. Code models learn from the web, and the web's median markup is div soup. For every carefully semantic <nav> in the training data, there are a hundred <div className="nav">. The model reproduces the statistical center of what it's seen.
There's a deeper reason, too: divs never visibly fail. A <div> with a click handler renders and functions identically to a <button> for a sighted mouse user — which is to say, for the person evaluating the output. Semantics fail silently, in a tree the author never opens, for users who aren't in the demo. A model optimizing for "looks right" has no gradient pushing it toward markup that merely is right.
So the review stance I'd suggest is this: treat generated UI like a fast, talented junior's first draft. Genuinely good, confidently delivered, and reliably weak in exactly four places.
Smell #1: The fake button
The most common and most damaging: interactive divs. Close icons, card actions, menu items — anything the model decided to make clickable without reaching for <button>.
// Generated: works for a mouse, invisible to everyone else
<div className="icon-btn" onClick={onClose}>
<XIcon />
</div>
// Reviewed: focusable, keyboard-operable, and it has a name
<button type="button" className="icon-btn" onClick={onClose} aria-label="Close">
<XIcon aria-hidden="true" />
</button>
The mechanical check is a text search of the diff for onClick (or @click, or on:click) on non-interactive elements — divs, spans, lis, SVGs. Every hit is either a bug or needs a one-line justification. The experiential check is even faster: pull the branch, put your mouse down, and Tab. Fake buttons are the things focus skips straight over.
Smell #2: Div soup where structure should be
Screen reader users don't read pages top to bottom — they navigate by landmarks (main, navigation, banner) and headings, jumping around a page's structure the way you visually scan it. Generated markup routinely ships none of that structure: the nav is a styled div, the page title is a <span className="text-2xl font-bold">, the list of items is a stack of divs, and heading levels — when they exist — jump from h1 to h4 because the model was matching font sizes, not building an outline.
None of this shows up in a screenshot. All of it shows up if you ask, "could someone skim this page without eyes?" In review I look for: one <h1>, headings that descend without gaps, <nav>/<main>/<header> around the obvious regions, and <ul>/<li> wherever the content is a list of like things. These are cheap comments to write and cheap fixes to make — the structure is already visually there; it just needs to be stated in the markup.
Smell #3: Confetti ARIA
Here's the counterintuitive one. Models don't just under-use semantics — they over-sprinkle ARIA, because attributes like role and aria-label appear near accessibility-flavored code in the training data. The result is decoration that reads as diligence:
<!-- Generated: ARIA as garnish -->
<div role="main" aria-label="Main content">
<div role="heading">Dashboard</div>
<span tabindex="0" aria-hidden="true">Settings</span>
</div>
Each line is a small disaster. role="heading" without aria-level is an incomplete promise — and <h2> was right there. And that last span is the nastiest pattern in the pile: aria-hidden="true" on a focusable element means keyboard users land on something screen readers are told doesn't exist — focus arrives, and the screen reader goes silent.
The rule of thumb I share with reviewers: wrong ARIA is worse than no ARIA. An unadorned div is merely unhelpful; a div with a false role actively lies to assistive technology about what it is and how it behaves. When you see ARIA in a generated diff, don't skim it approvingly — interrogate it. What native element was avoided here? Is the behavioral contract behind this role (keyboard handling, states, focus) actually implemented? Nine times out of ten, the right review comment is "delete the ARIA and use the real element."
Smell #4: The unlabeled icon
Icon-only buttons are everywhere in modern UI, and generated code almost never names them. A trash-can button with no accessible name is announced as, simply, "button." A toolbar of five unlabeled icon buttons is "button, button, button, button, button" — a slot machine where one of the levers deletes your project.
The fix is one attribute — aria-label="Delete project" on the button (or visually hidden text inside it) — plus aria-hidden="true" on the SVG so the icon itself stays out of the announcement. While you're at it, glance at generated <img> tags: models emit empty alt="" and wallpaper filler like alt="image" in roughly equal measure, and neither is a considered choice. Meaningful images get a real description; decorative ones get the deliberate empty string.
How I actually review generated UI
Process-wise, three habits cover most of it:
- Run it, don't just read it. Tab through the feature; open the accessibility tree on the main view. Two minutes of runtime beats twenty minutes of squinting at JSX, because these failures are failures of the rendered tree.
- Push the fix upstream. If a teammate's assistant keeps producing div soup, the highest-leverage review comment isn't the tenth "make this a button" — it's "add 'use semantic HTML and native interactive elements' to your prompt." Models respond well to being asked; they just don't volunteer.
- Keep the standard, drop the blame. The author didn't hand-type the fake button, and the point of review isn't archaeology about who wrote what. It's that your team's name is on the merge commit. Generated or not, the bar is the bar — and the tone can stay kind while the bar stays put.
A reviewer's checklist for generated UI
- Search the diff for click handlers on divs, spans, and SVGs. Each one becomes a
<button>or an<a>. - Check the outline: one
h1, no skipped heading levels, landmarks around nav and main content, real lists for lists. - Interrogate every
roleandaria-*: is it necessary, complete, and true? Prefer deleting ARIA in favor of native elements. - Every icon-only control gets an accessible name; every decorative icon gets
aria-hidden="true". - Tab through it once before you approve. The machine optimized for how it looks; you're there to check how it is.
The tools will get better at this — training feedback loops eventually catch up. Until then, semantics are a human's job at review time. It's not a burden, honestly. It's the most impactful thirty seconds you'll spend on the PR.