# Claude 4-Model Accessibility Test: Fable 5.1, Opus, Sonnet 5

> Claude Code's default Fable model moved to 5.1. I ran 40 accessibility-blind prompts across four models and compared the modal and form code by hand.

**Published:** 2026-09-03 | **Updated:** 2026-09-03

---

On September 1st, the Claude Code v2.1.257 release notes carried one short line: the default model for the Fable line had switched to 5.1. If you use Fable and have never touched the model setting, the brain writing your code got quietly swapped out that day.

> The same release notes had an exception, too — the `fable` alias in Claude app gateway sessions stays on Fable 5 for now. Read this as a story about the regular Claude Code you run from the terminal.

Normally you'd just assume it got better and move on. But I got curious about something specific. **When you don't ask an AI to care about accessibility, the line it draws on its own** can differ from model to model. That line becomes your team's default. So when the default model changes, does that line move with it?

If you're curious, you measure it. I originally planned to compare just the two Fable generations, but the result came back "basically the same," which raised a second question: is this a Fable thing, or a Claude thing across the board? So I put Opus 5 and Sonnet 5 in the same experiment. Once four models are measured against the same yardstick, adding the next one is just one more column.

{{< img src="images/contents/four-lattes.jpg" alt="Four cups of coffee, each with different latte art - just as the same order comes out differently in every cup, the same prompt produces slightly different results across four models" caption="Photo: <a href='https://unsplash.com/@jameswilliamr' target='_blank' title='Opens in new window'>James William</a> / <a href='https://unsplash.com' target='_blank' title='Opens in new window'>Unsplash</a>" >}}

## Experiment Design — Deliberately Not Mentioning Accessibility

Four models went into the comparison.

| Model ID | Role in the comparison |
|---|---|
| `claude-fable-5` | Default Fable model before the switch |
| `claude-fable-5-1` | Default Fable model from v2.1.257 onward |
| `claude-opus-5` | Opus tier |
| `claude-sonnet-5` | Sonnet tier |

For the prompts, I picked two UI patterns where accessibility problems tend to show up clearly. I already have a rubric for both, since writing about [form accessibility]({{< relref "/posts/form-accessibility-mastery" >}}) and [aria-live regions]({{< relref "/posts/aria-live-regions" >}}) taught me exactly what breaks when something's missing.

- **Modal**: "Build a delete-confirmation modal as a single-file HTML/CSS/JS page. The page has a 'Delete Item' button; clicking it opens a 'Are you sure you want to delete this?' modal with Cancel and Delete buttons."
- **Form**: "Build a sign-up form that collects email and password with live validation, as a single-file HTML/CSS/JS page. Show an error message if the format is wrong."

You've probably noticed: **the word "accessibility" appears nowhere in either prompt.** That's deliberate. Any model will do its best if you say "make it accessible." What I wanted to measure was the default behavior when nobody asks — because that's what most real-world prompts look like.

I'll confess: leaving the word "accessibility" out of those two lines was the hardest part. I write about accessibility so often that a prompt without the word somehow looked naked to me. I held off anyway. It's an experiment.

I generated five files for each model-prompt combination, 40 files in total. AI generation gives you a different answer every time even for the same question, so one good result doesn't let you say "this model is good at it." Every call went through the same version of Claude Code via `claude -p --model`, and I logged the `modelUsage` field from the JSON output for every run (one run = one model call) to confirm **the model I asked for was actually the one that answered**. All 40 matched.

I checked each file in two layers.

1. **Automated scan**: I batch-scanned all 40 files with [A11y Check](https://www.a11ychk.com/) and counted WCAG 2.2 AA / KWCAG 2.2 (Korean Web Content Accessibility Guidelines) violations — a tool I built myself.
2. **Pattern review**: I read through all 40 files by hand for things automated tools can't catch — focus trapping (keeping Tab from leaking outside the modal), focus restoration on close, and whether `aria-invalid` (the attribute that tells a screen reader a field's value is invalid) was actually wired up.

> Automated tools only see static state. A behavioral question like "does Tab leak into the background while the modal is open" can only be answered by reading the code, which is why the second layer was necessary.

## Result 1 — Automated Scan: Fable Scored Zero on All 20, the Rest Tripped on Contrast

Here's what the [A11y Check](https://www.a11ychk.com/) scan found.

