All jobs

Review Accessibility in Next.js Pull Requests

Yes. An agent runs your Next.js storefront in a browser, runs axe and the keyboard on each changed page, and comments each new violation on the pull request diff.

Run this on RushIn a Project that has your repository.

What the agent does

The agent checks out the pull request branch and reads the diff to find which routes it touches. In a Next.js app like vercel/commerce the App Router under app/ has a short list: the home page, app/product/[handle]/page.tsx, app/search/page.tsx and app/search/[collection]/page.tsx, and the content pages at app/[page]/page.tsx. A change to components/cart/modal.tsx or components/layout/navbar/index.tsx touches every route, so the agent treats shared components as global.

Then it starts the app. package.json pins next to 15.6.0-canary.60 with React 19, @headlessui/react 2.2 for dialogs and sonner for toasts, has a pnpm-lock.yaml, and its only test script is prettier:check. The storefront reads SHOPIFY_STORE_DOMAIN and SHOPIFY_STOREFRONT_ACCESS_TOKEN in lib/shopify/index.ts, so the sandbox needs those two values from .env.example filled in, as Project secrets, or the pages render without products. The agent runs pnpm install and pnpm dev, opens each affected route in the sandbox browser, and runs axe on it twice: once on the base branch and once on the head, so only violations the change introduced become findings.

The keyboard pass follows the paths a shopper takes. It tabs through the navbar, where components/layout/navbar/search.tsx renders an input with a placeholder and no label, into the mobile menu and the cart, both Headless UI Dialog panels in mobile-menu.tsx and cart/modal.tsx with aria-label on their open and close buttons; it moves through the option buttons in components/product/variant-selector.tsx, which carry aria-disabled and a title for out-of-stock values; and it submits add-to-cart.tsx, whose aria-live status paragraph should announce the result. It checks focus goes into each dialog on open and back to the trigger on close, and that components/product/gallery.tsx buttons and images keep their aria-label and alt text.

Each finding becomes one review comment on the changed line: the axe rule id and the element it matched, or the keyboard step that failed, and the smallest fix. Pre-existing problems the change did not introduce are listed once in the review summary, not on the diff. The agent edits no source file and opens no second pull request; the review and an accessibility.md with the full axe output are what it returns.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Review the open pull request on this Next.js storefront for accessibility. Read the diff to find the routes and shared components it touches. Fill SHOPIFY_STORE_DOMAIN and SHOPIFY_STOREFRONT_ACCESS_TOKEN from the Project's secrets, run pnpm install and pnpm dev, and open each affected route in the browser on the base branch and on the head branch. Run axe on each, and tab through the navbar search, the mobile menu and cart dialogs, the variant selector and the add-to-cart button, checking focus order, focus return on dialog close and the aria-live status text. Comment on the changed lines with each violation the change introduces, naming the axe rule or the keyboard step and the fix; list pre-existing violations once in the review summary and do not comment them on the diff. Write accessibility.md with the full axe output per route. Do not edit source files and do not open another pull request.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app/pull/42"
    }
  ]
}
  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 on the pull request and accessibility.md on the session's artifacts. Each review comment sits on a changed line and names the element, the axe rule, for example label on an input with only a placeholder or color-contrast on a new badge, or the keyboard step, for example focus staying behind the cart panel after it closes, with the fix in a sentence. The summary lists violations that were already on the base branch so the team can file them separately.

accessibility.md holds the per-route axe results for base and head side by side, the keyboard paths the agent walked, and the routes it could not reach, for example a collection page that needs a product the test store does not have. A reviewer checks that each comment points at a line in the diff, that the rule named matches the element, and that a fix suggested for a Headless UI dialog respects how that component manages focus.

Run it on every pull request opened or updated

Add an automation to the Project with this prompt and a github trigger on pull_request.opened and pull_request.synchronize, with branch_patterns set to main so only pull requests into the default branch start a run. Each push starts one run on that pull request's branch, and the review lands on the pull request as it would from a person. Only work by an owner, member or collaborator fires the trigger; pull requests from forks never do.

Without an automation, a handler for your GitHub webhook can send this request itself, replacing the pull/42 resource with the pull request's URL and using the head commit as Idempotency-Key, so a retried delivery returns the run already started. The key's webhook then reports run.completed with the session_url for the review.

The use case this job follows, with its automation and trigger: Review every pull request.

Why run it on Rush?

Accessibility review is the review that gets skipped, because doing it properly means running the app, opening a browser and tabbing through every changed page, and nobody does that for a one-line change to a shared component. The agent does exactly that for each pull request in its own sandbox with its own browser, and the comments arrive with the rest of the review.

The findings come with proof: the axe output per route, the base-versus-head comparison that separates new problems from old ones, and the keyboard steps it took. That keeps the review honest in both directions: a pull request is not blamed for a label that was missing before it, and a real regression in focus handling is caught before it ships.

Frequently asked questions

The storefront needs a Shopify store. How does the sandbox get one?

Through Project secrets: save SHOPIFY_STORE_DOMAIN and SHOPIFY_STOREFRONT_ACCESS_TOKEN for a development store in the Project and start the session there, and the agent reads them as environment variables. Without them lib/shopify/index.ts has nothing to fetch and the agent reports that it could only check static pages.

Does it report problems that existed before the pull request?

Not on the diff. It runs axe on the base branch and the head branch and comments only on violations that appear on head and not on base. Pre-existing violations go in the review summary as a list, which is where the search input's missing label would appear today.

Which checks go beyond axe?

The keyboard pass: tab order through the navbar and product page, focus moving into the mobile menu and cart dialogs and back to their buttons on close, the variant selector's disabled options, and whether the aria-live region in the add-to-cart and cart quantity buttons announces its status. axe cannot see those; the agent records each step it took.

Will it change the code or push to the branch?

No. The prompt forbids editing source files and opening another pull request. Each comment proposes the fix in words, and the author decides how to apply it.

Can it review a pull request that adds a whole new page?

Yes. There is no base version to compare, so every violation on the new route is new and gets a comment on the nearest changed line, usually the component that renders the element. The summary says the route had no base to diff against.