DEV Community

Cover image for Why axe and jsx-a11y miss Ant Design accessibility bugs (and a GitHub Action that catches them)
Muhammad Tayyab Sheikh
Muhammad Tayyab Sheikh

Posted on AI-assisted

Why axe and jsx-a11y miss Ant Design accessibility bugs (and a GitHub Action that catches them)

If your React app uses Ant Design, your accessibility tooling is probably giving you a false sense of safety.

This passes eslint-plugin-jsx-a11y without a single warning:

import { Button, Select, Modal, Tooltip } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';

<Button icon={<DeleteOutlined />} onClick={remove} />

<Select options={countries} placeholder="Country" />

<Modal open={open} onCancel={close}>
  Are you sure?
</Modal>

<Tooltip title="You don't have permission">
  <Button disabled>Publish</Button>
</Tooltip>
Enter fullscreen mode Exit fullscreen mode

Every line here is a real accessibility bug:

  • The icon-only button has no accessible name. A screen reader announces "button" and nothing else.
  • The Select has only a placeholder as its label. Placeholders aren't reliable accessible names.
  • The Modal has no title, so the dialog is unnamed.
  • The Tooltip wraps a disabled button. Keyboard users can never focus it, so they never see why it's disabled.

Why the usual tools miss these

jsx-a11y only understands plain JSX elements. It knows <button> needs a name. It has no idea that antd's <Button> renders a <button>, or that icon without children means there's no visible text.

axe does see the problem, because it scans the rendered DOM. But it reports a selector like .ant-btn.ant-btn-icon-only, not the file and line you wrote. On a big app that's a scavenger hunt, and it only runs if something starts your app and clicks through it.

So the antd-specific bugs slip through: too high-level for jsx-a11y, too far from the source for axe.

antd A11y Guard

I built antd A11y Guard to close that gap. It's a free, MIT-licensed GitHub Action that checks pull requests for accessibility problems in React + Ant Design code.

# .github/workflows/a11y.yml
name: Accessibility
on: pull_request

permissions:
  contents: read
  pull-requests: write
  security-events: write

jobs:
  antd-a11y:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: cstayyab/antd-a11y-action@v0
Enter fullscreen mode Exit fullscreen mode

There's nothing to install or configure. It ships its own parser and rules, and it reads only the files your PR changed.

What it checks

Ten antd-aware rules, plus a tuned version of jsx-a11y's recommended set:

Rule Catches
icon-button-has-name Icon-only Button with no aria-label
picker-has-name Unlabelled Select, DatePicker, Cascader, TreeSelect…
form-control-has-name Unlabelled Input, Switch, Slider, bare Checkbox/Radio
form-item-has-label Form.Item with name but no label
modal-has-title Modal / Drawer without a title
table-column-has-title Table columns with empty titles
image-has-alt antd Image without alt
tooltip-no-disabled-child Tooltip/Popover around a disabled button
popup-trigger-focusable Dropdown/Tooltip on a non-focusable span, icon or Avatar
auth-input-autocomplete autoComplete="off" or paste blocking on login fields (WCAG 3.3.8)

Every finding names the WCAG 2.2 success criterion it fails. Results show up as inline PR annotations, in GitHub Code Scanning (via SARIF), and in one sticky PR comment.

Built to stay quiet when unsure

A linter that cries wolf gets turned off, so the rules are conservative:

  • A rule only fires on components it can trace back to an antd import (named, aliased, namespaced, antd/es/*, or destructured like const { Item } = Form).
  • Spread props, ids and custom children count as "may be labelled".
  • Each rule's claim about antd's markup is checked in CI by rendering real antd 5 and 6 and inspecting the DOM.

If your team wraps antd in its own components, declare them as aliases and the rules follow through:

- uses: cstayyab/antd-a11y-action@v0
  with:
    aliases: |
      AppButton: Button
      AppSelect: Select
Enter fullscreen mode Exit fullscreen mode

Adopt it gradually on an existing codebase

Per-rule severity lets you ratchet instead of fixing everything on day one:

rules: |
  modal-has-title: error      # already clean, keep it that way
  picker-has-name: warn       # backlog, visible but not blocking
Enter fullscreen mode Exit fullscreen mode

Same check, locally

As of v0.10 the engine also ships as an ESLint plugin, so you catch issues before you push:

npm install --save-dev eslint-plugin-antd-a11y
Enter fullscreen mode Exit fullscreen mode
// eslint.config.js
import antdA11y from 'eslint-plugin-antd-a11y';
import tseslint from 'typescript-eslint';

export default [
  ...tseslint.configs.recommended,
  ...antdA11y.config(),
];
Enter fullscreen mode Exit fullscreen mode

It reads the same config file as the action, so eslint fails exactly when the PR check would.

Runtime mode

Static analysis can't see everything: portals, client state, and markup inside third-party components. An optional runtime sub-action starts your app, crawls your routes with Playwright + axe, and can open modals and tabs through an interactions script.

In Next.js (15.3+) it goes one step further. It injects a guard that patches the JSX runtime, so axe findings are traced back to the line in your code that rendered the offending component, not a CSS selector.

I need your feedback before 1.0

The action is in beta (v0.10). Before I lock the inputs for 1.0, I'd love feedback from people running antd in real codebases:

  1. False positives or misses. Wrapper components and design-system layers are where I expect gaps.
  2. Baseline adoption. A baseline file is planned for 1.0 so large apps can start without fixing everything first. How would you want it to work?
  3. Config naming. If an input or option confuses you, now is the time to tell me.
  4. Runtime setups beyond Next.js: Vite, Remix, apps behind a sign-in.

👉 github.com/cstayyab/antd-a11y-action

Open an issue or leave a comment here. "It flagged something it shouldn't have" is exactly the kind of report I'm after. And if it's useful, a ⭐ helps other antd teams find it.

Top comments (0)