| Model | Violations across 5 modals | Violations across 5 forms | What tripped |
|---|---|---|---|
| fable-5 | 0 | 0 | — |
| fable-5-1 | 0 | 0 | — |
| opus-5 | 1 | 0 | 12px helper text, 3.32:1 contrast |
| sonnet-5 | 1 | 2 | 15px red delete button at 4.37:1, 12px gray hint at 3.54:1 ×2 |

Scanning all 40 files took under a minute. Reading the results took three seconds, thinking "wait, that's it?" took five. Writing the next sentence took an hour.

All four violations were **contrast ratio** (the brightness difference between text color and background, expressed as a ratio). And they landed in the usual spots — 12px gray helper text, and white text on a red button. The standard for regular text is 4.5:1; some misses were razor-close, like 4.37:1, and others clearly fell short, like 3.3:1. AI trips over the exact same spot designers hand-building UIs trip over most often.

Both Fable generations scored zero violations across all 20 files. But that's not just reassuring — it also **exposes a trap**. Think about the modal page's initial state: it's a page with one button. The modal is still closed, so the scanner can't even see it. Real questions like "does focus get trapped once the modal opens" and "where does it go on close" all live in the realm of behavior, which static scanning can't reach.

That's not to say a 100% score means nothing — it means static things like label wiring and document structure genuinely passed. But this experiment's real question, how focus behaves once the modal is open, sits outside what static scanning can judge. That's as far as the automated scan can tell you, which is why I read all 40 files by hand.

{{< img src="images/contents/check-terminal.png" alt="Terminal output from checking all 40 generated files - the top shows the A11y Check scan summary: both Fable models at zero violations, opus-5 with one modal violation, and sonnet-5 with one modal and two form violations, all contrast-related. The middle is a pattern tally table with 7 modal items and 7 form items across all four model columns, with fable-5's disabled-button focus at 2/5 and sonnet-5's instant validation on every keystroke at 5/5 and locked submit button at 5/5 highlighted in red. The bottom shows fable-5 run4's close handler code, with the line calling openButton.focus() right after openButton.disabled = true highlighted" caption="A screenshot straight from the actual review run. Every number in the tables below comes from this output." >}}

## Result 2 — Reading the Code by Hand: Modals Followed Different Textbooks, Forms Split by Tier

Honestly, I picked up a red pen before I started reading. Most of it turned out to be textbook-correct. One model was correct too — it just looked like it studied from **a different textbook**.

{{< img src="images/contents/red-pencil-review.jpg" alt="A hand marking paper with a red pencil - I read all 40 files pen in hand, but there was less to mark than I expected" caption="Photo: <a href='https://unsplash.com/@kellysikkema' target='_blank' title='Opens in new window'>Kelly Sikkema</a> / <a href='https://unsplash.com' target='_blank' title='Opens in new window'>Unsplash</a>" >}}

Let me note what all four models nailed 5/5 first — dialog semantics and heading connection in the modal (`aria-labelledby`), and in the form: label wiring, error live regions (an announcement area that lets a screen reader user hear that an error happened even without seeing it), and `autocomplete="new-password"`. That's the baseline, so I left it out of the tables below and kept only what diverged. The numbers show how many of the 5 runs contained that pattern — read the rows tagged "defect" or "against best practice" in reverse, since for those, 0/5 is the good outcome.

**Modal (5 generations per model)**

| Item | fable-5 | fable-5-1 | opus-5 | sonnet-5 |
|---|---|---|---|---|
| Uses `<dialog>` + `showModal()` | 5/5 | 5/5 | 5/5 | 0/5 |
| Deactivates background (showModal / inert / aria-hidden) | 5/5 | 5/5 | 5/5 | 0/5 |
| Initial focus on Cancel button (destructive-action care) | 5/5 | 5/5 | 4/5 | 5/5 |
| Restores focus on close (explicit code) | 5/5 | 5/5 | 4/5 | 5/5 |
| Calls `focus()` on a disabled button — defect | 2/5 | 0/5 | 0/5 | 0/5 |

Both Fable generations and Opus took the same path. They opened `<dialog>` with `showModal()`, and wrote no code for `aria-modal` (the attribute telling assistive tech to ignore everything outside the modal for now), ESC, or focus trapping. That's the right call — with `showModal()`, the browser handles aria-modal and ESC for you, and the background becomes fully `inert` (a state that blocks clicks, focus, and even assistive-tech navigation all at once), so there's no way for Tab to leak into the background. All 15 runs chose the native approach that wraps up in about ten lines. One fable-5 run even added this comment, unprompted.

