@servicenow/eslint-plugin-aiux-a11y
v0.4.0
Published
ESLint rules for AIUX accessibility conventions in Lit templates. Static-analysis companion to the runtime accessibility audit panel.
Readme
@servicenow/eslint-plugin-aiux-a11y
ESLint rules for AIUX accessibility conventions in Lit templates. Catches WCAG 2.2 violations that are statically detectable. Static-analysis companion to the runtime accessibility audit panel (components/audit-panel).
Install
pnpm add -D @servicenow/eslint-plugin-aiux-a11yUsage
import aiuxA11y from '@servicenow/eslint-plugin-aiux-a11y';
export default [
...aiuxA11y.recommended
// or build your own using the rules export:
// {
// plugins: {'@servicenow/aiux-a11y': {rules: aiuxA11y.rules}},
// rules: {
// '@servicenow/aiux-a11y/button-needs-accessible-name': 'error',
// '@servicenow/aiux-a11y/img-needs-alt': 'error',
// '@servicenow/aiux-a11y/interactive-element-needs-role': 'error',
// '@servicenow/aiux-a11y/role-needs-accessible-name': 'error',
// '@servicenow/aiux-a11y/aria-prohibited-attr': 'error',
// '@servicenow/aiux-a11y/select-needs-accessible-name': 'error',
// },
// },
];Named imports work too:
import {recommended, rules} from '@servicenow/eslint-plugin-aiux-a11y';
export default [...recommended];Rules
button-needs-accessible-name
WCAG 2.2 Success Criterion 4.1.2 (Name, Role, Value). Flags <button> elements that have no programmatic name — no text content, no aria-label, no aria-labelledby, no title.
// ✗ Icon-only button (the most common a11y bug)
return html`<button><aiux-icon name="close" /></button>`;
// ✗ Empty button
return html`<button></button>`;
// ✓ Text content
return html`<button>Save</button>`;
// ✓ Icon button with aria-label
return html`<button aria-label="Close dialog">
<aiux-icon name="close" />
</button>`;
// ✓ Interpolated children — trusted (static analysis can't see the value)
return html`<button>${label}</button>`;
// ✓ Opt-out
return html`<button data-a11y-skip><aiux-icon name="x" /></button>`;Conservative by design — the rule only flags buttons whose inner content the linter can fully see and prove is empty (or icon-only). Buttons whose children include ${...} interpolations or aria-label=${expr} bindings are trusted.
img-needs-alt
WCAG 2.2 Success Criterion 1.1.1 (Non-text Content). Flags <img> elements without an alt attribute (or an equivalent decorative-image signal).
// ✗
return html`<img src="hero.png" />`;
// ✓ Meaningful description
return html`<img src="hero.png" alt="Welcome dashboard" />`;
// ✓ Decorative image (screen readers skip it)
return html`<img src="flourish.png" alt="" />`;
// ✓ Alternative ways to mark decorative
return html`<img src="x.png" role="presentation" />`;
return html`<img src="x.png" aria-hidden="true" />`;
// ✓ Interpolated alt — trusted
return html`<img src="x.png" alt=${altText} />`;interactive-element-needs-role
WCAG 2.2 Success Criterion 4.1.2 (Name, Role, Value). Flags non-interactive elements (<div>, <span>, <li>, etc.) that have Lit event bindings (@click, @keydown, etc.) but no role attribute.
// ✗ Clickable div without role (invisible to screen readers)
return html`<div @click=${handler}>Click me</div>`;
// ✗ Clickable li without role
return html`<li @click=${handler} tabindex="0">Item</li>`;
// ✓ Clickable div with role
return html`<div role="button" @click=${handler}>Click me</div>`;
// ✓ Native interactive element — no role needed
return html`<button @click=${handler}>Save</button>`;
// ✓ Interpolated role — trusted
return html`<div role=${role} @click=${handler}>X</div>`;role-needs-accessible-name
WCAG 2.2 SC 1.1.1 and 4.1.2. Flags elements with ARIA widget roles (progressbar, slider, dialog, meter, toolbar, img, region, etc.) that have no accessible name.
// ✗ Progressbar without label
return html`<div role="progressbar" aria-valuenow="50"></div>`;
// ✗ Dialog without label
return html`<div role="dialog" class="modal"></div>`;
// ✓ Progressbar with aria-label
return html`<div
role="progressbar"
aria-label="Upload progress"
aria-valuenow="50"
></div>`;
// ✓ Dialog with aria-labelledby
return html`<div role="dialog" aria-labelledby="dialog-title"></div>`;
// ✓ Interpolated aria-label — trusted
return html`<div role="progressbar" aria-label=${label}></div>`;aria-prohibited-attr
WCAG 2.2 SC 4.1.2. Flags aria-label and aria-labelledby on elements with a generic implicit ARIA role (<span>, <div>, <p>, etc. without an explicit role). These attributes have no effect on generic-role elements and are an authoring error.
// ✗ span with aria-label but no role (label is ignored)
return html`<span aria-label="Status">Active</span>`;
// ✗ div with aria-label but no role
return html`<div aria-label="Container">content</div>`;
// ✓ span with role — naming is allowed
return html`<span role="status" aria-label="Active">Active</span>`;
// ✓ button — natively nameable
return html`<button aria-label="Close">×</button>`;select-needs-accessible-name
WCAG 2.2 SC 4.1.2. Flags <select>, <input>, and <textarea> elements without an accessible name (aria-label, aria-labelledby, title, or an id that may reference a <label>).
// ✗ Select without any label
return html`<select class="page-size"></select>`;
// ✗ Input with only placeholder (not an accessible name)
return html`<input type="text" placeholder="Search..." />`;
// ✓ Select with aria-label
return html`<select aria-label="Page size"></select>`;
// ✓ Input with id (may have associated label)
return html`<input type="text" id="username" />`;
// ✓ Hidden input — doesn't need a label
return html`<input type="hidden" name="csrf" />`;Pairing with the runtime audit panel
The audit panel walks the live DOM checking the same things. Use the linter to catch issues in the editor / on CI; use the audit panel for spot-checks in the browser. Same gaps, two surfaces.
Pairing with Evinced
The Evinced MCP Server performs runtime WCAG scanning against the live rendered page. Use the lint rules to catch statically-detectable issues early; use Evinced for runtime issues like color contrast that require computed styles.
