Using in Frameworks
Web components are just DOM elements
Section titled “Web components are just DOM elements”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.
React 18 and earlier
Section titled “React 18 and earlier”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 eventsimport { 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
Section titled “React 19”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 bindingfunction 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
Section titled “Angular”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>Runnable demo
Section titled “Runnable demo”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.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
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 API | You handle complex props/events yourself, especially on React 18 where you have to rely on ref |
Use a wrapper library like @lit/react | Gives a typed component with props/events already bound for you, less boilerplate for consumers | Adds a dependency and an extra layer you have to keep in sync with the underlying element’s version |
Common mistakes
Section titled “Common mistakes”- 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
refto 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-namein 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)