```javascript
openBtn.addEventListener("click", () => {
  dialog.showModal();
  // Destructive action, so focus the safe option (Cancel) first
  cancelBtn.focus();
});
```

Sonnet studied from a different textbook. It never used `<dialog>` once — it **built the modal by hand out of divs.** That's not a failure, though. All five runs used `role="alertdialog"` (a role meaning a dialog that requires a response; one run used `dialog`) with `aria-modal`, ESC-to-close, a Tab-cycling trap, and hand-written code to save focus before opening and restore it on close. It's exactly what the WAI-ARIA APG (the official pattern guide) still teaches. This is how everyone built modals before native `<dialog>` was reliable. If you've ever hand-rolled a focus trap, you know the moment: Tab cycles fine, but you forgot Shift+Tab and QA catches it. Sonnet remembered Shift+Tab in all five runs.

One thing was missing — **it didn't deactivate the background.** The JS trap holds Tab in place, but containing the screen reader's virtual cursor (a separate reading cursor from Tab, that moves through the page content line by line) with `aria-modal` alone is left up to each screen reader. JAWS and NVDA generally respect it, but some, like iOS VoiceOver, can wander into content behind the modal, which is why the ARIA spec itself recommends (SHOULD) making the background `inert`. Building everything native `<dialog>` gives you for free, by hand, meant dropping one thing along the way.

Here's what's behind the defect row in the modal table. Two of the older Fable's runs, once a deletion was confirmed, set the "Delete Item" button to `disabled` **and then** called `focus()` on that same button. A disabled button can't receive focus, so the call fails silently and focus falls back to the top of the document. The corresponding runs from the newer Fable and Opus, in the same situation, gave `tabindex="-1"` to a status message or empty-state region and moved focus there instead — the recommended approach. One of Opus's two off-baseline runs (out of the 4/5) made the opposite mistake — it put initial focus on the **Delete button itself**. This is a modal where a single Enter press deletes the item. Asking "are you sure you want to delete this?" and then resting the cursor right on the Delete button is a bit like ending a meeting three seconds after asking "any objections?" The other off-baseline run simply didn't write explicit code to return focus on close — but since a `<dialog>` opened with `showModal()` already gets returned to its previous focus by the browser when it closes, this doesn't actually break anything in practice; it's only blank by the stricter "explicit code" standard.

**Form (5 generations per model)**

"After blur" in the table means the form doesn't nag you until you actually leave the field. Nobody wants to fill out a form that scolds them with "wrong format" starting from the very first character.

| Item | fable-5 | fable-5-1 | opus-5 | sonnet-5 |
|---|---|---|---|---|
| Marks error state with `aria-invalid` | 5/5 | 5/5 | 5/5 | 2/5 |
| When validation starts | after blur | after blur | after blur | from the first keystroke |
| Locks the submit button with `disabled` — against best practice | 0/5 | 0/5 | 0/5 | 5/5 |

This is where the tiers clearly split. Fable and Opus, in all 15 runs, kept a `touched`-style flag and validated only after the field was left, keeping the submit button enabled and validating again on submit. Sonnet, in all 5 runs, validated instantly from the first keystroke and locked the submit button with `disabled` until every field was filled. A `disabled` button can't receive focus and gives no explanation for why it's inactive, which is exactly the pattern accessibility guidance tells you to avoid. The other spot where things split was `aria-invalid` (2/5).

Don't get the wrong idea, though. Sonnet's forms have every label wired up, and errors are announced through a live region too. It's not a broken form — it's **a form with different defaults**. Instant validation and a locked submit button are an extremely common pattern on the web, and Sonnet picked the common one. Fable and Opus picked the less common option, the one accessibility guidance recommends.

## Conclusion — Generation Upgrades Kept the Same Defaults, Different Tiers Didn't

Before measuring, my hypothesis was "5.1 is smarter, so it'll probably handle accessibility better too." The data says smarts and habits are different axes. The two questions came back with two separate answers.

The switch from Fable 5 to 5.1 didn't move the baseline. Every row tied except the one defect row, and that single difference was an edge-case mistake — returning focus to a disabled button after deletion (2/5 → 0/5). Five samples aren't enough to declare "5.1 is better," but nothing got worse.

Different tiers, different baselines. Fable and Opus have nearly identical profiles, while Sonnet's profile is distinctly different: hand-built instead of native, instant validation instead of forgiving timing, a locked button instead of one left enabled. Neither approach means the model "doesn't know accessibility." It's just that, left unprompted, their hands reach in different directions.

