Max Design

Published:

This article follows on from Using the browser console for accessibility testing.

The examples below show how simple console scripts can help identify accessibility features and potential issues directly in the browser.

You can use the testing page to try each script as you work through the examples.

1. Highlight aria-invalid="true"

What it does

Finds all elements where aria-invalid="true" has been applied and highlights them with a badge.

Why it is important

When a form field contains an error, users need to know. Sighted users might see a red border or an error message, but screen reader users rely on aria-invalid="true" to identify fields that have failed validation.

This script helps confirm that the invalid state is being exposed programmatically.

Caveats

The script only targets the explicit value "true". It will not flag aria-invalid="spelling" or aria-invalid="grammar", which are also valid values.

Usage

Run the script after triggering validation errors on the page. If no validation errors have been triggered, elements may not yet have aria-invalid="true" applied.

View the script
// ----------------------
// Scroll-safe overlay container
// ----------------------
const overlay = document.createElement("div");
overlay.style.position = "absolute";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.pointerEvents = "none";
overlay.style.zIndex = "999999";
document.body.appendChild(overlay);

// ----------------------
// Main logic
// ----------------------
document.querySelectorAll("[aria-invalid='true']").forEach(el => {

  // Outline the element
  el.style.outline = "3px solid #b00020";

  // Compute position
  const rect = el.getBoundingClientRect();
  const left = rect.left + window.scrollX;
  const top = rect.top + window.scrollY;

  // Floating badge
  const badge = document.createElement("div");
  badge.textContent = "aria-invalid=\"true\"";
  badge.style.position = "absolute";
  badge.style.left = `${left}px`;
  badge.style.top = `${top - 30}px`;
  badge.style.background = "#b00020";
  badge.style.color = "white";
  badge.style.padding = "5px";
  badge.style.borderRadius = "5px";
  badge.style.fontSize = "16px";
  badge.style.whiteSpace = "nowrap";
  badge.style.pointerEvents = "none";
  badge.style.zIndex = "999999";

  overlay.appendChild(badge);
});

2. Highlight required and aria-required="true"

What it does

Finds all form fields using either the native HTML required attribute or aria-required="true" and highlights them in two different colours to distinguish the two approaches.

Why it is important

Users need to know which fields are required before submitting a form. The required and aria-required="true" attributes communicate this information through the accessibility tree, allowing screen readers and other assistive technologies to announce that a field is required.

This script helps testers quickly identify which approach has been used and whether required fields have been marked consistently across a page.

Caveats

The two approaches are similar, but not identical. The native required attribute also triggers built-in browser validation, while aria-required="true" does not.

Usage

If a field has both required and aria-required="true" applied, it will receive two badges. In most cases this is unnecessary, as the native required attribute already exposes the required state to assistive technologies.

View the script
// ----------------------
// Scroll-safe overlay container
// ----------------------
const overlay = document.createElement("div");
overlay.style.position = "absolute";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.pointerEvents = "none";
overlay.style.zIndex = "999999";
document.body.appendChild(overlay);

// ----------------------
// Colour map
// ----------------------
const colours = {
  required:      "#b05a00",
  ariaRequired:  "#6a1b9a"
};

// ----------------------
// Helper: draw badge
// ----------------------
function drawBadge(el, label, colour) {

  // Outline the element
  el.style.outline = `3px solid ${colour}`;

  // Compute position
  const rect = el.getBoundingClientRect();
  const left = rect.left + window.scrollX;
  const top = rect.top + window.scrollY;

  // Floating badge
  const badge = document.createElement("div");
  badge.textContent = label;
  badge.style.position = "absolute";
  badge.style.left = `${left}px`;
  badge.style.top = `${top - 30}px`;
  badge.style.background = colour;
  badge.style.color = "white";
  badge.style.padding = "5px";
  badge.style.borderRadius = "5px";
  badge.style.fontSize = "16px";
  badge.style.whiteSpace = "nowrap";
  badge.style.pointerEvents = "none";
  badge.style.zIndex = "999999";

  overlay.appendChild(badge);
}

