Skip to content

Instantly share code, notes, and snippets.

@stevefaulkner
Created September 3, 2025 08:04
Show Gist options
  • Select an option

  • Save stevefaulkner/a0f1f13c620a83e9584a4caae3c9808d to your computer and use it in GitHub Desktop.

Select an option

Save stevefaulkner/a0f1f13c620a83e9584a4caae3c9808d to your computer and use it in GitHub Desktop.
aria-activedescendant check
(() => {
let VERBOSE = false; // set true for detailed object dumps
const fmt = (el) => {
if (!el) return '—';
const id = el.id ? `#${el.id}` : '';
const role = el.getAttribute?.('role') || '';
const tag = el.tagName?.toLowerCase?.() || '';
return [tag+id, role?`(role=${role})`:''].filter(Boolean).join(' ');
};
const isVisible = (el) => !!(el && el.getClientRects().length &&
(el.offsetWidth || el.offsetHeight) &&
getComputedStyle(el).visibility !== 'hidden' &&
getComputedStyle(el).display !== 'none');
const relatedContainers = (owner) => {
const owns = (owner.getAttribute('aria-owns')||'').split(/\s+/).filter(Boolean);
const ctrls = (owner.getAttribute('aria-controls')||'').split(/\s+/).filter(Boolean);
const out = new Set();
for (const id of [...owns, ...ctrls]) {
const el = document.getElementById(id);
if (el) out.add(el);
}
return [...out];
};
function evaluate() {
const owner = document.activeElement;
const attr = owner?.getAttribute?.('aria-activedescendant') || '';
const info = {
focused: fmt(owner),
ownerRole: owner?.getAttribute?.('role') || '—',
ariaActivedescendant: attr || '—',
resolvesTo: '—',
targetRole: '—',
relation: '—',
status: '—'
};
let ok = false;
let line = '';
if (owner && attr) {
const target = document.getElementById(attr);
info.resolvesTo = fmt(target);
info.targetRole = target?.getAttribute?.('role') || '—';
if (!target) {
info.status = '⚠️ no target';
} else {
const containers = relatedContainers(owner);
const isDesc = owner.contains(target);
const inOwned = containers.some(c => c.contains(target) || c === target);
info.relation = isDesc ? 'descendant' : inOwned ? 'owned/controlled' : 'unrelated';
const reasons = [];
if (!isDesc && !inOwned) reasons.push('not descendant/owned');
if (!isVisible(target)) reasons.push('target hidden');
if (!isVisible(owner)) reasons.push('owner hidden');
ok = reasons.length === 0;
info.status = ok ? '✅ OK' : '⚠️ ' + reasons.join('; ');
}
} else {
info.status = owner ? '⚠️ missing aria-activedescendant' : '⚠️ no focus';
}
// Single-line summary
line = `${ok ? '✅' : '⚠️'} owner=${info.focused} activedescendant="${info.ariaActivedescendant}" target=${info.resolvesTo} rel=${info.relation} role(owner/target)=${info.ownerRole}/${info.targetRole} :: ${info.status}`;
if (VERBOSE) {
console.log('[aria-activedescendant]', line, info);
} else {
console.log('[aria-activedescendant]', line);
}
}
const mo = new MutationObserver((muts) => {
for (const m of muts) {
if (m.type === 'attributes' || m.type === 'childList') { evaluate(); break; }
}
});
mo.observe(document.documentElement, {
attributes: true,
subtree: true,
childList: true,
attributeFilter: ['aria-activedescendant','aria-owns','aria-controls','role','id','style','class'],
});
['focus','blur','keydown','keyup'].forEach(ev => {
document.addEventListener(ev, () => setTimeout(evaluate, 0), true);
});
// API
window.__stopAadInspector = () => { mo.disconnect(); console.log('[aria-activedescendant] stopped'); };
window.__aadVerbose = (on = true) => { VERBOSE = !!on; console.log(`[aria-activedescendant] verbose=${VERBOSE}`); };
console.log('[aria-activedescendant] started. Use __stopAadInspector() to stop, __aadVerbose(true|false) to toggle details.');
evaluate();
})();
@stevefaulkner
Copy link
Author

