LEARNING OBJECTIVES โต
- Understand the technical distinctions between HTML Attributes (string serialization) and DOM Properties (rich JavaScript references).
- Master the integration mechanics across React (including React 19's native Custom Elements support), Vue 3, and Angular.
- Implement property getters and setters with internal state caching for rich object passing (arrays, functions, objects).
- Dispatch and handle native
CustomEventpayloads across framework boundaries without memory leaks.
๐ The Mental Model & Story (Intuitive Foundation)
Imagine audio-visual hardware.
If you buy a proprietary wireless streaming protocol (e.g., Apple AirPlay or Google Cast), it works seamlesslyโas long as every sender and receiver belongs to that exact ecosystem. But if you try to beam audio from a proprietary device to an incompatible soundbar, you run into protocol walls.
Now consider the physical 3.5mm headphone jack or the universal HDMI cable. It doesn't matter if your source is a Sony camera, a Nintendo Switch, an Apple laptop, or a Linux workstationโthe HDMI standard defines exact physical pins and electrical signals.
+-------------------------------------------------------------------------------+
| THE UNIVERSAL CONNECTOR (STANDARD DOM) |
+-------------------------------------------------------------------------------+
| React 19 App Vue 3 Dashboard Angular Admin Static SSR |
| | | | | |
| +----------------------+-------------------+-------------------+ |
| | |
| v |
| [Standard DOM API: Properties, Attributes, Events] |
| | |
| v |
| <enterprise-grid></enterprise-grid> |
+-------------------------------------------------------------------------------+
Framework components (React JSX, Vue SFCs, Angular Templates) are proprietary internal protocols. Web Components are the universal HDMI standard of the browser. Every framework ultimately renders to the standard HTML DOM; therefore, every framework can communicate with Web Components through standard DOM properties, attributes, and events.
Technical Deep Dive & Specifications
The Fundamental Dichotomy: Attributes vs. Properties
One of the most frequent sources of confusion for frontend engineers is the distinction between HTML Attributes and DOM Properties:
+-------------------------------------------------------------------------------+
| HTML ATTRIBUTE (Declarative / Serialized) |
| <user-card user-id="4812" is-admin="true"></user-card> |
| - Always parsed as a STRING |
| - Visible in the HTML source code and DevTools Elements panel |
| - Inspected via element.getAttribute('user-id') |
+-------------------------------------------------------------------------------+
VS
+-------------------------------------------------------------------------------+
| DOM PROPERTY (Imperative / In-Memory Object Reference) |
| element.userData = { id: 4812, roles: ['admin', 'billing'] }; |
| - Can store ANY JavaScript type: Objects, Arrays, Functions, Symbols |
| - Lives in JavaScript heap memory; not serialized to HTML string |
| - Accessed directly via element.userData |
+-------------------------------------------------------------------------------+
Framework Interoperability Breakdown
+-----------------------------------------------------------------------------------------+
| FRAMEWORK INTEGRATION MATRIX |
+-------------------+--------------------------------+------------------------------------+
| Framework | Property & Rich Data Passing | Custom Event Handling |
+-------------------+--------------------------------+------------------------------------+
| React (<=18) | โ ๏ธ Passes everything as string | โ ๏ธ Synthetic events ignore DOM |
| | attributes; requires `ref` | CustomEvents; requires manual |
| | property assignment. | `ref.addEventListener()`. |
+-------------------+--------------------------------+------------------------------------+
| React 19+ | โ
Native support: Sets props | โ
Native support: Listens directly |
| | if present on prototype. | via `onCustomEventName={fn}`. |
+-------------------+--------------------------------+------------------------------------+
| Vue 3 | โ
Native support via `:` | โ
Native support via `@` |
| | (e.g. `:items="dataList"`). | (e.g. `@row-select="onSelect"`). |
+-------------------+--------------------------------+------------------------------------+
| Angular (v14+) | โ
Native support via `[...]` | โ
Native support via `(...)` |
| | (e.g. `[items]="dataList"`). | (Requires CUSTOM_ELEMENTS_SCHEMA). |
+-------------------+--------------------------------+------------------------------------+
| Svelte / Solid | โ
100% Native DOM binding | โ
100% Native DOM binding |
+-------------------+--------------------------------+------------------------------------+
1. Integrating with React 19 vs React 18
Historically (React 16 through 18), React treated custom elements as standard HTML tags, stringifying all props into attributes (items="[object Object]") and refusing to capture custom events through JSX onEvent handlers.
In React 19, React fully adopted the Custom Elements standard:
- If a prop matches a property key on the custom element prototype (
'userData' in customElement), React assigns it as a DOM property. - If no matching property exists, React sets it as an HTML attribute.
- React 19 JSX listens to native custom events directly:
<ui-modal onModalClose={handleClose} />.
2. Integrating with Vue 3
Vue has provided first-class support for Web Components since inception. You only need to tell Vue's template compiler not to mistake your custom elements for missing Vue components:
// vite.config.js (Vue 3)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// Treat all tags starting with 'ui-' as native Custom Elements
isCustomElement: (tag) => tag.startsWith('ui-')
}
}
})
]
});
3. Integrating with Angular
In Angular, import CUSTOM_ELEMENTS_SCHEMA in your standalone component or NgModule:
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<ui-datagrid [rows]="users" (rowSelect)="handleRow($event)"></ui-datagrid>
`
})
export class AppComponent {
users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
handleRow(event: any) {
console.log('Selected row:', event.detail);
}
}
๐ป Interactive Code Playground
Let's build a <data-table-grid> custom element that accepts complex nested JavaScript arrays via a DOM property setter and emits a rich row-select CustomEvent.
Starter Code
Line-by-Line Code Breakdown
- Line 49:
get data()/set data(value): Exposes a standard JavaScript property getter and setter on the custom element instance. - Line 52:
this._data = value; this.render();: When an application framework setselement.data = [...], the custom element intercepts the assignment and triggers a targeted render. - Line 64:
new CustomEvent('row-select', { detail: ..., composed: true }): Dispatches the custom event payload with standard W3C DOM structure. - Line 144:
grid.data = [ ... ]: Demonstrates how React 19, Vue, or Angular binds rich nested array data without stringification.
Expected Browser Render Output
A dark-themed data table appears containing three employee records. Clicking any row immediately renders the serialized JSON event payload (with selectedRow details and timestamp) in the preview box beneath the table.
๐๏ธ Hands-On Exercise
๐ฏ The Challenge: Bidirectional <search-combobox>
Build an interactive custom element named <search-combobox> that receives an array of string items via a property and fires an event when an option is selected.
Instructions:
- Expose a property
items(array of strings) with a getter and setter. - Render an
<input type="text">and a dropdown filter list in the Shadow DOM. - As the user types in the input, filter the dropdown choices in real time.
- When a user clicks a dropdown item, set the input value and dispatch a
CustomEvent('change', { detail: { value: selectedItem }, bubbles: true, composed: true }).
๐ Starter Code Sandbox
โ ๏ธ Common Pitfalls
- Stringifying Complex Objects in HTML Attributes: Writing
<user-card user='{"id": 1}'></user-card>forces repeated JSON parsing on every render and breaks when strings contain unescaped quotes. Always pass complex objects via DOM properties (element.user = { id: 1 }). - Forgetting
composed: trueon Custom Events: If you dispatchnew CustomEvent('my-event', { bubbles: true })inside Shadow DOM, the event will bubble to the shadow root and stop. You must addcomposed: truefor the event to cross the shadow boundary into the light DOM.
๐ก Pro Tips
- Property Reflection Invariant: For primitive types (strings, numbers, booleans), reflect properties to attributes (e.g.
set disabled(val)callsthis.toggleAttribute('disabled', Boolean(val))). For complex objects and arrays, never reflect to attributesโkeep them strictly in DOM properties. - Automated Framework Wrappers: When shipping a commercial design system, use tools like
@lit/reactor Stencil framework targets to generate thin, TypeScript-typed wrapper components for React and Angular automatically.
๐ Key Takeaways
- HTML Attributes are string-based and declarative; DOM Properties live in JavaScript heap memory and handle rich data types.
- React 19 natively supports Custom Elements, setting DOM properties and binding custom events directly in JSX.
- Vue 3 and Angular support Web Components out of the box with simple compiler/schema configurations.
- Custom events dispatched inside Shadow DOM must specify
{ bubbles: true, composed: true }to reach outer framework listeners. - Never reflect large objects or arrays to HTML attributes; store them in private instance fields with getters and setters.
- --