// ----------------------
// Main logic
// ----------------------

// 1. HTML required attribute
document.querySelectorAll("[required]").forEach(el => {
  drawBadge(el, "required", colours.required);
});

// 2. aria-required="true"
document.querySelectorAll("[aria-required='true']").forEach(el => {
  drawBadge(el, "aria-required=\"true\"", colours.ariaRequired);
});

3. Highlight aria-hidden="true"

What it does

Finds all elements where aria-hidden="true" has been applied and highlights them with a badge.

Why it is important

aria-hidden="true" removes content from the accessibility tree while leaving it visible on the page.

This means that screen reader users cannot access the content.

This is sometimes intentional. For example, authors may hide decorative icons that provide no useful information. However, it can also create accessibility issues.

Hidden content may create a completely different experience for screen reader users, including:

This script helps identify content that has been hidden from assistive technologies.

Caveats

The presence of aria-hidden="true" is not automatically an error.

Usage

Review each flagged element and ask: should this content be available to screen reader users?

Also check whether any flagged element contains focusable children. See the footnotes for more information.

View the script
// ----------------------
// Scroll-safe overlay container
// ----------------------
const overlay = document.createElement("div");
overlay.style.position = "absolute";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.pointerEvents = "none";
overlay.style.zIndex = "999999";
document.body.appendChild(overlay);

// ----------------------
// Main logic
// ----------------------
document.querySelectorAll("[aria-hidden='true']").forEach(el => {

  // Outline the element
  el.style.outline = "3px solid #b00020";

  // Compute position
  const rect = el.getBoundingClientRect();
  const left = rect.left + window.scrollX;
  const top = rect.top + window.scrollY;

  // Floating badge
  const badge = document.createElement("div");
  badge.textContent = "aria-hidden=\"true\"";
  badge.style.position = "absolute";
  badge.style.left = `${left}px`;
  badge.style.top = `${top - 30}px`;
  badge.style.background = "#b00020";
  badge.style.color = "white";
  badge.style.padding = "5px";
  badge.style.borderRadius = "5px";
  badge.style.fontSize = "16px";
  badge.style.whiteSpace = "nowrap";
  badge.style.pointerEvents = "none";
  badge.style.zIndex = "999999";

  overlay.appendChild(badge);
});

4. Highlight aria-expanded

What it does

Finds all elements with an aria-expanded attribute and highlights them, using green for "true" and orange for "false" to show the current state.

Why it is important

aria-expanded communicates whether a component is open or closed to screen reader users.

Common examples include:

Without this information, users may activate a control but have no indication whether anything expanded or collapsed.

This script helps testers quickly identify elements using aria-expanded and confirm whether the current state is being exposed correctly.

Caveats

This script only captures the state of the page at the moment it runs.

If you expand or collapse components after running the script, the badges will not update automatically. Run the script again to see the current state.

Usage

Run the script and review any highlighted elements.

Expand and collapse components, then run the script again to confirm that the aria-expanded value changes between "true" and "false" as expected.

View the script
// ----------------------
// Scroll-safe overlay container
// ----------------------
const overlay = document.createElement("div");
overlay.style.position = "absolute";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.pointerEvents = "none";
overlay.style.zIndex = "999999";
document.body.appendChild(overlay);

// ----------------------
// Colour map
// ----------------------
const colours = {
  expanded:   "#1b5e20",
  collapsed:  "#b05a00"
};

// ----------------------
// Main logic
// ----------------------
document.querySelectorAll("[aria-expanded]").forEach(el => {

  const value = el.getAttribute("aria-expanded").trim().toLowerCase();
  const isExpanded = value === "true";
  const colour = isExpanded ? colours.expanded : colours.collapsed;
  const label = isExpanded ? "aria-expanded=\"true\"" : "aria-expanded=\"false\"";

  // Outline the element
  el.style.outline = `3px solid ${colour}`;

  // Compute position
  const rect = el.getBoundingClientRect();
  const left = rect.left + window.scrollX;
  const top = rect.top + window.scrollY;

  // Floating badge
  const badge = document.createElement("div");
  badge.textContent = label;
  badge.style.position = "absolute";
  badge.style.left = `${left}px`;
  badge.style.top = `${top - 30}px`;
  badge.style.background = colour;
  badge.style.color = "white";
  badge.style.padding = "5px";
  badge.style.borderRadius = "5px";
  badge.style.fontSize = "16px";
  badge.style.whiteSpace = "nowrap";
  badge.style.pointerEvents = "none";
  badge.style.zIndex = "999999";

  overlay.appendChild(badge);
});

5. Highlight tabindex

What it does

Finds all elements with a tabindex attribute and highlights them in three colours:

Why it is important

Keyboard users rely on a logical and predictable focus order when navigating a page.

The tabindex attribute can be used to manage keyboard focus, but it can also create accessibility issues when used incorrectly. This script helps testers quickly identify where tabindex has been applied and assess whether it is being used appropriately.

Usage

Review the highlighted elements and check how each tabindex value is being used:

Green (tabindex="-1")

The element can receive focus programmatically but is not included in the normal tab order.

Common uses include:

Check that focus is being sent to these elements at the appropriate time.

Blue (tabindex="0")

The element has been added to the normal tab order.

This may be appropriate for custom widgets, but it is worth checking whether a native interactive element could be used instead.

Red (positive value)

The element has been given a specific position in the tab order.

This is almost always a mistake because it overrides the natural document order and can create a confusing keyboard experience.

View the script
// ----------------------
// Scroll-safe overlay container
// ----------------------
const overlay = document.createElement("div");
overlay.style.position = "absolute";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.pointerEvents = "none";
overlay.style.zIndex = "999999";
document.body.appendChild(overlay);

// ----------------------
// Colour map
// ----------------------
const colours = {
  negative: "#1b5e20",  // green  - tabindex="-1"
  zero:     "#0a558c",  // blue   - tabindex="0"
  positive: "#b00020"   // red    - tabindex > 0
};

// ----------------------
// Main logic
// ----------------------
document.querySelectorAll("[tabindex]").forEach(el => {

  const raw = el.getAttribute("tabindex").trim();
  const value = parseInt(raw, 10);

  // Determine colour and label based on value
  let colour;
  if (value < 0) {
    colour = colours.negative;
  } else if (value === 0) {
    colour = colours.zero;
  } else {
    colour = colours.positive;
  }

  // Outline the element
  el.style.outline = `3px solid ${colour}`;

  // Compute position
  const rect = el.getBoundingClientRect();
  const left = rect.left + window.scrollX;
  const top = rect.top + window.scrollY;

  // Floating badge
  const badge = document.createElement("div");
  badge.textContent = `tabindex="${value}"`;
  badge.style.position = "absolute";
  badge.style.left = `${left}px`;
  badge.style.top = `${top - 30}px`;
  badge.style.background = colour;
  badge.style.color = "white";
  badge.style.padding = "5px";
  badge.style.borderRadius = "5px";
  badge.style.fontSize = "16px";
  badge.style.whiteSpace = "nowrap";
  badge.style.pointerEvents = "none";
  badge.style.zIndex = "999999";

  overlay.appendChild(badge);
});

6. Highlight visible label vs accessible name mismatch

What it does

Finds all links and buttons on the page, compares the visible text with the accessible name, and highlights each one in one of three colours:

Why it is important

People using speech input software often activate buttons and links by speaking the visible text shown on screen.

If the accessible name does not contain that visible text, speech commands may fail or produce unexpected results.

This script helps identify cases where the visible label and accessible name do not match, making potential issues easy to spot without inspecting each element individually.

Usage

Review the highlighted elements and check how the visible label compares to the accessible name.

Green (MATCH)

The visible text and accessible name are identical. No issue.

Blue (MODIFIED)

The accessible name starts with the visible text and adds extra context. This is valid and common. For example, a visible label of "Read more" may have an accessible name of "Read more about accessibility testing".

Review these cases to confirm that any additional context is useful and relevant.

Red (MISMATCH)

The accessible name has no relationship to the visible text. This may indicate a failure of WCAG 2.5.3 Label in Name and should be investigated.

The comparison is case-insensitive, so differences in capitalisation alone will not trigger a red badge.

View the script
// ----------------------
// Scroll-safe overlay container
// ----------------------
const overlay = document.createElement("div");
overlay.style.position = "absolute";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.pointerEvents = "none";
overlay.style.zIndex = "999999";
document.body.appendChild(overlay);

// ----------------------
// Colour map
// ----------------------
const colours = {
  match:    "#1b5e20",  // green - exact match
  modified: "#0a558c",  // blue  - accessible name starts with visible text
  mismatch: "#b00020"   // red   - no relationship between the two
};

// ----------------------
// Helper: get visible text
// ----------------------
function getVisibleText(el) {
  return el.textContent.trim().replace(/\s+/g, " ");
}

// ----------------------
// Helper: get accessible name
// ----------------------
function getAccessibleName(el) {

  // 1. aria-labelledby
  const labelledby = el.getAttribute("aria-labelledby");
  if (labelledby) {
    const parts = labelledby.trim().split(/\s+/).map(id => {
      const ref = document.getElementById(id);
      return ref ? ref.textContent.trim() : "";
    }).filter(Boolean);
    const name = parts.join(" ").trim();
    if (name) return name;
  }

  // 2. aria-label
  const ariaLabel = el.getAttribute("aria-label");
  if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim();

  // 3. visible text fallback
  return getVisibleText(el);
}

// ----------------------
// Main logic
// ----------------------
const selector = 'a, button, [role="button"], [role="link"]';

document.querySelectorAll(selector).forEach(el => {

  const visibleText = getVisibleText(el);
  const accessibleName = getAccessibleName(el);

  // Case-insensitive comparison
  const visibleLower = visibleText.toLowerCase();
  const nameLower = accessibleName.toLowerCase();

  // Determine match type
  let colour;
  let label;

  if (nameLower === visibleLower) {
    colour = colours.match;
    label = `MATCH: ${accessibleName}`;
  } else if (nameLower.startsWith(visibleLower)) {
    colour = colours.modified;
    label = `MODIFIED: ${accessibleName}`;
  } else {
    colour = colours.mismatch;
    label = `MISMATCH: ${accessibleName}`;
  }

  // Outline the element
  el.style.outline = `3px solid ${colour}`;

  // Compute position
  const rect = el.getBoundingClientRect();
  const left = rect.left + window.scrollX;
  const top = rect.top + window.scrollY;

  // Floating badge
  const badge = document.createElement("div");
  badge.textContent = label;
  badge.style.position = "absolute";
  badge.style.left = `${left}px`;
  badge.style.top = `${top - 30}px`;
  badge.style.background = colour;
  badge.style.color = "white";
  badge.style.padding = "5px";
  badge.style.borderRadius = "5px";
  badge.style.fontSize = "16px";
  badge.style.whiteSpace = "nowrap";
  badge.style.pointerEvents = "none";
  badge.style.zIndex = "999999";

  overlay.appendChild(badge);
});

7. Highlight roles that must never have author-provided names

What it does

Finds elements with roles that prohibit accessible naming and have been given a name using aria-label or aria-labelledby.

Each issue is highlighted with a NAME PROHIBITED badge showing the role involved.

Why it is important

Some elements have roles that prohibit naming.

Applying aria-label or aria-labelledby to these roles should not produce accessible names.

However, some browsers and assistive technologies still expose these names, which can lead to unexpected behaviour.

This script makes roles with prohibited names immediately visible.

Caveats

The script covers name-prohibited roles from both ARIA 1.2 and ARIA 1.3.

The lists differ slightly between versions. For example:

Usage

Review each highlighted element and remove any unnecessary aria-label or aria-labelledby attributes.

If you see a NAME PROHIBITED: generic badge on an element that should have a more specific role, check the markup carefully. Browsers fall back to the generic role when a role value is unrecognised or misspelled.

