# Review Accessibility on Every Nuxt Pull Request

Yes. On each pull request, an agent builds your Nuxt app, runs the changed pages through a browser with axe, reads components for labels and focus, and comments.

[Run this on Rush](https://cloud.getrush.ai/run/review-accessibility/nuxt/), in a Project that has your repository.

## What the agent does

The agent reads what the project already checks so its review adds to it rather than repeating it. In an app like `nuxt/movies`, `nuxt.config.ts` loads `@unocss/nuxt`, `@nuxt/image`, `@nuxtjs/i18n` with more than a dozen locales including `fa-IR`, and `@nuxtjs/html-validator` with the `html-validate` recommended and standard rule sets but `failOnError: false`, so markup errors are logged and never block. `app/app.vue` sets `htmlAttrs.lang` from the active locale through `useHead`. `tests/e2e/homepage.spec.ts` already queries by role, `getByRole('navigation', { name: 'Primary' })` and `getByRole('heading', { level: 2 })`, and `.github/workflows` runs the Vitest component tests, lint, `nuxt typecheck` and the Playwright suite across desktop Chrome, Safari and a mobile Chrome project. What nobody runs is an accessibility engine, and the review fills that gap.

Then it reads the components the pull request touches, with the whole app as context for what is normal here. `app/components/NavBar.vue` is a `<nav aria-label="Primary">` whose four links are icon `<div>`s with a `:title` and no text, so a screen reader gets the title and a keyboard user gets no visible label. `carousel/Base.vue` has two scroll buttons with `title` only, styled `op0 hover:op100`, so they are invisible until the mouse arrives and a focused button stays invisible unless a focus style is added. `photo/Modal.vue` is a fixed overlay with close, previous and next buttons that have `data-testid` attributes but no accessible names, an image marked `aria-hidden` with no alternative text elsewhere, keyboard handling for Escape and the arrow keys through `useEventListener`, and no `role="dialog"`, no `aria-modal`, no focus trap and no return of focus when it closes. `StarsRate.vue` is two `aria-hidden` images, meaningful only because `media/Card.vue` prints `formatVote(item.vote_average)` beside it, and `pages/search.vue` has an input with a placeholder and no label. `LanguageSwitcher.vue`, by contrast, pairs its `<select>` with a `<label for="langSwitcher">`, and `media/Card.vue` gives its `NuxtImg` an `alt` from the title.

It also runs the app. In the sandbox it installs with pnpm, builds with `nuxt build`, starts the output with `node .output/server/index.mjs`, and drives a browser to the pages the pull request changes, running axe against each one at desktop and mobile widths and in `fa-IR` as well as English, because the app sets `lang` per locale but never sets `dir`, so right-to-left text renders in a left-to-right layout. It tabs through each changed page and records where focus goes, whether it is visible, and whether the modal keeps it. The axe engine is installed in the sandbox only; the repository is not changed.

The result is one review comment on the pull request. Each finding names the component and line, the WCAG criterion it fails, what a screen reader or keyboard user experiences, and the fix in this codebase's style: an `aria-label` bound to the same `$t()` string the `title` uses, `role="dialog"` and `aria-modal="true"` on the modal with focus moved to the close button on open and back to the trigger on close, a `focus-visible:op100` class beside `hover:op100` on the carousel buttons, a visually hidden `<label>` for the search input, and `dir` set from the locale in the same `useHead` call that sets `lang`. Findings that axe reported get the rule id; findings from reading get the reasoning. The agent pushes nothing.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Review this pull request to a Nuxt app for accessibility without changing any code. Read nuxt.config.ts, app/app.vue, i18n.config.ts, the Playwright and Vitest configs, tests/e2e/homepage.spec.ts, every component and page under app that the pull request changes, and NavBar.vue, carousel/Base.vue, photo/Modal.vue, StarsRate.vue, LanguageSwitcher.vue, media/Card.vue and pages/search.vue for the conventions the app follows. Install with pnpm, run nuxt build, start the output server, and for each changed page run axe in a browser at a desktop and a mobile width and in English and in fa-IR, then tab through the page and record where focus goes, whether it is visible, and whether any overlay traps and returns it. Report controls with no accessible name, icon-only links and buttons that rely on title, images with aria-hidden and no text alternative, inputs with only a placeholder, overlays without dialog semantics or focus management, hover-only visibility that hides focused controls, and a lang attribute without a matching dir for right-to-left locales. Post one review comment on the pull request: each finding with the component and line, the WCAG criterion, the axe rule id where one applies, what a screen reader or keyboard user experiences, and a fix in the style of this codebase, ordered by severity, ending with what the change does well. Push nothing.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app/pull/123"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a review comment on the pull request. It opens with what was tested: the pages the pull request changes, the two widths, the two locales, and the axe rule set, so a reader knows the scope before the findings. Each finding then names the component and line, the WCAG criterion and the axe rule id where axe found it, what a screen reader announces or a keyboard user sees, and the fix written with the same UnoCSS attributes and `$t()` strings the component already uses.

Findings that the browser run produced carry the axe output; findings from reading, such as a modal without `role="dialog"` or focus that does not return to the trigger, carry the tab sequence the agent recorded. The comment ends with what the change does well, such as a labelled control or an image with a real `alt`, so the review is a fair account rather than a list of faults.

A reviewer checks the findings against the design, decides which ones block the merge, and asks the author for the fixes, or sends a follow-up message to the same session asking the agent to push them to the branch, since the sandbox still has the app built and the browser ready to rerun axe on the result.

## Run it on every pull request that changes a page or component

In the Project that has your repository, add an automation with this prompt, mark it read-only so its runs comment but never push, and give it a GitHub trigger on `pull_request.opened` and `pull_request.synchronize` with `branch_patterns` set to `main`. Each pull request to main starts one run with the pull request appended to the prompt as the event, and each new push reruns the review on the new commits. Only work by an owner, member or collaborator fires it; pull requests from forks never do.

The trigger cannot filter on paths, so the prompt tells the agent to stop early when the diff touches nothing under `app`, and the automation's `max_runs_per_hour` caps how many reviews a busy day can start.

## Why run it on Rush?

An accessibility review needs a built app in a real browser, a screen reader's view of each control, a keyboard pass, and a second locale, and it needs all of that on every pull request, not once a quarter when someone remembers. In a Rush sandbox the agent builds the branch, runs axe at two widths and in two locales, tabs through the changed pages, and keeps the axe output and the tab sequence in the transcript, so each finding is reproducible.

The comment reads like a colleague's review rather than a tool's dump: it knows that `LanguageSwitcher.vue` already labels its select and that `NavBar.vue` relies on `title`, it writes fixes with the project's own UnoCSS attributes and translation strings, and it says what the change got right. Every pull request gets the same pass, whoever opened it.

## Frequently asked questions

### Does it change my components or push to the branch?

No. The automation runs read-only, so the result is a review comment. If you want the fixes applied, a follow-up message to the same session asks the agent to push them, and the browser in the sandbox reruns axe on the result before it does.

### The project already runs html-validator. Why is this different?

`@nuxtjs/html-validator` checks markup validity and runs with `failOnError: false`, so it logs and never blocks, and it does not run a browser. The review adds axe against the rendered page, a keyboard pass, and a reading of each component for names, roles, focus and direction, which validity checks do not cover.

### Why test in fa-IR as well as English?

Because `app/app.vue` sets `lang` from the active locale but nothing sets `dir`, so Farsi renders in a left-to-right layout with mirrored carousel arrows. Running the changed pages in a right-to-left locale catches that and anything else that only appears when the text direction flips.

### Will it flag every pull request, even ones with no UI change?

The prompt tells the agent to read the diff first and stop with a one-line comment when nothing under `app` changed. The automation's `max_runs_per_hour` keeps the cost of those short runs bounded.

### Does it add axe to the repository?

No. The engine is installed in the sandbox for the run and the repository is left as it was. If the team wants the check in CI, the comment can sketch a Playwright test using the same engine next to `tests/e2e/homepage.spec.ts`, but adding it is a separate change.
