Shadow DOM Cross-Framework Usage — Complete Guide
In this tutorial, you will learn about Shadow Dom Cross. We cover key concepts, practical examples, and best practices to help you master this topic.
Shadow DOM cross-framework usage integrates Web Components seamlessly into React, Vue, and Angular applications, enabling framework-agnostic component libraries.
What You'll Learn
- How to use Shadow DOM components in React applications
- How to use Shadow DOM components in Vue applications
- How to use Shadow DOM components in Angular applications
- How to handle framework-specific quirks with web components
Why It Matters
Building component libraries with Shadow DOM allows them to work across frameworks without rewriting. Teams can maintain one component library and use it everywhere.
flowchart LR A[Shadow DOM Component Lib] --> B[React App] A --> C[Vue App] A --> D[Angular App] A --> E[Vanilla JS App] B --> F[React wrapper handles props/events] C --> G[Vue v-model integration] D --> H[Angular CUSTOM_ELEMENTS_SCHEMA]
Using Web Components in React
React requires a ref to access DOM properties and events on web components. React does not automatically pass props or listen to custom events.
// React wrapper for a Shadow DOM web component
import React, { useRef, useEffect } from 'react';
function MyWrapper({ value, onMyEvent, label }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
// Set properties (not attributes)
el.value = value;
// Listen for custom events
const handler = (e) => onMyEvent(e.detail);
el.addEventListener('my-event', handler);
return () => el.removeEventListener('my-event', handler);
}, [value, onMyEvent]);
// Use className to pass classes that penetrate the shadow root
// Regular HTML attributes like 'label' work natively
return <my-component ref={ref} class="my-class" label={label}></my-component>;
}
// Usage:
// <MyWrapper value="test" onMyEvent={(detail) => console.log(detail)} label="Input" />
Handling React Synthetic Events
React's synthetic event system does not bubble through shadow boundaries. Use native event listeners for custom events.
import React, { useRef, useEffect, useState } from 'react';
function CounterWrapper() {
const ref = useRef(null);
const [count, setCount] = useState(0);
useEffect(() => {
const el = ref.current;
// React's onClick won't work for events from inside Shadow DOM
// Use native addEventListener instead
const handler = (e) => {
setCount(e.detail.count);
};
el.addEventListener('count-changed', handler);
return () => el.removeEventListener('count-changed', handler);
}, []);
return (
<div>
<p>Count from component: {count}</p>
<my-counter ref={ref}></my-counter>
</div>
);
}
Using Web Components in Vue
Vue has excellent web component support. v-model can work with custom elements if they follow the convention.
// Vue 3 component using a Shadow DOM web component
<template>
<div>
<p>Value: {{ componentValue }}</p>
<!-- Vue automatically binds props -->
<!-- Use .prop modifier for property binding -->
<my-input
ref="inputRef"
:label="inputLabel"
:model-value.prop="componentValue"
@update:model-value="handleUpdate"
@my-event="handleMyEvent"
/>
</div>
</template>
<script>
export default {
data() {
return {
componentValue: '',
inputLabel: 'Enter text'
};
},
methods: {
handleUpdate(value) {
this.componentValue = value;
},
handleMyEvent(detail) {
console.log('Custom event:', detail);
}
}
};
</script>
Vue v-model with Web Components
Vue's v-model works with web components that emit an input event with composed: true and a value property.
// Web component designed for v-model
class VueFriendlyInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this._internals = this.attachInternals();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
input { padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
</style>
<input type="text" id="input">
`;
this.shadowRoot.getElementById('input').addEventListener('input', () => {
const value = this.shadowRoot.getElementById('input').value;
this._value = value;
// Dispatch input event for v-model compatibility
this.dispatchEvent(new CustomEvent('input', {
bubbles: true,
composed: true,
detail: value
}));
});
}
get value() { return this._value || ''; }
set value(val) {
this._value = val;
const input = this.shadowRoot.getElementById('input');
if (input) input.value = val;
}
}
customElements.define('vue-friendly-input', VueFriendlyInput);
// Vue usage: <vue-friendly-input v-model="data"></vue-friendly-input>
Using Web Components in Angular
Angular needs CUSTOM_ELEMENTS_SCHEMA to allow custom tag names.
// app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA], // Allow custom elements
bootstrap: [AppComponent]
})
export class AppModule { }
// app.component.ts
import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div>
<p>Value: {{componentValue}}</p>
<my-counter #counter></my-counter>
<button (click)="resetCounter()">Reset</button>
</div>
`
})
export class AppComponent implements AfterViewInit {
@ViewChild('counter', { static: false }) counterRef: ElementRef;
componentValue = 0;
ngAfterViewInit() {
const el = this.counterRef.nativeElement;
el.addEventListener('count-changed', (e) => {
this.componentValue = e.detail.count;
// Required for Angular change detection
this.ngZone.run(() => {});
});
}
resetCounter() {
this.counterRef.nativeElement.reset();
}
}
Framework-Agnostic Component Design
Design components to work well in any framework by following conventions.
class FrameworkReady extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host { display: inline-block; }
.wrapper { padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
</style>
<div class="wrapper">
<slot></slot>
</div>
`;
}
// 1. Use properties (not just attributes) for complex data
// 2. Dispatch composed custom events
// 3. Follow v-model conventions (value property + input event)
// 4. Expose public methods on the element
// 5. Use part attributes for styling
}
customElements.define('framework-ready', FrameworkReady);
Common Mistakes
- Using React's synthetic onClick and expecting it to handle events from inside Shadow DOM.
- Forgetting to use
.propmodifier in Vue for DOM property binding on custom elements. - Not adding CUSTOM_ELEMENTS_SCHEMA in Angular, causing template parse errors.
- Assuming framework change detection handles web component property changes automatically.
- Not dispatching events with
composed: trueso they exit the shadow tree and reach framework event handlers.
Practice Questions
- Why doesn't React's onClick work for events inside Shadow DOM? React's synthetic event system does not cross shadow boundaries.
- How do you make v-model work with web components in Vue? The component must have a
valueproperty and dispatch aninputevent withcomposed: true. - What Angular configuration is needed for web components? Add CUSTOM_ELEMENTS_SCHEMA to the module's schemas array.
- What is the most important event option for cross-framework compatibility?
composed: trueso events escape shadow boundaries.
Challenge
Build a counter web component with Shadow DOM that works identically in React, Vue, and Angular. The counter should have increment, decrement, and reset functionality. Dispatch a count-changed event with the current count. Test it in all three frameworks.
FAQ
Mini Project
Build a reusable date-picker web component with Shadow DOM. Create wrappers for React (hook-based), Vue (component wrapper), and Angular (directive/component). The date-picker should accept min/max dates, emit a date-selected event, and support v-model in Vue.
What's Next
Lesson 20: Advanced Shadow DOM Patterns
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro