06 - Web Components ​
1. Shadow DOM ​
What: An encapsulated DOM subtree attached to an element. CSS and JS inside the shadow DOM don't leak out, and outside styles don't leak in.
// Create shadow DOM
const host = document.getElementById('my-widget');
const shadow = host.attachShadow({ mode: 'open' }); // 'open' = accessible via JS
shadow.innerHTML = `
<style>
/* These styles ONLY apply inside the shadow DOM */
p { color: red; font-size: 20px; }
/* Global styles from the page DON'T affect this p */
</style>
<p>I'm inside shadow DOM</p>
`;mode: 'open' vs 'closed':
open:element.shadowRootreturns the shadow root (inspectable, accessible)closed:element.shadowRootreturns null (harder to inspect/test)
Style encapsulation:
Page CSS: p { color: blue; }
Shadow DOM CSS: p { color: red; }
Result: Shadow <p> is red, page <p> is blue. No leaking.Exceptions that DO pierce shadow DOM:
- Inherited properties:
font-family,color,line-height(via CSS inheritance) - CSS custom properties (CSS variables):
--my-colorcrosses shadow boundary :hostselector styles the shadow host from inside::part()selector styles named parts from outside
/* Inside shadow DOM — expose parts for external styling */
<button part="action-btn">Click me</button>
/* Outside — style the exposed part */
my-component::part(action-btn) { background: blue; }Why it matters for SDKs: A checkout SDK embedded in merchant pages NEEDS style isolation. Shadow DOM ensures the merchant's CSS doesn't break the SDK UI, and SDK CSS doesn't affect the merchant's page.
2. Custom Elements Lifecycle ​
What: Custom elements are user-defined HTML elements with their own behavior. They have a defined lifecycle, similar to React components.
class PaymentButton extends HTMLElement {
// 1. CONSTRUCTOR — element created (not yet in DOM)
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Don't access attributes or children here — element isn't connected yet
}
// 2. CONNECTED CALLBACK — element added to DOM (like componentDidMount)
connectedCallback() {
this.render();
this.addEventListener('click', this.handleClick);
// Safe to read attributes, fetch data, set up listeners
}
// 3. DISCONNECTED CALLBACK — element removed from DOM (like componentWillUnmount)
disconnectedCallback() {
this.removeEventListener('click', this.handleClick);
// Cleanup: remove listeners, cancel timers, abort fetches
}
// 4. ATTRIBUTE CHANGED CALLBACK — observed attribute changed (like componentDidUpdate)
static get observedAttributes() {
return ['amount', 'currency']; // only these trigger the callback
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
if (oldValue !== newValue) {
this.render(); // re-render on attribute change
}
}
// 5. ADOPTED CALLBACK — element moved to a new document (rare, via adoptNode)
adoptedCallback() {
// Almost never needed
}
private handleClick = () => {
this.dispatchEvent(new CustomEvent('payment-click', {
detail: { amount: this.getAttribute('amount') },
bubbles: true,
composed: true, // crosses shadow DOM boundary
}));
};
private render() {
this.shadowRoot!.innerHTML = `
<style>button { background: #4CAF50; color: white; padding: 12px 24px; }</style>
<button>Pay ${this.getAttribute('amount')} ${this.getAttribute('currency')}</button>
`;
}
}
// Register
customElements.define('payment-button', PaymentButton);<!-- Usage -->
<payment-button amount="500" currency="INR"></payment-button>Lifecycle order:
constructor → (attribute set) → connectedCallback → [attributeChangedCallback] → disconnectedCallbackImportant gotchas:
- Don't read attributes/children in constructor (element isn't in DOM yet)
attributeChangedCallbackfires BEFOREconnectedCallbackfor initial attributes- Only attributes listed in
observedAttributestrigger the callback - Properties !== attributes (attributes are strings, properties can be any type)
3. Web Components Interoperability ​
What: How Web Components interact with frameworks (React, Vue, Angular) and vice versa.
Using Web Components in React:
// React treats custom elements as HTML elements
function App() {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
// React doesn't set properties on custom elements — only attributes
// For complex data, use ref
ref.current?.addEventListener('payment-complete', handleComplete);
return () => ref.current?.removeEventListener('payment-complete', handleComplete);
}, []);
return <payment-widget ref={ref} amount="500" currency="INR" />;
}React 19 improvement: React 19 properly handles custom element properties:
// React 19 — sets properties directly instead of attributes
<payment-widget amount={500} onPaymentComplete={handleComplete} />
// Before React 19, complex values had to be set via refWrapping Web Components for React:
function PaymentWidget({ amount, currency, onComplete }: PaymentWidgetProps) {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
// Set complex properties
(el as any).config = { amount, currency };
const handler = (e: Event) => onComplete?.((e as CustomEvent).detail);
el.addEventListener('payment-complete', handler);
return () => el.removeEventListener('payment-complete', handler);
}, [amount, currency, onComplete]);
return <payment-widget ref={ref} />;
}Using React inside Web Components:
class ReactWidget extends HTMLElement {
connectedCallback() {
const root = createRoot(this.attachShadow({ mode: 'open' }));
root.render(<App />);
}
disconnectedCallback() {
// Cleanup React
}
}
customElements.define('react-widget', ReactWidget);Framework interop matrix:
| Attributes | Properties | Events | Slots | |
|---|---|---|---|---|
| React <19 | String only | Via ref | Via ref | Via children (limited) |
| React 19 | Yes | Yes | Yes | Via children |
| Vue | Yes | Yes (.prop) | Yes (@) | Yes (<slot>) |
| Angular | Yes | Yes ([]) | Yes (()) | Yes (<ng-content>) |
Why it matters for SDKs: A checkout SDK built as Web Components works in ANY framework the merchant uses. No React dependency for the SDK consumer.