View the script
// ----------------------
// Scroll-safe overlay container
// ----------------------
const overlay = document.createElement("div");
overlay.style.position = "absolute";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.pointerEvents = "none";
overlay.style.zIndex = "999999";
document.body.appendChild(overlay);

// ----------------------
// Roles that must never be named
// ARIA 1.2: caption, code, deletion, emphasis, generic,
//           insertion, paragraph, presentation, strong,
//           subscript, superscript
// ARIA 1.3 adds: definition, mark, none, suggestion,
//           term, time, tooltip
// Note: presentation is 1.2 only; none is the 1.3 preferred synonym
// ----------------------
const forbiddenNamedRoles = [
  // ARIA 1.2
  "caption",
  "code",
  "deletion",
  "emphasis",
  "generic",
  "insertion",
  "paragraph",
  "presentation",
  "strong",
  "subscript",
  "superscript",
  // ARIA 1.3
  "definition",
  "mark",
  "none",
  "suggestion",
  "term",
  "time",
  "tooltip"
];

// ----------------------
// Native elements that implicitly carry these roles
// ----------------------
const implicitRoleMap = {
  // ARIA 1.2
  caption:  "caption",
  code:     "code",
  del:      "deletion",
  s:        "deletion",
  em:       "emphasis",
  div:      "generic",
  span:     "generic",
  ins:      "insertion",
  p:        "paragraph",
  strong:   "strong",
  sub:      "subscript",
  sup:      "superscript",
  // ARIA 1.3
  dfn:      "definition",
  mark:     "mark",
  time:     "time"
};

// ----------------------
// Build selectors
// ----------------------
const roleAttrSelectors = forbiddenNamedRoles.map(r => `[role="${r}"]`).join(", ");
const implicitTagSelectors = Object.keys(implicitRoleMap).join(", ");

// ----------------------
// Main logic
// ----------------------
document.querySelectorAll(`${roleAttrSelectors}, ${implicitTagSelectors}`).forEach(el => {

  const hasAriaLabel = el.hasAttribute("aria-label") && el.getAttribute("aria-label").trim() !== "";
  const hasAriaLabelledby = el.hasAttribute("aria-labelledby") && el.getAttribute("aria-labelledby").trim() !== "";

  // Only flag elements that have an incorrectly applied name
  if (!hasAriaLabel && !hasAriaLabelledby) return;

  // Determine the role -- explicit takes priority over implicit
  const explicitRole = el.getAttribute("role");
  const role = (explicitRole && forbiddenNamedRoles.includes(explicitRole))
    ? explicitRole
    : implicitRoleMap[el.tagName.toLowerCase()];

  // Outline the element
  el.style.outline = "3px solid #b00020";

  // Compute position
  const rect = el.getBoundingClientRect();
  const left = rect.left + window.scrollX;
  const top = rect.top + window.scrollY;

  // Floating badge
  const badge = document.createElement("div");
  badge.textContent = `NAME PROHIBITED: ${role}`;
  badge.style.position = "absolute";
  badge.style.left = `${left}px`;
  badge.style.top = `${top - 30}px`;
  badge.style.background = "#b00020";
  badge.style.color = "white";
  badge.style.padding = "5px";
  badge.style.borderRadius = "5px";
  badge.style.fontSize = "16px";
  badge.style.whiteSpace = "nowrap";
  badge.style.pointerEvents = "none";
  badge.style.zIndex = "999999";

  overlay.appendChild(badge);
});

Footnotes

A focusable element inside aria-hidden="true" should always be investigated as an accessibility issue.

An example could be a button inside a parent that is set to aria-hidden="true":

<div aria-hidden="true">
  <button>Hidden button</button>
</div>

Some browsers may still expose parts of that element to assistive technologies. For example, Chrome may expose the button’s name in the accessibility tree.

This does not make the pattern safe.

The ARIA specification says that elements are considered hidden when they, or any of their ancestors, have aria-hidden="true" applied. If a focusable control is inside that hidden content, different browsers and assistive technologies may handle it differently.

Do not rely on browser forgiveness. Move the focusable element outside the aria-hidden="true" content, or remove aria-hidden="true" from the parent.