Skip to content

Using in Frameworks

A custom element registered with customElements.define is a standard DOM node. Any framework that outputs HTML can use it. You place it in JSX, a Vue template, or an Angular template the same way you place a <div>.

Because frameworks ultimately produce real DOM nodes, they all interoperate with custom elements at the same level — the browser’s own element API. Properties, attributes, and events work the same way regardless of which framework is rendering the surrounding tree.

In React 18 and earlier, custom elements receive string attributes, not rich object props. Pass primitives (strings, numbers, booleans) as attributes. For complex data (arrays, objects) or for listening to custom events you must use a ref callback:

// React 18 — attach ref to wire up events
import { useRef, useEffect } from 'react';
function App() {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
el.addEventListener('my-event', (e) => console.log(e.detail));
return () => el.removeEventListener('my-event', () => {});
}, []);
return <my-counter ref={ref} label="Score"></my-counter>;
}

React 19 ships full custom element support. Properties and event handlers (prefixed with on) now bind directly without a ref workaround:

// React 19 — direct prop and event binding
function App() {
return (
<my-counter
label="Score"
count={0}
onmy-event={(e) => console.log(e.detail)}
/>
);
}

Vue treats custom elements as first-class citizens. Attributes and properties bind with :prop="value" (property) or attr="value" (attribute). Custom events are listened to with @event-name:

<!-- Vue template -->
<my-counter
:label="label"
:count="score"
@score-changed="handleChange"
/>

Vue automatically differentiates between a property (set on the DOM node) and an attribute (set via setAttribute) based on whether the key exists on the element’s prototype.

Angular supports custom elements via the CUSTOM_ELEMENTS_SCHEMA. Add it to your module’s schemas array to suppress unknown-element errors:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}

In templates, use [property] for property binding and (event-name) for custom events:

<!-- Angular template -->
<my-counter [label]="title" [count]="score" (score-changed)="onScoreChanged($event)"></my-counter>

The demo below shows a vanilla web component (<demo-badge>) rendered in plain HTML — the same element would work identically when dropped into a React, Vue, or Angular tree because it is just a DOM node.

OptionBenefitCost
Use the framework’s native interop directly (e.g. React 19 direct binding, Vue :prop)No added dependency, smaller bundle, and relies purely on the browser’s standard APIYou handle complex props/events yourself, especially on React 18 where you have to rely on ref
Use a wrapper library like @lit/reactGives a typed component with props/events already bound for you, less boilerplate for consumersAdds a dependency and an extra layer you have to keep in sync with the underlying element’s version
  • Building your own wrapper component when the framework already has native interop (e.g. React 19) — adds an unnecessary layer and maintenance burden when you could bind the property/event directly.
  • Passing an array/object as a JSX attribute on React 18 and being confused why the value is wrong — React 18 and earlier stringifies everything as an attribute. Use a ref to set the property directly, or upgrade to React 19.
  • Binding a custom event with plain attribute syntax instead of the framework’s specific syntax — forgetting @event-name in Vue or (event-name) in Angular means the event handler never gets called at all.

💡 Real-world examples

Google Material Web — the project’s documentation demonstrates using the same component across React, Angular, and Vue without writing a separate wrapper for each framework. (material-components/material-web)

Home Assistant — embeds Lit-built custom elements directly into its dashboard UI, mixed with other frontend code, through native DOM interop. (home-assistant/frontend)

In React 18, what is the recommended way to listen to a custom event fired by a web component?
What does Vue use to decide whether to set a value as a DOM property or an HTML attribute on a custom element?
Which Angular module token allows you to use unregistered custom element tag names without compile errors?