v2.3 stable
Two closed-shadow web components side by side. One opts in
via el.nac3, one does not. Watch the contrast.
A closed shadow root is OPAQUE from outside by browser
design. NAC3 cannot reach in unless the component
voluntarily exposes itself by implementing
get nac3() { return { query(...) } } on its
host element. If it does, the runtime delegates. If it
does not, the runtime emits
nac:shadow_blocked so debugging tooling
surfaces the dead end.
el.nac3Same closed shadow root, same opaque from the outside, BUT the author exposes a `query` method. The runtime delegates, finds the element, fires the click.
class TodoCardClosedExposed extends HTMLElement {
constructor() {
super();
/* CLOSED shadow root -- inaccessible from outside. */
this._shadow = this.attachShadow({ mode: 'closed' });
}
/* The voluntary NAC3 exposure contract. */
get nac3() {
return {
query: sel => this._shadow.querySelector(sel),
queryAll: sel => [...this._shadow.querySelectorAll(sel)],
describe: () => [...this._shadow.querySelectorAll('[data-nac-id]')]
.map(el => ({
id: el.getAttribute('data-nac-id'),
role: el.getAttribute('data-nac-role'),
label: el.textContent.trim().slice(0, 40)
}))
};
}
}
el.nac3
Same closed shadow root, same opaque-from-outside, BUT
no exposure method. The runtime cannot find the inner
element. It emits nac:shadow_blocked with
reason='not_exposed' so the page sees
exactly why.
class TodoCardClosedBlind extends HTMLElement {
constructor() {
super();
/* CLOSED shadow root -- inaccessible from outside. */
this._shadow = this.attachShadow({ mode: 'closed' });
}
/* No nac3 getter. The host element declares
data-nac-shadow="closed" so the NAC3 runtime knows this
is a deliberate closed boundary (vs an element that has
no shadow at all) and emits nac:shadow_blocked. */
}
Watch this panel while clicking each "Toggle" button. The
positive side fires nac:action:succeeded; the
negative side fires nac:shadow_blocked first,
then NAC.click rejects with
code='not_found'.