Catch accessibility issues in your editor, not in production. Zero-config ESLint plugin + programmatic API for React, Vue, and JSX.
- ✅ Zero config - Works out of the box with React, Vue, and JSX
- ✅ Real-time feedback - Catch issues in your editor, not in production
- ✅ 43 accessibility rules - Covers images, forms, buttons, landmarks, ARIA, focus, and more
- ✅ Editor suggestions - Get actionable fixes directly in your editor
- ✅ Dual API - Use as ESLint plugin OR programmatic API
- ✅ Large project ready - Minimal preset for incremental adoption
- ✅ Framework agnostic - Works with React, Vue, Preact, Solid, and more
- ✅ Fast & lightweight - Pure AST validation, 35KB bundle, zero memory issues
npm install --save-dev eslint-plugin-a11yeslint(>=8.0.0) - Required for ESLint pluginvue-eslint-parser(>=9.0.0) - Optional, only needed for Vue supportjsdom(>=23.0.0) - Optional, only needed for A11yChecker core library (programmatic API)
Note: The ESLint plugin does NOT use jsdom. It performs pure AST validation for maximum performance. jsdom is only required if you use the programmatic A11yChecker API in your tests.
Add to your .eslintrc.js or .eslintrc.json:
// .eslintrc.js
module.exports = {
plugins: ['a11y'],
extends: ['plugin:a11y/recommended']
}Or for React/JSX projects:
module.exports = {
plugins: ['a11y'],
extends: ['plugin:a11y/react']
}That's it! Start catching accessibility issues immediately in your editor.
Use in your tests:
import { A11yChecker } from 'eslint-plugin-a11y/core'
// Test a DOM element for accessibility violations
const violations = await A11yChecker.check(element)
// Or check specific patterns
const imageViolations = A11yChecker.checkImageAlt(element)
const buttonViolations = A11yChecker.checkButtonLabel(element)Use ESLint Plugin when:
- ✅ You want real-time feedback in your editor
- ✅ You want to catch issues during development
- ✅ You want CI/CD integration to prevent commits with violations
- ✅ You want team-wide enforcement
Use Programmatic API when:
- ✅ You want to write specific accessibility tests
- ✅ You need to test dynamically generated DOM
- ✅ You want custom reporting or analytics
- ✅ You're writing integration/E2E tests
You can use both! Many teams use ESLint plugin for development and programmatic API for comprehensive test coverage.
Classic Config (.eslintrc.js):
plugin:a11y/minimal- Only 3 critical rules (best for large projects)plugin:a11y/recommended- Balanced approach (default)plugin:a11y/strict- All rules as errorsplugin:a11y/react- Optimized for React/JSXplugin:a11y/vue- Optimized for Vue SFC
Flat Config (eslint.config.js) - ESLint v9+:
flat/recommended- Rules only (minimal assumptions)flat/recommended-react- Rules + React parserflat/react- Full React setupflat/vue- Full Vue setupflat/minimal- Minimal rules onlyflat/strict- All rules as errors
React/JSX:
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: { jsx: true }
},
plugins: ['a11y'],
extends: ['plugin:a11y/react']
}Vue:
module.exports = {
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
},
plugins: ['a11y'],
extends: ['plugin:a11y/vue']
}Ignore a single line:
// eslint-disable-next-line a11y/image-alt
<img src="decorative.jpg" alt="" />Ignore an entire file:
/* eslint-disable a11y/heading-order */Ignore directories:
module.exports = {
plugins: ['a11y'],
extends: ['plugin:a11y/recommended'],
ignorePatterns: [
'**/node_modules/**',
'**/dist/**',
'**/*.test.{js,ts,jsx,tsx}'
]
}Many rules support configuration options for fine-tuned control:
image-alt - Decorative images:
{
'a11y/image-alt': ['error', {
allowMissingAltOnDecorative: true,
decorativeMatcher: {
markerAttributes: ['data-decorative']
}
}]
}link-text - Custom denylist:
{
'a11y/link-text': ['warn', {
denylist: ['click here', 'read more'],
caseInsensitive: true
}]
}heading-order - Skip tolerance:
{
'a11y/heading-order': ['warn', {
allowSameLevel: true,
maxSkip: 2
}]
}See Configuration Guide for all options.
Map your design-system components to native HTML elements:
module.exports = {
plugins: ['a11y'],
extends: ['plugin:a11y/recommended'],
settings: {
'a11y': {
components: {
Link: 'a', // Treat <Link> as <a>
Button: 'button', // Treat <Button> as <button>
Image: 'img' // Treat <Image> as <img>
},
polymorphicPropNames: ['as', 'component'] // Support <Link as="a">
}
}
}Now rules apply to your components:
<Link href="/about">Click here</Link> // ⚠️ Warning: nonDescriptive
<Button></Button> // ❌ Error: missingLabel// eslint.config.js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import react from 'eslint-plugin-react'
import testA11y from 'eslint-plugin-a11y'
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
plugins: {
react,
'a11y': testA11y
},
languageOptions: {
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
ecmaFeatures: { jsx: true }
}
},
...testA11y.configs['flat/recommended-react']
},
// Optional: Vue-specific rules only on .vue files
{
files: ['**/*.vue'],
languageOptions: {
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2022,
sourceType: 'module'
}
},
plugins: {
'a11y': testA11y
},
...testA11y.configs['flat/vue']
}
]These presets mirror the classic .eslintrc presets and are the easiest way to drop eslint-plugin-a11y into a modern ESLint v9+ setup, alongside @eslint/js, typescript-eslint, and eslint-plugin-react.
See Configuration Guide for more flat-config examples and advanced setups.
Many rules provide suggestions that appear in your editor, allowing you to quickly fix issues:
- iframe-title: Suggests adding
title=""placeholder - button-label: Suggests adding
aria-label=""for icon-only buttons - link-text: Suggests replacing non-descriptive text
- heading-order: Suggests correct heading level
In VS Code and other editors with ESLint support, suggestions appear as Quick Fix options (Cmd/Ctrl + .).
Note: Suggestions are not autofixes - they require manual review and approval.
The plugin provides 43 accessibility rules:
Core rules:
a11y/image-alt- Enforce images,input[type=image], andareaelements have alt attributesa11y/button-label- Enforce buttons have labelsa11y/link-text- Enforce links have descriptive texta11y/form-label- Enforce form controls have labelsa11y/heading-order- Enforce proper heading hierarchya11y/iframe-title- Enforce iframes have title attributesa11y/fieldset-legend- Enforce fieldsets have legend elementsa11y/table-structure- Enforce tables have proper structurea11y/details-summary- Enforce details elements have summarya11y/video-captions- Enforce video elements have caption tracksa11y/audio-captions- Enforce audio elements have tracks or transcriptsa11y/landmark-roles- Enforce proper use of landmark elementsa11y/dialog-modal- Enforce dialog elements have proper accessibility attributesa11y/aria-validation- Validate ARIA roles, properties, and ID references (AST-first)a11y/semantic-html- Enforce proper use of semantic HTML elements (AST-first)a11y/form-validation- Validate form validation patterns (AST-first)
Attribute & document rules:
a11y/no-access-key- Disallow accessKey on elementsa11y/no-autofocus- Disallow autoFocusa11y/tabindex-no-positive- Disallow positive tabIndexa11y/no-distracting-elements- Disallow blink and marqueea11y/lang- Enforce valid lang attribute valuesa11y/html-has-lang- Enforce html element has lang attribute
Focusable & ARIA rules:
a11y/no-aria-hidden-on-focusable- Disallow aria-hidden on focusable elementsa11y/no-role-presentation-on-focusable- Disallow role="presentation" on focusable elementsa11y/no-interactive-element-to-noninteractive-role- Disallow role="none/presentation" on interactive elements (button, a, input, etc.)a11y/no-noninteractive-element-to-interactive-role- Disallow interactive roles on non-interactive elements without keyboard supporta11y/no-redundant-roles- Disallow explicit roles that match the element's implicit ARIA rolea11y/prefer-tag-over-role- Prefer semantic native HTML elements over ARIA role attributes on generic elementsa11y/control-has-associated-label- Enforce ARIA-role interactive controls have an accessible labela11y/scope- Enforce valid use of the scope attribute on<th>elementsa11y/aria-activedescendant-has-tabindex- Enforce aria-activedescendant targets are focusable
Event & keyboard rules:
a11y/click-events-have-key-events- Enforce onClick has keyboard equivalenta11y/mouse-events-have-key-events- Enforce mouse handlers have keyboard equivalenta11y/no-static-element-interactions- Disallow static handlers on non-interactive elementsa11y/no-noninteractive-element-interactions- Disallow interactive handlers on non-interactive elementsa11y/interactive-supports-focus- Enforce interactive elements are focusablea11y/no-noninteractive-tabindex- Disallow tabindex on non-interactive elements
Content & media rules:
a11y/heading-has-content- Enforce headings have contenta11y/img-redundant-alt- Enforce img alt does not contain redundant wordsa11y/anchor-ambiguous-text- Enforce link text is not generica11y/anchor-is-valid- Enforce anchors have valid href (not empty,#, orjavascript:)a11y/accessible-emoji- Enforce emoji have accessible labelsa11y/autocomplete-valid- Enforce autocomplete attribute is valid
import { A11yChecker } from 'eslint-plugin-a11y/core'
// Comprehensive check
const results = await A11yChecker.check(element)
// Individual checks
A11yChecker.checkImageAlt(element)
A11yChecker.checkButtonLabel(element)
A11yChecker.checkLinkText(element)
A11yChecker.checkFormLabels(element)
A11yChecker.checkHeadingOrder(element)
A11yChecker.checkIframeTitle(element)
A11yChecker.checkFieldsetLegend(element)
A11yChecker.checkTableStructure(element)
A11yChecker.checkDetailsSummary(element)
A11yChecker.checkVideoCaptions(element)
A11yChecker.checkAudioCaptions(element)
A11yChecker.checkLandmarks(element)
A11yChecker.checkDialogModal(element)
// Advanced checks (available in core library)
A11yChecker.checkAriaRoles(element)
A11yChecker.checkAriaProperties(element)
A11yChecker.checkAriaRelationships(element)
A11yChecker.checkAccessibleName(element)
A11yChecker.checkCompositePatterns(element)
A11yChecker.checkSemanticHTML(element)
A11yChecker.checkFormValidationMessages(element)interface A11yViolation {
id: string
description: string
element: Element
impact: 'critical' | 'serious' | 'moderate' | 'minor'
}
interface A11yResults {
violations: A11yViolation[]
}// ❌ ESLint will catch this
function MyComponent() {
return (
<div>
<img src="photo.jpg" /> {/* Missing alt */}
<button></button> {/* No label */}
</div>
)
}
// ✅ Fixed
function MyComponent() {
return (
<div>
<img src="photo.jpg" alt="A beautiful landscape" />
<button aria-label="Close menu">×</button>
</div>
)
}<!-- ❌ ESLint will catch this -->
<template>
<img src="photo.jpg" />
<button></button>
</template>
<!-- ✅ Fixed -->
<template>
<img src="photo.jpg" alt="A beautiful landscape" />
<button aria-label="Close menu">×</button>
</template>import { render } from '@testing-library/react'
import { A11yChecker } from 'eslint-plugin-a11y/core'
test('component is accessible', async () => {
const { container } = render(<MyComponent />)
const results = await A11yChecker.check(container)
expect(results.violations).toHaveLength(0)
})For large codebases, start with minimal rules:
// .eslintrc.js
module.exports = {
plugins: ['a11y'],
extends: ['plugin:a11y/minimal'],
ignorePatterns: ['**/node_modules/**', '**/dist/**']
}See Large Project Setup Guide for incremental adoption strategies.
The plugin includes a progress-aware ESLint wrapper that shows which files are being linted, similar to Vite's test output.
Replace next lint with the progress wrapper in your package.json:
{
"scripts": {
"lint": "node node_modules/eslint-plugin-a11y/bin/eslint-with-progress.js",
"lint:fix": "node node_modules/eslint-plugin-a11y/bin/eslint-with-progress.js --fix"
}
}Or use the binary directly:
npx eslint-with-progress- ✅ Progress display - Shows "Linting files..." message
- ✅ File-by-file results with line numbers
- ✅ Summary showing total files, errors, and warnings
- ✅ Color-coded output (errors in red, warnings in yellow)
- ✅ Timing information
Example output:
Linting files...
src/components/Button.tsx
5:12 ✖ Image missing alt attribute (a11y/image-alt)
8:3 ⚠ Button should have accessible label (a11y/button-label)
────────────────────────────────────────────────────────────
Summary: 15 files linted • 2 errors • 5 warnings
Completed in 1.23s
Note: This is optional. Your plugin rules work automatically with next lint - this just adds progress display.
For large projects, use ESLint caching:
npx eslint . --cacheSee Performance Guide for optimization tips.
- ✅ React/JSX - Full support via JSX AST
- ✅ Vue - Full support via vue-eslint-parser
- ✅ HTML Strings - Support for template literals (requires jsdom for core API)
- ✅ Any JSX-based framework - Preact, Solid, etc.
- Configuration Guide - ESLint plugin configuration options, rule options, component mapping
- Migration Guide - Migrate from eslint-plugin-jsx-a11y
- Large Project Setup Guide - Incremental adoption strategies
- Performance Guide - Performance optimization tips
- ESLint Plugin Guide - Complete ESLint plugin documentation
- Vue Usage Guide - Vue-specific setup and examples
- Examples - Real-world code examples
- Troubleshooting Guide - Common issues and solutions
- JSDOM Guide - When and how to use jsdom
- vs
eslint-plugin-jsx-a11y: Similar JSX accessibility coverage, plus Vue SFC support, flat-config presets, and a matching runtimeA11yCheckerAPI. You can run both plugins side by side and selectively disable overlapping rules in either one. - vs
eslint-plugin-vuejs-accessibility: This plugin covers both Vue templates and JSX/TSX with a single rule set, which is useful in mixed React/Vue or design-system-heavy codebases. - vs runtime-only tools (e.g.
@axe-core/react):a11yfocuses on static, editor-time feedback and CI linting, while the runtime A11yChecker API complements it for dynamic DOM testing.
For rule-by-rule mapping from eslint-plugin-jsx-a11y to eslint-plugin-a11y, see the Migration Guide.
| Feature | a11y | eslint-plugin-jsx-a11y | @axe-core/react |
|---|---|---|---|
| Zero config | ✅ | ❌ | ❌ |
| Vue support | ✅ | ❌ | ❌ |
| Programmatic API | ✅ | ❌ | ✅ |
| Editor integration | ✅ | ✅ | ❌ |
| Large project ready | ✅ | ||
| Framework agnostic | ✅ | React only | React only |
-
Does this replace
eslint-plugin-jsx-a11y?
Not necessarily. It can replace it in many React-only projects, but it also adds Vue SFC support, flat-config presets, and a runtime A11yChecker API. You can also run both plugins side by side and disable overlapping rules where needed. -
Can I run
eslint-plugin-a11yandeslint-plugin-jsx-a11ytogether?
Yes. Add both plugins to your config and then selectively turn off overlapping rules in one or the other. The Migration Guide shows rule mappings and suggestions. -
Does it support ESLint v9 flat config?
Yes. All presets haveflat/*equivalents (for example,flat/recommended,flat/recommended-react,flat/vue,flat/minimal,flat/strict). See the flat-config quick start above or the Configuration Guide. -
Does it work with Vue Single File Components (SFC)?
Yes. Installvue-eslint-parserand use thevuepresets (classic orflat/vue). The flat-config example above shows how to scope Vue rules to**/*.vuefiles. -
Why does it warn on dynamic
alt/text instead of erroring?
Dynamic attributes (likealt={altText}) cannot be fully validated statically. The plugin treats them as warnings by default and expects you to cover them via runtime checks using the A11yChecker API or other testing tools.
Contributions are welcome! After cloning, npm install runs the prepare script (build + ContextKit git hooks; hooks live in .contextkit/hooks/). Please see the contributing guidelines for more information.
Marlon Maniti (https://github.com/nolrm)
MIT