stevefaulkner commented Sep 3, 2025

note: this script and documentation was vomited out by ChatGPT

aria-activedescendant Console Inspector --- Documentation

What it is

A zero-install snippet you paste into your browser's DevTools Console to
verify aria-activedescendant wiring on any page. It watches focus
and relevant DOM changes, then prints a single-line status showing
whether the focused element's aria-activedescendant resolves to a
valid target and how that target is related.

Quick start

  1. Open the target page\
  2. Open DevTools → Console\
  3. Paste the script and press Enter\
  4. Move focus with the keyboard (e.g., arrow keys in a
    combobox/listbox)

You'll see log lines like:

[aria-activedescendant] ✅ owner=input#search(role=combobox) activedescendant="opt-3" target=div#opt-3(role=option) rel=owned/controlled role(owner/target)=combobox/option :: ✅ OK

What it reports

Each log line contains:

  • owner --- The currently focused element, formatted as
    tag#id (role=…).
  • activedescendant --- The ID value from aria-activedescendant
    on the owner (or if absent).
  • target --- The element that ID resolves to, similarly formatted
    (or if missing).
  • rel --- Relationship between owner and target:
    • descendant → target is inside the owner\
    • owned/controlled → target is inside any element referenced by
      owner's aria-owns or aria-controls\
    • unrelated → none of the above
  • role(owner/target) --- The roles found on both elements.
  • status --- Either ✅ OK or ⚠️ … with reasons joined by ;
    (e.g., not descendant/owned, target hidden, owner hidden,
    no target, missing aria-activedescendant).

How it works (high level)

  • Focus & keyboard listeners trigger a check after each event.\
  • A MutationObserver monitors attribute/DOM changes on the
    document (including aria-activedescendant, aria-owns,
    aria-controls, role, id, style, class) and re-evaluates
    when any change occurs.\
  • The evaluation:
    1. Reads the focused element ("owner") and its
      aria-activedescendant value\
    2. Looks up the target element by ID\
    3. Determines relation: descendant vs inside any
      aria-owns/aria-controls container vs unrelated\
    4. Checks visibility of both owner and target\
    5. Prints a one-line summary with ✅/⚠️

Console API

  • Stop the inspector:

    window.__stopAadInspector()
  • Toggle verbose mode (prints the concise line plus a full info
    object):

    window.__aadVerbose(true)   // on
    window.__aadVerbose(false)  // off

Typical use cases

  • Validate combobox/listbox, grid, tree, menu, tablist patterns that
    drive focus via aria-activedescendant.
  • Confirm that the active option (or row/treeitem/tab) exists, is
    visible, and is correctly related to its owner.
  • Catch common mistakes:
    • Owner has no aria-activedescendant
    • Target ID doesn't exist
    • Target lives outside expected containers
    • Owner/target hidden via CSS

Tips & caveats

  • This tool checks DOM wiring and visibility heuristics. It does
    not verify assistive technology announcements or user agent
    mapping.\
  • If the page is extremely dynamic, the MutationObserver's broad scope
    (including style and class) may be chatty. If needed, you can:
    • Switch off verbose logs: __aadVerbose(false)
    • Stop the current run and re-paste a trimmed variant with a
      reduced attributeFilter (e.g., only aria-activedescendant
      and role).\
  • "Owned/controlled" includes elements referenced by either
    aria-owns or aria-controls. That's helpful in practice, though
    note that aria-controls is association, not parent/child
    semantics.

Troubleshooting

  • No output? Ensure DevTools Console is open and you pressed Enter
    after pasting.\
  • Too many logs / sluggish page?
    • Run __aadVerbose(false) (default is already concise).\
    • Run __stopAadInspector() to stop, then re-run a trimmed script
      with fewer observed attributes.\
  • Target reads as : Check that the owner's
    aria-activedescendant value exactly matches an element ID on
    the page.

Removal

At any time:

__stopAadInspector()

That's it---you've got a lightweight way to verify
aria-activedescendant live as you navigate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment