| title | Browser overlay |
|---|---|
| description | The script that runs in your page and sends live data to the devtools. |
The overlay runs inside your *Angular page. It reads Angular's debug API and sends live data to the devtools server. Importing the module starts it, so in most apps one dynamic import in main.ts is all you need.
Load the overlay after bootstrap, with a dynamic import that only runs in development:
// src/main.ts
import {bootstrapApplication} from '@angular/platform-browser';
import {App} from './app/app';
import {appConfig} from './app/app.config';
bootstrapApplication(App, appConfig)
.then(() => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
return import('@santoshyadavdev/ng-devtools/overlay');
}
return undefined;
})
.catch((err) => console.error(err));// src/main.ts
import {bootstrapApplication} from '@angular/platform-browser';
import {App} from './app/app';
import {appConfig} from './app/app.config';
bootstrapApplication(App, appConfig).then(() => {
if (import.meta.env.DEV) void import('@santoshyadavdev/ng-devtools/overlay');
});The overlay reads window.ng, Angular's debug API. Production builds remove it, so the overlay has nothing to read there. The dynamic import keeps the overlay out of your production bundle.
The overlay looks for the devframe connection next to the page first. Then it tries these paths in order:
/__ng-devtools//__devframes/ng-devtools/
It also adds the floating button. With the hub mounted, the button opens the whole hub, with every dock in a side rail.
On Angular 20 and later, the overlay reads the page about 250 ms after Angular runs change detection. It also reads it every 4 seconds as a heartbeat. Until the app bootstraps, it reads the page every 3 seconds instead. Change that interval with limits.refreshMs.
Each read skips data that did not change. Router events are sent as they happen.
Each browser tab gets its own page id, kept in sessionStorage. The devtools use it to tell tabs apart. When a tab closes, its data is dropped.
If you mount the devtools somewhere else, call initOverlay with that path:
// src/main.ts
import {bootstrapApplication} from '@angular/platform-browser';
import {App} from './app/app';
import {appConfig} from './app/app.config';
bootstrapApplication(App, appConfig).then(async () => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
const {initOverlay} = await import('@santoshyadavdev/ng-devtools/overlay');
const dispose = await initOverlay({baseURL: '/__my-devtools/'});
}
});baseURL takes one path or a list of paths to try in order. initOverlay resolves to a function that stops the overlay it started and removes its hooks.
The floating button follows the path the overlay connected to. If that path is <base>ng-devtools/ and a hub answers at <base>, the button opens the hub. Otherwise it opens the devtools panel at that path.
Only one overlay runs on a page. Importing the module already starts one on the default URLs. When you call initOverlay, it stops the running overlay first, the auto-started one included, and then starts yours. The page never ends up with two connections.
An overlay that was stopped or replaced before it connected does not report its connection error.
Call disposeOverlay to turn the overlay off:
// src/app/devtools-toggle.ts
import {disposeOverlay} from '@santoshyadavdev/ng-devtools/overlay';
export async function stopDevtools() {
await disposeOverlay();
}disposeOverlay stops whichever overlay is running, including the one that importing the module started. It closes the connection, clears its timers, listeners and observers, and removes the floating button.
To send data again, call initOverlay. It does not add the floating button back.
The overlay also exports registerNgrxSignals. Call it once with patchState so that restoring a store's state also notifies watchState listeners:
// src/main.ts
import {bootstrapApplication} from '@angular/platform-browser';
import {App} from './app/app';
import {appConfig} from './app/app.config';
bootstrapApplication(App, appConfig).then(() => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
return Promise.all([
import('@santoshyadavdev/ng-devtools/overlay'),
import('@ngrx/signals'),
]).then(([devtools, {patchState}]) => devtools.registerNgrxSignals({patchState}));
}
return undefined;
});See Restore NgRx signal state.
If no path answers, the overlay logs an error that starts with [ng-devtools] No devtools server found and lists the paths it tried. The floating button still appears, and its panel says No devtools server found with a link to the setup guide.
Common causes:
- No server part is mounted, for example plain
ng servewithoutinitNgDevtoolsHub()inserver.ts. - The hub is mounted after
express.staticor the SSR handler, so the app answers first. - The hub runs on a custom
base, and the overlay still uses the default paths. Pass the path toinitOverlay.
The overlay and the button do not start inside the devtools panel frame, so a misconfigured panel never shows a second button.
When you hover or focus a row in the devtools, the overlay draws an amber box around its element in the page. The box follows the element and stays until the pointer or focus leaves the row. The box also clears when the panel closes, reloads or loses its connection to the dev server. If that clear never reaches the page, the box clears after 60 seconds. A box drawn by the highlight agent tool clears after 2 seconds.
While you pick an element on the page, the box follows the pointer and clears when the pointer leaves the page or picking ends.
- The box also works for SVG hosts, like
g[app-bar]in a chart. - A host with
display: contentshas no box of its own, so the box goes around its children. - A hidden host (
display: none, an inactive tab panel) gets no box. - The box is shown in the browser top layer, so it stays visible over an open
<dialog>, a popover or a CDK overlay. - The
highlightagent tool and a click on a component chip in the Pipes tab also scroll the element into view.