array(
'title' => __( 'Image has no alt attribute', 'adinav-accessibility-audit-eaa' ),
'severity' => self::CRITICAL,
'wcag' => '1.1.1',
'level' => 'A',
'en301549' => '9.1.1.1',
'why' => __( 'A screen reader has nothing to announce, so it falls back to reading the file name — or says nothing at all. If the image is a product photo, the product becomes invisible.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Add alt text describing what the image conveys: . If the image is purely decorative, use an explicitly empty alt="" so assistive technology skips it. An absent alt and an empty alt mean completely different things.', 'adinav-accessibility-audit-eaa' ),
),
'img-alt-is-filename' => array(
'title' => __( 'Alt text is a file name', 'adinav-accessibility-audit-eaa' ),
'severity' => self::SERIOUS,
'wcag' => '1.1.1',
'level' => 'A',
'en301549' => '9.1.1.1',
'why' => __( 'Alt text like "IMG_4021.jpg" is technically present and completely useless. Automated compliance checkers score it as a pass; a blind user gets nothing.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Replace the file name with a description of what the image communicates in this context.', 'adinav-accessibility-audit-eaa' ),
),
'input-missing-label' => array(
'title' => __( 'Form field has no label', 'adinav-accessibility-audit-eaa' ),
'severity' => self::CRITICAL,
'wcag' => '1.3.1, 3.3.2',
'level' => 'A',
'en301549' => '9.1.3.1, 9.3.3.2',
'why' => __( 'The user is told there is a text box, but not what to type into it. On a checkout form this ends the sale. Placeholder text does not count: it disappears the moment typing starts, and many screen readers ignore it.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Give the field an id and point a label at it: Email . A wrapping , an aria-label, or an aria-labelledby all work too. A placeholder alone does not.', 'adinav-accessibility-audit-eaa' ),
),
'control-no-accessible-name' => array(
'title' => __( 'Button or link has no accessible name', 'adinav-accessibility-audit-eaa' ),
'severity' => self::CRITICAL,
'wcag' => '2.4.4, 4.1.2',
'level' => 'A',
'en301549' => '9.2.4.4, 9.4.1.2',
'why' => __( 'An icon-only control — a bare SVG cart, a hamburger menu, an × to close — announces itself as "button" and nothing else. The user is asked to guess. This is the single most common blocker on modern e-commerce themes.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Add aria-label="Close" (or visually hidden text) to the control. If it wraps an image, give the image real alt text. If it wraps an , add role="img" and an aria-label, or a as the SVG\'s first child.', 'adinav-accessibility-audit-eaa' ),
),
'link-generic-text' => array(
'title' => __( 'Link text says nothing out of context', 'adinav-accessibility-audit-eaa' ),
'severity' => self::MODERATE,
'wcag' => '2.4.4',
'level' => 'A',
'en301549' => '9.2.4.4',
'why' => __( 'Screen-reader users often navigate by listing every link on the page. Twenty entries that all say "Read more" is a dead end.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Make the link text describe its destination — "Read more about our returns policy". If the visible text must stay short, add an aria-label carrying the full meaning.', 'adinav-accessibility-audit-eaa' ),
),
'html-missing-lang' => array(
'title' => __( 'Page does not declare its language', 'adinav-accessibility-audit-eaa' ),
'severity' => self::SERIOUS,
'wcag' => '3.1.1',
'level' => 'A',
'en301549' => '9.3.1.1',
'why' => __( 'The screen reader does not know which voice to use, so French text is read aloud with English pronunciation rules. The result is close to unintelligible.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Set the language on the root element: . In WordPress this is normally emitted by language_attributes() in header.php — a theme that hard-codes will have dropped it.', 'adinav-accessibility-audit-eaa' ),
),
'missing-page-title' => array(
'title' => __( 'Page has no title', 'adinav-accessibility-audit-eaa' ),
'severity' => self::SERIOUS,
'wcag' => '2.4.2',
'level' => 'A',
'en301549' => '9.2.4.2',
'why' => __( 'The title is the first thing announced on load and the only way to tell twenty open tabs apart.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Ensure is present and unique per page. In WordPress, confirm the theme declares add_theme_support( \'title-tag\' ).', 'adinav-accessibility-audit-eaa' ),
),
'heading-level-skipped' => array(
'title' => __( 'Heading level skipped', 'adinav-accessibility-audit-eaa' ),
'severity' => self::MODERATE,
'wcag' => '1.3.1',
'level' => 'A',
'en301549' => '9.1.3.1',
'why' => __( 'Headings are the table of contents a screen-reader user navigates by. Jumping from h2 straight to h4 implies a missing section, and the user goes looking for content that is not there.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Descend one level at a time. Heading level is structure, not styling — if a heading looks too big, change the CSS, not the tag.', 'adinav-accessibility-audit-eaa' ),
),
'empty-heading' => array(
'title' => __( 'Heading is empty', 'adinav-accessibility-audit-eaa' ),
'severity' => self::MODERATE,
'wcag' => '1.3.1',
'level' => 'A',
'en301549' => '9.1.3.1',
'why' => __( 'An empty heading appears in the navigation list as a blank entry that leads nowhere. It is usually a page-builder artefact.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Remove the empty heading, or give it text. If it exists purely for spacing, delete it and use CSS margin.', 'adinav-accessibility-audit-eaa' ),
),
'no-h1' => array(
'title' => __( 'Page has no h1', 'adinav-accessibility-audit-eaa' ),
'severity' => self::MODERATE,
'wcag' => '1.3.1, 2.4.6',
'level' => 'A',
'en301549' => '9.1.3.1',
'why' => __( 'The h1 states what this page is. Without one, a screen-reader user landing from a search result has no anchor.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Give every page exactly one h1 carrying its main subject.', 'adinav-accessibility-audit-eaa' ),
),
'iframe-no-title' => array(
'title' => __( 'iframe has no title', 'adinav-accessibility-audit-eaa' ),
'severity' => self::SERIOUS,
'wcag' => '4.1.2',
'level' => 'A',
'en301549' => '9.4.1.2',
'why' => __( 'The user is dropped into an unnamed frame with no idea what it contains — a map, a video, a payment form, an advert.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Add title="Checkout payment form" (or similar) to the iframe. If the frame is genuinely decorative and empty, add aria-hidden="true" and tabindex="-1".', 'adinav-accessibility-audit-eaa' ),
),
'table-no-headers' => array(
'title' => __( 'Data table has no header cells', 'adinav-accessibility-audit-eaa' ),
'severity' => self::SERIOUS,
'wcag' => '1.3.1',
'level' => 'A',
'en301549' => '9.1.3.1',
'why' => __( 'Without , a screen reader reads the cells as a flat run of values. In a size or pricing table the numbers arrive with nothing attached to them.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Use and . If the table is only there for visual layout, mark it role="presentation" — then this rule will correctly ignore it.', 'adinav-accessibility-audit-eaa' ),
),
'duplicate-id' => array(
'title' => __( 'Duplicate id attribute', 'adinav-accessibility-audit-eaa' ),
'severity' => self::MODERATE,
'wcag' => '4.1.1',
'level' => 'A',
'en301549' => '9.4.1.1',
'why' => __( 'label[for], aria-labelledby and aria-describedby all resolve an id to exactly one element. When the id is duplicated, the association silently binds to the wrong element.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Make every id unique on the page. Duplicates usually come from a widget or block rendered more than once.', 'adinav-accessibility-audit-eaa' ),
),
'positive-tabindex' => array(
'title' => __( 'Positive tabindex', 'adinav-accessibility-audit-eaa' ),
'severity' => self::MODERATE,
'wcag' => '2.4.3',
'level' => 'A',
'en301549' => '9.2.4.3',
'why' => __( 'A positive tabindex rips the element out of the natural tab order and jumps it to the front, so keyboard focus leaps around the page in an order that matches nothing the user can see.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Use tabindex="0" (focusable, natural order) or tabindex="-1" (focusable only by script). Never a positive number. If the tab order is wrong, fix the DOM order instead.', 'adinav-accessibility-audit-eaa' ),
),
'aria-hidden-focusable' => array(
'title' => __( 'Focusable element hidden from assistive technology', 'adinav-accessibility-audit-eaa' ),
'severity' => self::SERIOUS,
'wcag' => '1.3.1, 4.1.2',
'level' => 'A',
'en301549' => '9.1.3.1, 9.4.1.2',
'why' => __( 'aria-hidden="true" removes the element from the accessibility tree but not from the tab order. A keyboard user tabs into a control that, as far as the screen reader is concerned, does not exist. Focus vanishes.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Either remove aria-hidden, or make the contents genuinely unfocusable (tabindex="-1", or the inert attribute).', 'adinav-accessibility-audit-eaa' ),
),
'viewport-zoom-disabled' => array(
'title' => __( 'Pinch-to-zoom is disabled', 'adinav-accessibility-audit-eaa' ),
'severity' => self::SERIOUS,
'wcag' => '1.4.4',
'level' => 'AA',
'en301549' => '9.1.4.4',
'why' => __( 'user-scalable=no forbids a partially sighted person from zooming in on their phone. It is a one-line meta tag that locks out an entire class of user, and it is still shipped by default in many themes.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Use . Remove user-scalable=no and any maximum-scale below 2.', 'adinav-accessibility-audit-eaa' ),
),
'meta-refresh' => array(
'title' => __( 'Page auto-refreshes or redirects on a timer', 'adinav-accessibility-audit-eaa' ),
'severity' => self::SERIOUS,
'wcag' => '2.2.1',
'level' => 'A',
'en301549' => '9.2.2.1',
'why' => __( 'The page moves out from under the user while they are still reading it. Anyone who reads slowly, or with a screen reader, loses their place with no way to stop it.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Remove the meta refresh. If a redirect is needed, do it server-side (HTTP 301/302) so it is instant rather than timed.', 'adinav-accessibility-audit-eaa' ),
),
'video-no-captions' => array(
'title' => __( 'Video has no captions track', 'adinav-accessibility-audit-eaa' ),
'severity' => self::SERIOUS,
'wcag' => '1.2.2',
'level' => 'A',
'en301549' => '9.1.2.2',
'why' => __( 'A deaf or hard-of-hearing visitor gets moving pictures and silence. If the video explains the product, they do not get the product.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Add inside the . Auto-generated captions must be corrected — WCAG requires accurate captions, not approximate ones.', 'adinav-accessibility-audit-eaa' ),
),
'media-autoplays' => array(
'title' => __( 'Audio or video plays automatically', 'adinav-accessibility-audit-eaa' ),
'severity' => self::MODERATE,
'wcag' => '1.4.2',
'level' => 'A',
'en301549' => '9.1.4.2',
'why' => __( 'Sound that starts by itself plays straight over the top of a screen reader\'s own speech. The user cannot hear the interface well enough to find the control that would stop it.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Do not autoplay with sound. If autoplay is required for a background video, add the muted attribute; otherwise expose a visible pause/stop control within the first tab stop.', 'adinav-accessibility-audit-eaa' ),
),
'autocomplete-missing' => array(
'title' => __( 'Personal-data field has no autocomplete', 'adinav-accessibility-audit-eaa' ),
'severity' => self::MODERATE,
'wcag' => '1.3.5',
'level' => 'AA',
'en301549' => '9.1.3.5',
'why' => __( 'The autocomplete token tells the browser (and assistive tech) what this field means, so it can be filled automatically. For someone with a motor impairment or a memory impairment, retyping a full address by hand at checkout is the reason the order is abandoned.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Add the standard token: autocomplete="email", "tel", "given-name", "family-name", "street-address", "postal-code", "country-name". This is a WCAG 2.1 AA criterion — it is inside the EAA\'s required level, and it is almost universally missed.', 'adinav-accessibility-audit-eaa' ),
),
'no-main-landmark' => array(
'title' => __( 'Page has no main landmark', 'adinav-accessibility-audit-eaa' ),
'severity' => self::MODERATE,
'wcag' => '1.3.1, 2.4.1',
'level' => 'A',
'en301549' => '9.1.3.1, 9.2.4.1',
'why' => __( 'Screen-reader users jump straight to to skip the header and navigation. Without it, they tab through the entire menu on every single page.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Wrap the page content in (or role="main"), exactly once per page.', 'adinav-accessibility-audit-eaa' ),
),
'contrast-inline-fail' => array(
'title' => __( 'Text contrast below 4.5:1 (inline styles)', 'adinav-accessibility-audit-eaa' ),
'severity' => self::SERIOUS,
'wcag' => '1.4.3',
'level' => 'AA',
'en301549' => '9.1.4.3',
'why' => __( 'Low-contrast text is unreadable for older eyes, for anyone outdoors on a phone, and for the roughly 1 in 12 men with a colour-vision deficiency.', 'adinav-accessibility-audit-eaa' ),
'fix' => __( 'Raise the contrast to at least 4.5:1 for normal text, or 3:1 for text at 18.66px bold / 24px and above.', 'adinav-accessibility-audit-eaa' ),
'note' => __( 'Partial coverage. This check runs only where a colour and a background colour are both set in the same inline style, which it can verify with certainty. Contrast that comes from a stylesheet cannot be judged without rendering the page, and we will not guess at a legal obligation. Treat a clean result here as "no proof of failure", not as "contrast passes".', 'adinav-accessibility-audit-eaa' ),
),
);
}
public static function get( $id ) {
$all = self::definitions();
return isset( $all[ $id ] ) ? $all[ $id ] : null;
}
public static function severity_rank( $severity ) {
$order = array(
self::CRITICAL => 0,
self::SERIOUS => 1,
self::MODERATE => 2,
self::ADVISORY => 3,
);
return isset( $order[ $severity ] ) ? $order[ $severity ] : 9;
}
}