Angular 116 🅰️ Angular Elements
Angular Elements packages Angular components as custom elements—a browser standard for defining new HTML tags in a framework-agnostic way. The result is a component that any web application can use, regardless of whether it is built with Angular, React, Vue, or plain HTML. The Angular infrastructure—change detection, dependency injection, template rendering—travels with the element, so the consumer needs no knowledge of Angular to use it .
This capability solves a specific organizational problem. A team building an Angular application may also need to provide embeddable widgets: a search form, a dashboard panel, a configuration interface. Without Angular Elements, embedding those widgets in a non-Angular CMS, a legacy application, or a partner’s site would require rebuilding them in the host technology. Angular Elements makes the Angular component itself the deliverable, packaged as a standard HTML element that any page can instantiate with a tag .
The mechanism is the createCustomElement() function from @angular/elements, which converts an Angular component class into a class the browser can register as a custom element. Once registered, the element bootstraps itself when added to the DOM and destroys itself when removed, with Angular handling the lifecycle transparently .
Key point: createCustomElement() converts an Angular component into a custom element class. Register it with customElements.define(), and the component becomes a standard HTML tag. Inputs map to attributes, outputs map to Custom Events, and the element is self-bootstrapping.
Why Angular Elements exists
The framework-agnostic distribution problem. Angular components are not usable outside an Angular application without Angular Elements. The component depends on Angular’s dependency injection, change detection, and template compilation. Angular Elements bundles that infrastructure into the custom element, so the component runs anywhere the element is registered, without the host page needing Angular .
The multi-team widget problem. An organization with multiple teams and multiple applications needs a way to share UI components without imposing a framework choice. Angular Elements provides a neutral packaging format: the component is delivered as a custom element, and the consuming team uses it like any other HTML tag. The Angular team describes this as extending the reach of Angular code to “widgets and components that can be embedded in applications by other developers, on other teams, and even using other technologies” .
The micro-frontend integration problem. Micro-frontend architectures often need a framework-agnostic composition mechanism. Angular Elements provides one: the shell loads a bundle, the bundle registers custom element tags, and the shell uses those tags in its own markup. The shell does not need to know that the component was built with Angular .
The incremental migration problem. A legacy application can be migrated to Angular one component at a time. Each migrated component is packaged as a custom element and embedded in the legacy page. The legacy application continues to function, and the Angular component runs alongside it without requiring a full rewrite .
The bootstrap problem. A component added to a page needs to be instantiated, have its dependencies injected, run change detection, and be cleaned up when removed. Doing this manually with dynamic component loading requires the caller to attach the view to ApplicationRef, set inputs, subscribe to outputs, and detach the view. Angular Elements handles all of this automatically: the element bootstraps when connected and destroys when disconnected .
a. Creating a custom element
The process has three steps: create the component, convert it with createCustomElement(), and register it with customElements.define(). The @angular/elements package must be installed first .
npm install @angular/elements
The conversion function takes the component class and an injector, and returns a class that extends HTMLElement:
import { createCustomElement } from '@angular/elements';
import { Injector } from '@angular/core';
import { PopupComponent } from './popup.component';
export class AppModule {
constructor(private injector: Injector) {
const element = createCustomElement(PopupComponent, {
injector: this.injector,
});
customElements.define('my-popup', element);
}
}
With standalone components and the modern createApplication API, the pattern is similar:
import { createApplication } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { CommentsComponent } from './comments.component';
(async () => {
const app = await createApplication({ providers: [] });
const commentsElement = createCustomElement(CommentsComponent, {
injector: app.injector,
});
customElements.define('comments-widget', commentsElement);
})();
The injector provides the component access to the application’s services and providers. The customElements.define() call registers the tag name with the browser’s CustomElementRegistry. When the browser encounters <my-popup> in the markup, it instantiates the component .
Avoid using the component’s selector as the custom element tag. Angular creates two component instances for a single DOM element when the selector and tag match: one from the Angular component and one from the custom element. Use a distinct tag name .
b. Mapping inputs and outputs
The custom element bridges Angular’s component interface and standard DOM APIs. Component @Input properties become HTML attributes, and @Output properties become Custom Events .
Inputs to attributes. Angular parses the component’s input properties and defines corresponding attributes. Because HTML attributes are case-insensitive, the names are transformed to dash-separated lowercase. An input named myInputProp becomes the attribute my-input-prop .
@Component({ selector: 'my-popup', template: '...' })
export class PopupComponent {
@Input() message = '';
}
<my-popup message="Use Angular!"></my-popup>
For complex objects that cannot be represented as attributes, the consumer assigns them as JavaScript properties on the element:
<pathway-browser></pathway-browser>
<script>
const el = document.querySelector('pathway-browser');
el.pathwayId = 'R-HSA-123456';
</script>
Outputs to Custom Events. Component outputs are dispatched as HTML Custom Events. The event name matches the output name, and the emitted data is available on the event’s detail property .
@Component({ selector: 'my-popup', template: '...' })
export class PopupComponent {
@Output() valueChanged = new EventEmitter<string>();
}
<my-popup></my-popup>
<script>
document.querySelector('my-popup').addEventListener('valueChanged', (event) => {
console.log(event.detail);
});
</script>
An alias on the output changes the event name: @Output('myClick') clicks = new EventEmitter<string>() dispatches events named myClick .
c. Standalone components and modern configuration
Angular 17 and later use standalone components by default, and the Angular Elements workflow reflects this. The createApplication function bootstraps the Angular platform without a root component, and createCustomElement takes the standalone component directly .
// elements.ts
import { createCustomElement } from '@angular/elements';
import { createApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { appConfig } from './app.config';
createApplication(appConfig).then((appRef) => {
const element = createCustomElement(AppComponent, {
injector: appRef.injector,
});
customElements.define('pathway-browser', element);
});
The build uses --output-hashing none to produce filenames that are predictable for inclusion in a host page:
ng build --configuration production --output-hashing none
The host page includes the generated scripts and uses the custom element tag:
<!DOCTYPE html>
<html>
<head>
<script src="dist/main.js" type="module"></script>
</head>
<body>
<pathway-browser pathway-id="R-HSA-123456"></pathway-browser>
</body>
</html>
This pattern allows a standalone component to function both as part of an Angular application and as a standalone web component, with all standalone features—signals, inputs, outputs—preserved .
d. Zone.js and zoneless configuration
By default, Angular Elements relies on Zone.js for change detection. When the element is used in a non-Angular host, Zone.js monkey-patches browser APIs to detect asynchronous operations and trigger change detection. This can cause conflicts with other frameworks that also patch the same APIs, and it increases the bundle size of every element .
Modern Angular supports zoneless change detection, which is the recommended configuration for Angular Elements. Zoneless change detection removes the Zone.js dependency, eliminating the API conflicts and reducing the bundle size. However, it requires explicit configuration: the component must use signals or manually trigger change detection for updates .
The guidance is to ensure all Angular applications on the same page use zoneless mode if any of them do. Mixing zoneless and Zone.js elements on the same page can cause inconsistent change detection behavior .
Complete Example Session
# ============================================
# PART 1: INSTALL ANGULAR ELEMENTS
# ============================================
npm install @angular/elements
// ============================================
// PART 2: CREATE A COMPONENT
// ============================================
// popup.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'my-popup',
standalone: true,
template: `
<span>Popup: {{ message }}</span>
<button type="button" (click)="closed.emit()">✖</button>
`,
})
export class PopupComponent {
@Input() message = '';
@Output() closed = new EventEmitter<void>();
}
// ============================================
// PART 3: CONVERT TO CUSTOM ELEMENT
// ============================================
// main.ts
import { createApplication } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { PopupComponent } from './popup.component';
(async () => {
const app = await createApplication({ providers: [] });
const popupElement = createCustomElement(PopupComponent, {
injector: app.injector,
});
customElements.define('my-popup', popupElement);
})();
<!-- ============================================
PART 4: USE THE ELEMENT IN HTML
============================================ -->
<my-popup message="Use Angular!"></my-popup>
// ============================================
// PART 5: LISTEN TO OUTPUT EVENTS
// ============================================
const popup = document.querySelector('my-popup');
popup.addEventListener('closed', () => {
console.log('Popup closed');
});
<!-- ============================================
PART 6: PASS COMPLEX DATA VIA PROPERTY
============================================ -->
<pathway-browser id="browser"></pathway-browser>
<script>
const browser = document.getElementById('browser');
browser.pathwayId = 'R-HSA-123456';
</script>
// ============================================
// PART 7: BUILD FOR PRODUCTION
// ============================================
// No output hashing for predictable filenames
ng build --configuration production --output-hashing none
<!-- ============================================
PART 8: INCLUDE IN A HOST PAGE
============================================ -->
<!DOCTYPE html>
<html>
<head>
<script src="dist/main.js" type="module"></script>
</head>
<body>
<my-popup message="Hello from the host page"></my-popup>
</body>
</html>
// ============================================
// PART 9: AVOID SELECTOR CONFLICT
// ============================================
// Component selector: 'my-popup'
// Custom element tag: 'app-popup' (different)
customElements.define('app-popup', popupElement);
// ============================================
// PART 10: ZONELESS CONFIGURATION
// ============================================
// Use zoneless change detection for elements
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
const app = await createApplication({
providers: [provideExperimentalZonelessChangeDetection()],
});
These ten parts cover installation, component creation, conversion, HTML usage, event listening, property assignment, production build, host page integration, selector conflict avoidance, and zoneless configuration.
Quick Reference
Essential API
| API | Purpose |
|---|---|
createCustomElement() | Convert component to custom element class |
customElements.define() | Register tag name with browser |
NgElement | Type for the custom element instance |
NgElementConfig | Configuration for createCustomElement |
CUSTOM_ELEMENTS_SCHEMA | Allow custom element tags in Angular templates |
Input/Output Mapping
| Angular | DOM |
|---|---|
@Input() name | Attribute name |
@Input('myInputProp') | Attribute my-input-prop |
@Output() changed | Event changed |
@Output('myClick') | Event myClick |
| Emitted value | event.detail |
Configuration Options
| Option | Purpose |
|---|---|
injector | Provides dependency injection context |
strategyFactory | Custom change detection strategy |
exposedMethods | Component methods exposed on element |
Best Practices
| Practice | Reason |
|---|---|
| Avoid selector as tag name | Prevents double instantiation |
| Use zoneless | Avoids Zone.js conflicts |
| Build without output hashing | Predictable filenames for host pages |
| Use aliases for inputs | HTML attributes cannot have uppercase |
| Exclude from compilation | Remove from angular.json build if not used in app |
Best Practices
✅ Do This:
const element = createCustomElement(PopupComponent, { injector });
customElements.define('app-popup', element); // Different from selector
<app-popup message="Hello"></app-popup>
document.querySelector('app-popup').addEventListener('closed', handler);
❌ Don’t Do This:
customElements.define('my-popup', element); // ❌ Same as selector
<my-popup></my-popup> <!-- ❌ Creates two instances -->
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Element not rendering | Tag not registered | Call customElements.define() |
| Input not updating | Attribute name case mismatch | Use dash-separated lowercase |
| Output not firing | Listening on wrong event name | Match event name exactly |
| Zone.js conflict | Host page uses another framework | Use zoneless change detection |
| Two instances created | Selector used as tag name | Use a distinct tag name |
| Bundle size large | Zone.js and Angular runtime included | Use zoneless; consider tree-shaking |
Real-World Examples
1. Popup Widget
@Component({ selector: 'my-popup', template: '...' })
export class PopupComponent {
@Input() message = '';
@Output() closed = new EventEmitter<void>();
}
const element = createCustomElement(PopupComponent, { injector });
customElements.define('app-popup', element);
2. Search Form Embedded in a CMS
<my-search-form default-query="angular"></my-search-form>
3. Standalone Component as Web Component
createApplication(appConfig).then((appRef) => {
const element = createCustomElement(AppComponent, {
injector: appRef.injector,
});
customElements.define('pathway-browser', element);
});
4. Complex Input via Property
<pathway-browser></pathway-browser>
<script>
document.querySelector('pathway-browser').pathwayId = 'R-HSA-123456';
</script>
5. Event with Data
@Output() countChange = new EventEmitter<number>();
element.addEventListener('countChange', (e) => {
console.log(e.detail); // The emitted number
});
6. Zoneless Element
const app = await createApplication({
providers: [provideExperimentalZonelessChangeDetection()],
});
7. Build Without Hashing
ng build --configuration production --output-hashing none
8. Micro-Frontend Registration
const elements = [ButtonComponent, CardComponent, InputComponent];
elements.forEach(comp => {
const el = createCustomElement(comp, { injector });
customElements.define(comp.selector, el);
});
9. Exclude from App Compilation
// angular.json: remove from app's build if not used in app
10. CUSTOM_ELEMENTS_SCHEMA in Templates
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class HostComponent {}
Visual
Custom Element Lifecycle
┌──────────────────────────────────────────────────────────────┐
│ createCustomElement(PopupComponent, { injector }) │
│ │ │
│ ▼ │
│ Class extends HTMLElement │
│ │ │
│ ▼ │
│ customElements.define('app-popup', Class) │
│ │ │
│ ▼ │
│ Browser sees <app-popup> │
│ │ │
│ ▼ │
│ connectedCallback() → bootstrap Angular component │
│ │ │
│ ▼ │
│ Component renders with change detection │
│ │ │
│ ▼ │
│ disconnectedCallback() → destroy component │
└──────────────────────────────────────────────────────────────┘
Input/Output Mapping
┌──────────────────────────────────────────────────────────────┐
│ ANGULAR COMPONENT DOM CUSTOM ELEMENT │
│ │
│ @Input() name <app-popup name="Alice"> │
│ @Input('myInputProp') <app-popup my-input-prop="..."> │
│ │
│ @Output() changed element.addEventListener( │
│ @Output('myClick') 'changed', handler) │
│ emitter.emit(data) event.detail === data │
└──────────────────────────────────────────────────────────────┘
Selector Conflict
┌──────────────────────────────────────────────────────────────┐
│ CORRECT WRONG │
│ │
│ Component selector: my-popup Component selector: my-popup │
│ Custom tag: app-popup Custom tag: my-popup │
│ │
│ One instance Two instances │
│ Angular + custom element Angular + custom element │
│ work together conflict │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Angular Elements | Package Angular components as custom elements |
| Package | @angular/elements |
| Conversion API | createCustomElement(component, config) |
| Registration | customElements.define('tag-name', elementClass) |
| Inputs | Mapped to dash-case attributes |
| Outputs | Dispatched as Custom Events |
| Event data | event.detail |
| Bootstrap | Automatic on DOM connection |
| Teardown | Automatic on DOM disconnection |
| Zone.js | Optional; zoneless recommended |
| Selector conflict | Avoid using selector as tag name |
Key takeaways:
- Angular Elements packages Angular components as custom elements. The component becomes a standard HTML tag that any framework can use. The Angular infrastructure travels with the element, so the host needs no Angular knowledge .
createCustomElement()does the conversion. It takes the component class and an injector, and returns a class the browser can register withcustomElements.define().- Inputs become attributes; outputs become Custom Events. Input names are transformed to dash-separated lowercase. Output events carry their data on
event.detail. - Avoid using the component selector as the custom element tag. Matching names cause Angular to create two component instances for one DOM element .
- Standalone components work with the modern
createApplicationAPI. The pattern bootstraps the platform without a root component, then registers the custom element with the platform’s injector . - Zone.js is the traditional change detection mechanism but is problematic for elements. Zoneless change detection is recommended for Angular Elements because it avoids conflicts with other frameworks and reduces bundle size .
- Complex objects are passed as properties, not attributes. HTML attributes are strings. For objects and arrays, assign the value to a JavaScript property on the element instance .
Remember: Angular Elements is the bridge between Angular’s component model and the browser’s custom element standard. It packages a component with its dependencies and Angular infrastructure into a self-bootstrapping HTML element. The component’s inputs and outputs map to standard DOM attributes and events, so the consumer interacts with it as they would with any other element. The primary use cases are sharing components across teams and frameworks, embedding widgets in non-Angular applications, and incremental migration of legacy applications. The primary concerns are Zone.js conflicts and bundle size, both addressed by zoneless change detection. The selector conflict—using the component’s selector as the custom element tag—is the most common mistake and produces two instances for one element. When used deliberately, Angular Elements extends the reach of Angular code beyond the Angular application boundary.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!