NAC3 v2.3 -- Closed Shadow DOM

v2.3 stable  Two closed-shadow web components side by side. One opts in via el.nac3, one does not. Watch the contrast.

The contract

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.

Positive case

Component implements el.nac3

Same closed shadow root, same opaque from the outside, BUT the author exposes a `query` method. The runtime delegates, finds the element, fires the click.

awaiting click...
Show source
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)
        }))
    };
  }
}
Negative case

Component does NOT implement 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.

awaiting click...
Show source
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. */
}

Event log

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'.