npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

eslint-plugin-a11y-enforce

v0.2.1

Published

ESLint plugin that catches accessibility composition errors that element-level tools miss.

Readme

eslint-plugin-a11y-enforce

npm Socket Badge License: MIT

ESLint plugin that catches accessibility composition errors that element-level tools miss.

eslint-plugin-jsx-a11y checks individual elements: "does this img have alt text?" a11y-enforce checks how elements relate to each other: "does this trigger's aria-haspopup match its content's role?" "Is this accordion trigger inside a heading?"

Use both. They complement each other.

What this catches that jsx-a11y does not

| Pattern | jsx-a11y | a11y-enforce | |---------|----------|--------------| | role="dialog" without aria-modal="true" | No | Yes | | aria-haspopup with invalid value | No | Yes | | Interactive elements inside role="tooltip" | No | Yes | | Accordion trigger not inside a heading | No | Yes | | role="menuitem" on a <button> | No | Yes | | role="dialog" without accessible name | No | Yes | | tabIndex={0} without keyboard handler | No | Yes | | Form input without accessible label | Unreliable | Yes | | Radio buttons without grouping container | No | Yes | | tabIndex greater than 0 | Warning | Error |

Install

npm install --save-dev eslint-plugin-a11y-enforce

Usage

ESLint 9+ (flat config)

// eslint.config.js
import a11yEnforce from 'eslint-plugin-a11y-enforce';

export default [
  a11yEnforce.configs.recommended,
];

ESLint 8 (legacy config)

{
  "extends": ["plugin:a11y-enforce/legacy/recommended"]
}

Rules

All rules are set to error in the recommended config.

Component pattern rules

These validate ARIA relationships in compound components like Dialog, Menu, Select, Accordion, and Tooltip. No other public ESLint plugin checks these patterns.

dialog-requires-modal

Elements with role="dialog" or role="alertdialog" must have aria-modal="true". Without it, screen readers allow users to navigate outside the dialog using virtual cursor.

// Bad: screen reader can read content behind the dialog
<div role="dialog">Content</div>

// Good: navigation restricted to dialog
<div role="dialog" aria-modal="true" aria-labelledby="title">Content</div>

haspopup-role-match

aria-haspopup must be one of: menu, listbox, tree, grid, dialog, true, false. Invalid values are silently treated as false by browsers, meaning the popup is never announced.

// Bad: "dropdown" is not a valid value
<button aria-haspopup="dropdown">Open</button>

// Good: matches the popup's role
<button aria-haspopup="menu">Actions</button>

tooltip-no-interactive

role="tooltip" must not contain focusable elements (buttons, links, inputs, elements with tabIndex >= 0). Tooltips disappear on blur, making interactive content inside them unreachable by keyboard users.

// Bad: keyboard user cannot reach the link
<div role="tooltip"><a href="/help">Learn more</a></div>

// Good: text-only content
<div role="tooltip">Save your changes (Ctrl+S)</div>

accordion-trigger-heading

A <button> or element with role="button" that has aria-expanded (accordion trigger) should be inside a heading element (h1-h6 or role="heading"). Screen reader users navigate pages by headings. Without a heading wrapper, accordion sections are invisible to heading navigation.

// Bad: invisible to heading navigation
<div><button aria-expanded="true">Section</button></div>

// Good: discoverable via heading navigation
<h3><button aria-expanded="true">Section</button></h3>

menuitem-not-button

role="menuitem" (including menuitemcheckbox and menuitemradio) should not be on <button> elements. Buttons have an implicit "button" role, causing some screen readers to double-announce: "button, menuitem, Edit."

// Bad: double announcement in some screen readers
<button role="menuitem">Edit</button>

// Good: single role, programmatically focusable
<div role="menuitem" tabIndex={-1}>Edit</div>

dialog-requires-title

role="dialog" or role="alertdialog" must have aria-labelledby or aria-label. Without a name, screen readers announce "dialog" with no context about its purpose.

// Bad: "dialog" with no context
<div role="dialog" aria-modal="true">Are you sure?</div>

// Good: "Confirm deletion, dialog"
<div role="dialog" aria-modal="true" aria-labelledby="title">
  <h2 id="title">Confirm deletion</h2>
</div>

General interaction rules

These catch common accessibility mistakes in everyday React code. They fire on patterns every developer writes.

focusable-has-interaction

Elements with tabIndex={0} must have a keyboard event handler (onKeyDown, onKeyUp, or onKeyPress). Making an element focusable implies it's interactive. Without a keyboard handler, it's a dead end in the Tab sequence.

// Bad: reachable by Tab but inert
<div tabIndex={0}>Card</div>

// Good: keyboard interaction supported
<div tabIndex={0} onKeyDown={handleKeyDown}>Card</div>

input-requires-label

<input>, <select>, and <textarea> must have an accessible label via aria-label, aria-labelledby, or id (implying a <label htmlFor> association). Placeholder text is not a label.

// Bad: screen reader says "edit text" with no context
<input type="text" placeholder="Enter name" />

// Good: screen reader says "Full name, edit text"
<input type="text" aria-label="Full name" />

radio-group-requires-grouping

<input type="radio"> must be inside a <fieldset> or an element with role="radiogroup". Without grouping, screen readers announce each radio independently with no indication they form a set.

// Bad: "radio button, Red" then "radio button, Blue" (no relationship)
<div>
  <input type="radio" name="color" value="red" /> Red
  <input type="radio" name="color" value="blue" /> Blue
</div>

// Good: "Color, group. Radio button, Red. Radio button, Blue."
<fieldset>
  <legend>Color</legend>
  <input type="radio" name="color" value="red" /> Red
  <input type="radio" name="color" value="blue" /> Blue
</fieldset>

no-positive-tabindex

tabIndex must not be greater than 0. Positive values override the natural DOM tab order, creating unpredictable keyboard navigation. jsx-a11y has this as a warning. This plugin makes it an error because there is no legitimate use case.

// Bad: receives focus before all tabIndex={0} elements
<div tabIndex={5}>Out of order</div>

// Good: focusable in DOM order
<div tabIndex={0} onKeyDown={handleKey}>In order</div>

Why these rules exist

Over 5,000 ADA digital accessibility lawsuits were filed in 2025 across federal and state courts, up from roughly 4,000 in 2024 (UsableNet 2025 Year-End Report). The European Accessibility Act started enforcement on June 28, 2025. India's Supreme Court declared digital access a fundamental right under Article 21 in April 2025, and SEBI mandated WCAG compliance for the financial sector in July 2025.

The composition errors this plugin catches — mismatched ARIA relationships, missing modal semantics, unlabeled dialogs, broken focus patterns — are among the most common findings in accessibility audits.

Your linter should catch these before they ship. jsx-a11y catches the element-level issues. a11y-enforce catches the composition-level issues.

Design decisions

  • Zero runtime dependencies. The plugin uses only ESLint's built-in AST APIs.
  • ESLint 8 and 9 support. Rules are version-agnostic. Only the config export format differs.
  • Educational error messages. Every violation includes what is wrong, why it matters for users, and how to fix it.
  • Single recommended preset. All 10 rules as errors. No strict/recommended split until real-world usage data justifies one.
  • Complements jsx-a11y. No rule overlap. Install both.

Stats

  • 10 rules (6 component pattern, 4 general interaction)
  • 207 tests
  • Zero runtime dependencies
  • ESM + CJS dual output
  • TypeScript source with full type safety

License

MIT