You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
|[template-no-aria-hidden-body](docs/rules/template-no-aria-hidden-body.md)| disallow aria-hidden on body element | 📋 | 🔧 ||
265
-
|[template-no-aria-unsupported-elements](docs/rules/template-no-aria-unsupported-elements.md)| disallow ARIA roles, states, and properties on elements that do not support them | 📋 |||
|[template-no-redundant-role](docs/rules/template-no-redundant-role.md)| disallow redundant role attributes | 📋 | 🔧 ||
280
-
|[template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md)| disallow ARIA attributes that are not supported by the element role | 📋 | 🔧 ||
281
-
|[template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md)| disallow excess whitespace within words (e.g. "W e l c o m e") | 📋 |||
282
-
|[template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md)| require non-interactive elements with aria-activedescendant to have tabindex | 📋 | 🔧 ||
283
-
|[template-require-context-role](docs/rules/template-require-context-role.md)| require ARIA roles to be used in appropriate context | 📋 |||
284
-
|[template-require-iframe-title](docs/rules/template-require-iframe-title.md)| require iframe elements to have a title attribute | 📋 |||
285
-
|[template-require-input-label](docs/rules/template-require-input-label.md)| require label for form input elements | 📋 |||
286
-
|[template-require-lang-attribute](docs/rules/template-require-lang-attribute.md)| require lang attribute on html element | 📋 |||
|[template-require-media-caption](docs/rules/template-require-media-caption.md)| require captions for audio and video elements | 📋 |||
289
-
|[template-require-presentational-children](docs/rules/template-require-presentational-children.md)| require presentational elements to only contain presentational children | 📋 |||
290
-
|[template-require-valid-alt-text](docs/rules/template-require-valid-alt-text.md)| require valid alt text for images and other elements | 📋 |||
291
-
|[template-require-valid-form-groups](docs/rules/template-require-valid-form-groups.md)| require grouped form controls to have fieldset/legend or WAI-ARIA group labeling ||||
292
-
|[template-table-groups](docs/rules/template-table-groups.md)| require table elements to use table grouping elements | 📋 |||
|[template-link-href-attributes](docs/rules/template-link-href-attributes.md)| require href attribute on link elements | 📋 |||
262
+
|[template-mouse-events-have-key-events](docs/rules/template-mouse-events-have-key-events.md)| require mouseover/mouseout to be accompanied by focus/blur (or focusin/focusout) for keyboard-only users ||||
|[template-no-aria-hidden-body](docs/rules/template-no-aria-hidden-body.md)| disallow aria-hidden on body element | 📋 | 🔧 ||
266
+
|[template-no-aria-unsupported-elements](docs/rules/template-no-aria-unsupported-elements.md)| disallow ARIA roles, states, and properties on elements that do not support them | 📋 |||
|[template-no-redundant-role](docs/rules/template-no-redundant-role.md)| disallow redundant role attributes | 📋 | 🔧 ||
281
+
|[template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md)| disallow ARIA attributes that are not supported by the element role | 📋 | 🔧 ||
282
+
|[template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md)| disallow excess whitespace within words (e.g. "W e l c o m e") | 📋 |||
283
+
|[template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md)| require non-interactive elements with aria-activedescendant to have tabindex | 📋 | 🔧 ||
284
+
|[template-require-context-role](docs/rules/template-require-context-role.md)| require ARIA roles to be used in appropriate context | 📋 |||
285
+
|[template-require-iframe-title](docs/rules/template-require-iframe-title.md)| require iframe elements to have a title attribute | 📋 |||
286
+
|[template-require-input-label](docs/rules/template-require-input-label.md)| require label for form input elements | 📋 |||
287
+
|[template-require-lang-attribute](docs/rules/template-require-lang-attribute.md)| require lang attribute on html element | 📋 |||
|[template-require-media-caption](docs/rules/template-require-media-caption.md)| require captions for audio and video elements | 📋 |||
290
+
|[template-require-presentational-children](docs/rules/template-require-presentational-children.md)| require presentational elements to only contain presentational children | 📋 |||
291
+
|[template-require-valid-alt-text](docs/rules/template-require-valid-alt-text.md)| require valid alt text for images and other elements | 📋 |||
292
+
|[template-require-valid-form-groups](docs/rules/template-require-valid-form-groups.md)| require grouped form controls to have fieldset/legend or WAI-ARIA group labeling ||||
293
+
|[template-table-groups](docs/rules/template-table-groups.md)| require table elements to use table grouping elements | 📋 |||
Enforce that `{{on "mouseover" …}}` is accompanied by `{{on "focus" …}}` / `{{on "focusin" …}}`, and `{{on "mouseout" …}}` by `{{on "blur" …}}` / `{{on "focusout" …}}`. `{{on "mouseenter" …}}` / `{{on "mouseleave" …}}` are NOT checked by default — opt in via `hoverInHandlers` / `hoverOutHandlers` options (see below).
6
+
7
+
Keyboard-only users can't trigger mouse events. Pairing hover-in events with focus events (and hover-out events with blur events) ensures the same UI state transitions happen for keyboard navigation.
8
+
9
+
## On "normative basis"
10
+
11
+
[WCAG 2.1 SC 2.1.1 Keyboard (Level A)](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) requires all functionality to be operable via the keyboard. Pointer-only UI transitions (hover effects that show/hide content, highlight rows, etc.) don't satisfy this when no keyboard equivalent exists. However, this rule's specific "hover event + focus event pairing" heuristic isn't literally mandated by the SC — [Understanding 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) allows any keyboard path. The event-pairing convention comes from [WAI-ARIA APG keyboard-interaction guidance](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/) (authoring guidance, not normative), and from all four peer a11y plugins adopting it as the strongest static-analysis proxy. This rule follows the convention.
12
+
13
+
For many simple hover effects the cleaner fix is a CSS `:hover` + `:focus` combined selector rather than paired JS handlers — [Inclusive Components: Tooltips](https://inclusive-components.design/tooltips-toggletips/) is the canonical reference.
-`hoverInHandlers` (default `["mouseover"]`) — which events require a focus pair. Matches jsx-a11y's default. Add `"mouseenter"` to also check the non-bubbling per-element variant.
38
+
-`hoverOutHandlers` (default `["mouseout"]`) — which events require a blur pair. Matches jsx-a11y's default. Add `"mouseleave"` to also check the non-bubbling per-element variant.
39
+
40
+
### Why are `mouseenter` / `mouseleave` opt-in?
41
+
42
+
`mouseenter`/`mouseleave` don't bubble — they fire once on entry/exit of the bound element, never on transitions between children. Authors frequently choose them specifically because they want a per-element effect (highlight one row, show one tooltip) that doesn't fire for every child element transition. Those effects are often cleaner to express with CSS `:hover` + `:focus` combined selectors than paired JS handlers. Flagging `mouseenter`/`mouseleave` by default therefore produces noisy false positives on a common authoring pattern. We default to jsx-a11y's narrower handler set; opt in when your project wants the wider check.
43
+
44
+
## References
45
+
46
+
-[WAI-ARIA APG — Keyboard Interaction](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/)
0 commit comments