There's one practical takeaway. **When your team's model tier changes, your accessibility review checklist should change with it.** A generation upgrade within the same tier, at least this time, didn't require any change. If your team uses Fable or Opus, the fundamentals of modals and forms will mostly show up on their own, so spend review time on the edges — focus after deletion, initial focus placement. If your team uses Sonnet, put background deactivation for modals and validation timing plus the submit button at the top of your review list. Same company, same lineup of models, different review priorities.

## Measuring It Yourself — Scripts and Steps

If you've read this far and thought "so what about the model my team is using?" — that's exactly why I left the scripts behind. The exact files I used for the experiment are in the [GitHub demo folder](https://github.com/IsaacEryn/isaaceryn.github.io/tree/main/demo_codes/claude-model-a11y-benchmark). You need three things: a logged-in Claude Code CLI (if `claude -p "OK"` answers, you're set), Python 3 (standard library only), and one automated scanner.

**1. Generate.** Everything hinges on this one line. You need `--output-format json` for the actually-responding model to show up in `modelUsage`.

```bash
printf '%s' "$PROMPT" | claude -p --model claude-sonnet-5 --output-format json > run1.json
```

The demo folder's `gen.sh` runs this across models × 2 prompt types × 5 repetitions, and separately logs the responding model to `.meta.json`.

```bash
./gen.sh claude-fable-5-1,claude-opus-5,claude-sonnet-5
```

> Calling another `claude` from inside a Claude Code session can mix in the parent session's auth variables and trigger a 401. That's why `gen.sh` strips `ANTHROPIC_*` and `CLAUDE*` variables before calling out.

**2. Automated scan.** I scanned the whole local folder at once using A11y Check's Claude Code plugin (`scan_dir`). If you'd rather use the web version, host the generated files somewhere and paste the URL into [A11y Check](https://www.a11ychk.com/); if you want to stay fully open-source, spin up a local server and run the axe CLI.

```bash
npx serve gen
npx @axe-core/cli http://localhost:3000/claude-sonnet-5/P_MODAL/run1.html
```

**3. Pattern tally.** For the behavioral code automated scanning can't see, `check.py` counts it for you.

```bash
python3 check.py claude-fable-5-1,claude-opus-5,claude-sonnet-5
```

One thing to keep in mind — **this script's regex patterns were tuned to this specific batch of output.** They'll misfire on different prompts. Even while writing this post, it got things wrong twice (it missed `openButton` because it was looking for `openBtn`, and it counted focus restoration as zero for modals that didn't use `<dialog>`), and I caught both by reading the files directly. Open the files before you trust the numbers.

## What This Experiment Doesn't Tell You

- **Just two simple UI patterns, short prompts, five runs per model.** If a constraint forces the model away from native `<dialog>` — something like "build it as our design system's Modal component" — the moment where the browser can no longer do the work for you, Fable's and Opus's results could well slide down toward Sonnet's.
- I measured with only the model changed, on the same version of Claude Code (same system prompt). The model difference is controlled for, but this doesn't speak for whatever else a CLI update might bring along with it.
- This isn't a claim that Sonnet's "instant validation, locked button" approach is bad UX. All I can say is that it runs against accessibility guidance.
- And a high default score doesn't mean you can skip review. Automated scanning gave all 20 Fable files 100%, but a run where focus evaporated was sitting right there inside that set. Reading behavior is still a job for human eyes.

## One-Page Summary

- As of Claude Code v2.1.257, the default Fable model switched to `claude-fable-5-1`
- I measured 40 generated files: 2 accessibility-blind prompts × 4 models × 5 runs, logging and cross-checking the actually-responding model for every run
- Automated scan (WCAG 2.2 AA / KWCAG 2.2): 0 violations across 20 Fable files, 1 for Opus, 3 for Sonnet — all contrast issues on 12px helper text and the red button
- Fable 5 → 5.1 tied on every row but one defect row. That one difference was an edge-case mistake returning focus to a disabled button after deletion (2/5 → 0/5)
- Fable and Opus: native `<dialog>`, validation after blur, submit button left enabled. Sonnet: hand-built modal (trapping and restoration present, but background deactivation missing), instant validation, locked submit button — the defaults split across three points: the modal, validation timing, and the submit button

---

The exact prompts and tallying criteria used in this experiment are right here in the post, so when the next model ships, adding one more column to the same experiment turns this post into your baseline. That's part of why these numbers exist.

