# 12 students, one dropdown, and the question AI couldn't answer

> Half my students built an accessible dropdown with AI, half without. The AI group was faster. Then we tested with the keyboard.

- Pool Ortega, September 28, 2026
- URL: https://www.poolortega.com/en/blog/accessible-dropdown-ai-students
- Tags: accessibility, javascript, ai, teaching

Last week 12 students got the same exercise: build an accessible dropdown with plain HTML, CSS and JavaScript. No framework, no component library. Half of them could use AI. Half couldn't.

The AI group finished in 25 minutes on average. The other group took 60.

Then I asked everyone to use their dropdown with the keyboard only. No mouse, no trackpad.

5 of the 6 AI-built dropdowns trapped focus or skipped the options entirely. 4 of the 6 slower ones worked.

## The part that stuck with me

The keyboard results weren't the surprise. What stayed with me came after.

When I asked the AI group why they used a `div` with an `onClick` instead of a `button`, most of them couldn't answer. They hadn't chosen it. They had accepted it.

The slower group could explain every line, including the ones that were broken. That matters more than it sounds. If you can explain a broken line, you can fix it. If you never chose the line in the first place, you don't even know where to start looking.

## What an accessible dropdown actually needs

"Dropdown" covers a few different patterns, and that's part of the problem. A list of links that opens from a button is a **disclosure**. A custom `<select>` replacement is a **listbox**. An app-style list of actions is a **menu**. Each one has different keyboard rules, and mixing them is how you end up with something that looks right and behaves wrong.

For the exercise, a disclosure was enough. This is what it needs:

1. **A real `<button>`.** It's focusable, it fires on Enter and Space, and screen readers announce it as a button. A `div` gives you none of that. You end up adding `tabindex`, `role` and two key handlers to rebuild what the browser already had.
2. **`aria-expanded` on the button**, kept in sync with the list. That's how a screen reader user knows if it's open.
3. **`aria-controls`** pointing to the list, so the relationship is explicit.
4. **The list hidden for real when closed.** `hidden` or `display: none`, not just `opacity: 0`. Otherwise keyboard users tab through links they can't see.
5. **Escape closes it and sends focus back to the button.** Without that, the user is left somewhere on the page with no idea where.
6. **Tab still works normally.** Don't trap focus in a disclosure. If focus leaves the component, close it.
7. **Arrow keys are a bonus**, not a requirement, for a disclosure. If you add them, make sure they don't break Tab.

Here's a version that covers all of that:

```html
<div class="dropdown">
  <button type="button" aria-expanded="false" aria-controls="lang-list">
    Language
  </button>
  <ul id="lang-list" hidden>
    <li><a href="/en">English</a></li>
    <li><a href="/es">Español</a></li>
  </ul>
</div>
```

```js
const root = document.querySelector(".dropdown");
const button = root.querySelector("button");
const list = root.querySelector("ul");
const links = [...list.querySelectorAll("a")];

function setOpen(open) {
  button.setAttribute("aria-expanded", String(open));
  list.hidden = !open;
}

button.addEventListener("click", () => {
  setOpen(button.getAttribute("aria-expanded") !== "true");
});

root.addEventListener("keydown", (event) => {
  if (event.key === "Escape" && !list.hidden) {
    setOpen(false);
    button.focus();
    return;
  }

  // Optional: arrow keys move between links
  if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
  event.preventDefault();
  setOpen(true);

  const step = event.key === "ArrowDown" ? 1 : -1;
  const current = links.indexOf(document.activeElement);
  const next =
    current === -1
      ? step === 1 ? 0 : links.length - 1
      : (current + step + links.length) % links.length;
  links[next].focus();
});

// Close when focus moves to something outside the component (e.g. Tab)
root.addEventListener("focusout", (event) => {
  if (event.relatedTarget && !root.contains(event.relatedTarget)) {
    setOpen(false);
  }
});

// Close when clicking anywhere else on the page
document.addEventListener("click", (event) => {
  if (!root.contains(event.target)) setOpen(false);
});
```

A few things worth noticing. There's no `role="menu"` here. Adding it would promise screen reader users menu behavior (arrow keys only, Tab leaves the menu, typeahead) that this component doesn't implement. No ARIA is better than wrong ARIA.

The `focusout` handler checks `relatedTarget` before closing. Some browsers don't move focus to a button when you click it, so `relatedTarget` can be `null` on a click. The separate document click handler covers that case.

If you need a real `<select>` replacement, that's the listbox pattern, and it's more work: `role="listbox"`, `role="option"`, `aria-selected`, and managing the active option with either a roving `tabindex` or `aria-activedescendant`. Check the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/) before building one. And honestly, ask first if a native `<select>` would do.

## The rule I added

I write React and Next.js for a living and I use AI every single day. I'm not telling anyone to stop.

I just added one rule to my class: **you can paste AI code once you can explain what it's replacing.**

If the AI gives you a `div` with an `onClick`, you should be able to say "this replaces a button, and here's what I lose". If you can't say that, you're not ready to paste it.

The AI group wasn't worse at coding. They skipped the step where you decide. The slower group made more decisions, some of them wrong, and that's exactly why they could fix them.

## What I'd hire for

Speed is easy to get now. Anyone can produce a dropdown in five minutes.

The skill I'd hire for is the second one: knowing why the code looks the way it does. That's what lets you review AI output, catch the `div` that should be a `button`, and test with a keyboard before shipping.

If you mentor or lead juniors, I'm curious: what rule have you set for AI on your team?
