In Angular, how do you package a standalone component as a custom element with @angular/elements so a plain HTML page can use its tag?
answer
- two calls, two owners
- a class, not a registered tag
- the injector comes from an application
- createApplication, then customElements.define
- tag name differs from the selector
basics
~20 sCall createCustomElement(Component, { injector }) from @angular/elements to get a custom-element class, then register it with the browser's customElements.define('tag-name', Class). The injector usually comes from createApplication(), and the tag must differ from the component's selector.
solid answer
~40 s`createCustomElement()` from `@angular/elements` takes the component class plus an `NgElementConfig` whose `injector` is required, and returns an `NgElementConstructor` - a class extending `NgElement`, which extends `HTMLElement`. It does not register anything: you pass that class to the browser's `customElements.define('kj-feedback', FeedbackElement)`. On a page with no Angular root component, you get the injector from `createApplication()` in `@angular/platform-browser`, which builds the platform and application injectors without bootstrapping a component; `appRef.injector` is what you hand over. From then on, each `<kj-feedback>` the browser upgrades creates its own component instance when it is connected. Pick a tag distinct from the component's `selector`: the Angular docs warn that reusing it can give one DOM node two component instances.
code
ts · 14 linesimport { createApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { createCustomElement } from '@angular/elements';
import { FeedbackWidget } from './feedback-widget';
createApplication({ providers: [provideHttpClient()] })
.then((appRef) => {
const FeedbackElement = createCustomElement(FeedbackWidget, {
injector: appRef.injector,
});
// FeedbackWidget's selector is 'app-feedback-widget'; the tag is distinct.
customElements.define('kj-feedback', FeedbackElement);
})
.catch((err) => console.error(err));go deeper
Remember the two calls and who owns each: createCustomElement from @angular/elements builds the class, and the browser's customElements.define registers the tag.
Explain why the injector is required, where it comes from on a page without a root component (createApplication), and what the element does when it connects.
Show you have embedded widgets in pages you do not own: a distinct tag from the selector, one application injector for the providers the widget needs, and no zone.js by default.
Frame Angular Elements as a distribution boundary: decide which widgets share one application and injector, and what the host page is allowed to know about them.
## What Angular Elements is **Angular Elements** is the `@angular/elements` package. It turns an Angular component into a **custom element** - a tag the browser itself knows how to create, so any HTML page can use it without knowing anything about Angular. The browser keeps a registry that maps tag names to classes; Angular's job is to produce a class whose instances host your component, wire its inputs and outputs to the DOM, and run change detection for it. The whole packaging step is two calls with two different owners: | Step | API | Owner | What it does | |---|---|---|---| | 1 | `createCustomElement(Widget, { injector })` | `@angular/elements` | Builds a class (`NgElementConstructor`) that hosts `Widget` | | 2 | `customElements.define('kj-feedback', WidgetElement)` | the browser | Registers the tag so the parser and `document.createElement` use that class | A common misreading is that step 1 also registers the tag. It does not: `createCustomElement` returns a constructor and nothing more. Until you call `customElements.define`, `<kj-feedback>` in the page is just an unknown element. ## What createCustomElement needs The function signature is `createCustomElement<P>(component, config: NgElementConfig)`. The config has: - **`injector`** (required) - the injector each element instance uses. The default strategy reads `ApplicationRef`, `NgZone` and Angular's change-detection scheduler from it, attaches every created component's host view to that `ApplicationRef`, and uses it as the component's environment injector, so anything the component `inject()`s from the root level must be resolvable there. - **`strategyFactory`** (optional) - a replacement for the default strategy that decides how the component is created and connected. Almost nobody sets it. The returned class also exposes a static `observedAttributes` list, derived from the component's inputs, and a constructor that accepts an optional `Injector` to override the configured one for a single instance you create with `new`. ## Getting an injector on a non-Angular page Inside an existing Angular application you can simply inject `Injector` (the official popup example does this in its root component's constructor). The interesting case is a page that has **no Angular application at all** - a CMS template, a server-rendered legacy page. There, `bootstrapApplication()` is the wrong tool because it needs a root component element to render into. Use `createApplication()` from `@angular/platform-browser` instead: it creates the platform and the application's environment injector, applies the providers you pass (for example `provideHttpClient()`), and resolves to an `ApplicationRef` without rendering any component. `appRef.injector` is then the injector for `createCustomElement`. Since Angular v21 applications are zoneless by default, so this entry point needs no `zone.js` polyfill; if you opted back in with `provideZoneChangeDetection()`, zone.js would have to load first. ## What happens when the tag appears 1. The browser meets `<kj-feedback>` (in parsed HTML, via `innerHTML`, or `document.createElement`) and constructs an instance of the registered class. 2. Any observed attributes already present are delivered to the element, which caches them as pending input values. 3. When the element is connected to the document, Angular creates the component with the element itself as the host, applies the cached inputs, subscribes to its outputs, attaches the host view to the `ApplicationRef`, and runs an initial change detection. 4. When it is removed, destruction is scheduled on a short timer so a simple move within the DOM does not destroy it. Each tag on the page therefore gets its own component instance, while all of them share the root-level services of the one application whose injector you passed. ## Choosing the tag name The Angular guide explicitly warns: **do not use the component's selector as the custom element tag.** If Angular also compiles templates that contain that tag, it matches the component on the element as a normal Angular component, and the browser upgrades the same node as a custom element - two component instances on one DOM node. Keep `selector: 'app-feedback-widget'` for Angular templates and register something like `kj-feedback` for the outside world. (The browser's own naming rules for custom element tags are a separate subject.) ## Common mistakes - Expecting `createCustomElement` to register the tag, then wondering why nothing renders. - Calling `bootstrapApplication()` on a page that has no root component tag, when `createApplication()` is what builds an injector without rendering. - Passing a freshly made `Injector.create({ providers: [] })` with no parent: it has no `ApplicationRef` or `NgZone`, so creating the element's strategy fails. - Reusing the component selector as the tag. - Believing the component must be declared in an NgModule first; standalone components (the default since v19) convert directly.
- Why does the Angular guide warn against registering the custom element under the component's own selector?If Angular templates that contain that tag are compiled, Angular matches the component on the element as a regular component, and the browser also upgrades the node as the registered custom element. The result is two component instances hosted on one DOM node, with duplicated rendering and side effects. Keep the Angular selector for templates and register a distinct tag for the page.
- Can one instance of an Angular Elements class use a different injector from the one passed to createCustomElement?Yes. The returned `NgElementConstructor` accepts an optional `Injector` argument, and an instance created with `new FeedbackElement(otherInjector)` uses it instead of the configured one. Elements the browser creates from markup or `document.createElement` call the constructor without arguments, so they always fall back to the injector in the config.
saying these in an interview costs you the question
- createCustomElement registers the tag with the browser on its own
- Reuse the component's selector as the custom element tag name
- A plain page must call bootstrapApplication with a root component first
- Any empty Injector.create() works as the element's injector
- The component must be declared in an NgModule before conversion