Labels & rendering

A new control. The same reference.

The outside label references the component. Angular Elements renders the checkbox inside its shadow root.

Try: Click the label, replace the checkbox, then click the label again. Each replacement starts unchecked.

Follow a rendered target

DOM observationWaiting for Angular Elements to render.

Popover targeting

An outside button. A rendered panel.

Angular Elements creates a native popover inside another component. The outside buttons reference its host.

Try: Open the popover, then dismiss it with its close button or Escape.

Open an internal popover

DOM observationWaiting for Angular Elements to render.
How the examples are connected

Each component sets its shadow root’s referenceTarget to the internal target ID. The fallback loads before the component definitions, so it can capture the roots when the renderer creates them and observe later DOM changes.

The page waits for the renderer’s first update before enabling its buttons. Replacing the checkbox changes the component’s revision attribute; the renderer creates a new input with the same ID.

<label for="renderer-checkbox">Send me release notes</label>
<rt-angular-checkbox id="renderer-checkbox" revision="0"></rt-angular-checkbox>

<button type="button" popovertarget="renderer-popover">Open popover</button>
<rt-angular-popover id="renderer-popover"></rt-angular-popover>

The selected setup uses labels({ activation: "focus-and-click", naming: true }) and popoverTargets(). The page’s event listeners only display state; they do not forward label or popover actions.

Component source

components.ts

import {
  type AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  enableProdMode,
  inject,
  input,
  numberAttribute,
  ViewEncapsulation,
} from "@angular/core";
import { createCustomElement } from "@angular/elements";
import { createApplication } from "@angular/platform-browser";
import { withRendererTimeout } from "../../examples/shared/renderer-readiness.js";

function setReferenceTarget(host: HTMLElement, target: string) {
  const root = host.shadowRoot as ShadowRoot & { referenceTarget: string };
  root.referenceTarget = target;
}

@Component({
  selector: "angular-checkbox-view",
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.ShadowDom,
  styleUrl: "../shared/components.css",
  template: `
    <div class="component-preview">
      @for (current of [revision()]; track current) {
        <input id="control" type="checkbox" [attr.data-revision]="current">
      }
      <span aria-hidden="true">Native checkbox</span>
    </div>
    <p class="hint">Render revision {{ revision() }}</p>
  `,
})
class AngularCheckbox implements AfterViewInit {
  readonly revision = input(0, { transform: (value: unknown) => numberAttribute(value, 0) });
  private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);

  ngAfterViewInit() {
    setReferenceTarget(this.host.nativeElement, "control");
  }
}

@Component({
  selector: "angular-popover-view",
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.ShadowDom,
  styleUrl: "../shared/components.css",
  template: `
    <div id="panel" popover="auto" role="dialog" aria-labelledby="panel-title">
      <p class="eyebrow">Angular Elements · shadow DOM</p>
      <h2 id="panel-title">Rendered with Angular</h2>
      <p>This native popover lives inside an Angular component shadow root.</p>
      <button type="button" popovertarget="panel" popovertargetaction="hide" autofocus>Close popover</button>
    </div>
  `,
})
class AngularPopover implements AfterViewInit {
  private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);

  ngAfterViewInit() {
    setReferenceTarget(this.host.nativeElement, "panel");
  }
}

enableProdMode();
// Angular 22 is zoneless. Both component templates and their styles are compiled
// during the build, so this page needs neither Zone.js nor a runtime compiler.
const application = createApplication().then(app => {
  // Keep Angular Elements' standard lifecycle. Its documented limitation on
  // reconnecting a destroyed element also applies here; create a new instance
  // after removal instead of reusing the old host after Angular tears it down.
  customElements.define("rt-angular-checkbox", createCustomElement(AngularCheckbox, { injector: app.injector }));
  customElements.define("rt-angular-popover", createCustomElement(AngularPopover, { injector: app.injector }));
  return app;
});

export async function whenReady() {
  await withRendererTimeout((async () => {
    const app = await application;
    await app.whenStable();
    for (const [id, target] of [["renderer-checkbox", "control"], ["renderer-popover", "panel"]]) {
      if (!document.getElementById(id)?.shadowRoot?.getElementById(target)) {
        throw new Error(`Angular did not finish rendering ${id}`);
      }
    }
  })(), "Angular");
}

components.js

export { whenReady } from "../../dist/angular/components.js";

Functional JavaScript sizes

Functional page JavaScript

330.049 KB 107.952 KB gzip

Always loaded.

Fallback additional

23.929 KB 8.430 KB gzip

Core + selected adapters.

Fallback route total

353.978 KB 116.382 KB gzip

Always-loaded and fallback files; shared files counted once.

Native-surface route total

331.038 KB 108.490 KB gzip

Includes 0.989 KB (0.538 KB gzip) of behavioral probes requested only after the property surface is present.

Minified / gzip. 1 KB = 1,000 bytes. Demo-only syntax highlighting is excluded from the functional totals and reported separately below.

Whole-page delivery context

HTML document

20.615 KB 1 request

Generated markup, including these measurements.

Initial supporting assets

18.520 KB raw 17.897 KB local transfer · 3 requests

Styles, icon, and the highlighting scheduler; functional JS is separate above.

Initial page with fallback

393.113 KB raw 154.894 KB local transfer · 9 requests

Document + functional fallback path + initial supporting assets, with unique files counted once.

Deferred highlighting

17.861 KB raw 8.318 KB local transfer · 9 requests

Engine, theme, and this page’s grammar closure, requested after a source disclosure opens or during idle time.

“Local transfer” matches the included development server: gzip JavaScript sidecars, but uncompressed HTML, CSS, and SVG. GitHub Pages, browser caching, HTTP headers, and production compression can change transferred bytes and request scheduling. Module preloads fetch functional modules early without evaluating the app and do not add duplicate transfers.

Inspect the 6 generated JavaScript files

Selected adapters: labels, popover-targets.

Gzip totals sum each compressed file. The fallback and native-probe routes are mutually exclusive. Separate CSS files, HTML, JSON, source maps, HTTP headers, and Microlighter assets are excluded. Component styles embedded in JavaScript are included. Transfer sizes depend on compression and caching. Microlighter MIT license.

Files included in this page’s totals
FileDeliveryMinifiedGzip
angular/main.js Page 3.257 KB 1.351 KB
shared/chunks/app-PGAJV7HR.js Page 326.650 KB 106.463 KB
shared/chunks/chunk-CLCOZ6O7.js Fallback / native probe shared 0.849 KB 0.422 KB
shared/chunks/chunk-S2QLMWTP.js Page 0.142 KB 0.138 KB
shared/chunks/detect-X6QVIAY4.js Native-surface probe 0.140 KB 0.116 KB
shared/chunks/reference-target.setup-F3BB382F.js Fallback additional 23.080 KB 8.008 KB

Inspect the size manifest

Renderer integration & limitations

Angular Elements and Reference Target

Angular’s createCustomElement exposes standalone components with ViewEncapsulation.ShadowDom. A numeric signal input drives a tracked block that replaces the checkbox. The page waits for the application to become stable and for its targets to exist. Templates and Angular libraries are compiled and linked during the build; the browser loads no compiler or Zone.js.

Angular Elements does not reliably support reattaching the same element after its destruction delay. These cases update mounted components; create a fresh element after removal and teardown. This is a renderer lifecycle limitation, independent of reference forwarding.

The library and its component code are included in Page JavaScript above. Each renderer is built as an independent page with only the labels and popover-targets adapters in its optional fallback bundle.

These examples use open, client-rendered roots. They do not test server rendering or hydration. The label fallback does not recreate native label.control or input.labels relationships. Readouts describe DOM state; computed names and assistive technology need separate validation.

Angular Elements documentation ↗