Custom HTML Elements for Widget Embedding

You embed a third-party widget on your site and an hour later find that buttons have turned red and scripts conflict with jQuery. Sound familiar? We offer a solution: custom HTML elements with Shadow DOM permanently eliminate this headache. The client inserts three lines of code and gets a working w

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1283
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1238
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    980
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1029
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1104
  • image_website-_0.webp
    Website development for Red Pear
    552

You embed a third-party widget on your site and an hour later find that buttons have turned red and scripts conflict with jQuery. Sound familiar? We offer a solution: custom HTML elements with Shadow DOM permanently eliminate this headache. The client inserts three lines of code and gets a working widget, isolated from the host site's environment. A third-party widget on someone else's site means someone else's DOM, someone else's styles, potentially conflicting library versions. Custom Elements v1 solve this problem radically: no CSS conflicts, no broken scripts. The load time for such a widget is 0.5–1.5s (LCP), whereas an iframe alternative takes 2–3s. This is a web component implementation that provides seamless integration and complete isolation.

How Custom HTML Elements Solve Website Conflicts

  • Style conflicts: The widget's internal styles may accidentally override the site's styles (e.g., button { background: red }). Shadow DOM guarantees isolation in both directions.
  • Version management: The widget is connected via a single script with an SRI hash. The client will not accidentally update the widget — only by intentionally changing the version in the URL.
  • Cross-browser support: Shadow DOM works in all modern browsers. For IE11 we include a polyfill. We test on real devices.
  • SEO: Unlike iframes, content in Custom Elements is indexed by search engines. This is critical for review, rating, and catalog widgets.

What Shadow DOM Isolation Provides

Shadow DOM is the only reliable solution. We create a <template> with encapsulated styles and structure. Here is a minimal example:

<review-widget data-product-id="SKU-12345" data-theme="light" data-locale="en" ></review-widget> 

The element loads data via API and renders reviews. Styles are isolated — no * { all: unset } from the host page can break the widget.

Why Custom Elements Are Better Than iframes for Widgets

Criterion Custom Elements iframe
Load time (LCP) < 1.5s ~3s
SEO indexing Yes No
Responsiveness Automatic Requires wrapper
Script size ~15 KB (minified) > 50 KB
Style isolation Full (Shadow DOM) Full (separate document)
Integration complexity 3 lines of code Size setup and postMessage
Bundle size after tree-shaking ~8 KB ~30 KB
Traffic savings (per month) up to $2,000 $0

Custom Elements are 2–3 times faster than iframes in load time and do not hide content from search engines. Traffic savings up to 70% (up to $2,000 per month).

Browser Custom Elements Support Polyfill
Chrome 54+ Full No
Firefox 63+ Full No
Safari 10.1+ Full No
Edge 79+ Full No
IE11 None Required

How the Widget Works Internally

The architecture includes three layers: Custom Element (registers the tag, manages lifecycle), Shadow DOM (isolated styles and markup), and API client (loads data with pagination). Below is the full code for ReviewWidget:

// src/ReviewWidget.ts const TEMPLATE = document.createElement('template'); TEMPLATE.innerHTML = ` <style> :host { display: block; contain: content; font-family: var(--rw-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif); font-size: var(--rw-font-size, 14px); color: var(--rw-text-color, #1a1a2e); } :host([hidden]) { display: none; } .container { border: 1px solid var(--rw-border-color, #e5e7eb); border-radius: var(--rw-radius, 8px); padding: 16px; background: var(--rw-bg, #ffffff); } .rating { display: flex; align-items: center; gap: 4px; margin-bottom: 12px; } .star { color: #f59e0b; font-size: 18px; } .star.empty { color: #d1d5db; } .reviews-list { list-style: none; margin: 0; padding: 0; } .review-item { padding: 10px 0; border-top: 1px solid #f3f4f6; } .review-author { font-weight: 600; font-size: 13px; } .review-text { margin-top: 4px; line-height: 1.5; } .load-more { margin-top: 12px; width: 100%; padding: 8px; background: var(--rw-accent, #3b82f6); color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; } .load-more:hover { opacity: .9; } .skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 4px; height: 14px; margin: 6px 0; } @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } </style> <div class="container"> <div class="rating" aria-label="Product rating"></div> <ul class="reviews-list" role="list"></ul> <button class="load-more" style="display:none">Show more</button> </div> `; interface Review { id: string; author: string; rating: number; text: string; date: string; } export class ReviewWidget extends HTMLElement { static get observedAttributes() { return ['data-product-id', 'data-theme', 'data-locale']; } private shadow: ShadowRoot; private page = 1; private allLoaded = false; private apiBase = '/api/reviews'; constructor() { super(); this.shadow = this.attachShadow({ mode: 'open' }); this.shadow.appendChild(TEMPLATE.content.cloneNode(true)); } connectedCallback() { this.applyTheme(); this.fetchReviews(true); this.shadow.querySelector('.load-more') ?.addEventListener('click', () => this.fetchReviews(false)); } attributeChangedCallback(name: string, old: string, next: string) { if (old === next) return; if (name === 'data-product-id') { this.page = 1; this.allLoaded = false; this.fetchReviews(true); } if (name === 'data-theme') { this.applyTheme(); } } private applyTheme() { const theme = this.dataset.theme ?? 'light'; if (theme === 'dark') { const container = this.shadow.querySelector<HTMLElement>('.container'); if (container) { container.style.setProperty('--rw-bg', '#1f2937'); container.style.setProperty('--rw-text-color', '#f9fafb'); container.style.setProperty('--rw-border-color', '#374151'); } } } private showSkeleton() { const list = this.shadow.querySelector('.reviews-list')!; list.innerHTML = Array(3).fill( '<li><div class="skeleton"></div><div class="skeleton" style="width:70%"></div></li>' ).join(''); } private async fetchReviews(reset: boolean) { const productId = this.dataset.productId; if (!productId) return; if (reset) { this.page = 1; this.showSkeleton(); } try { const url = new URL(`${this.apiBase}/${productId}`, window.location.origin); url.searchParams.set('page', String(this.page)); url.searchParams.set('per_page', '5'); url.searchParams.set('locale', this.dataset.locale ?? 'en'); const res = await fetch(url.toString(), { headers: { 'X-Widget-Version': '2.1.0' }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data: { reviews: Review[]; total: number; avg_rating: number } = await res.json(); this.renderRating(data.avg_rating, data.total); this.renderReviews(data.reviews, reset); this.allLoaded = data.reviews.length < 5; const btn = this.shadow.querySelector<HTMLElement>('.load-more'); if (btn) btn.style.display = this.allLoaded ? 'none' : 'block'; this.page++; // inform the host page this.dispatchEvent(new CustomEvent('reviews:loaded', { detail: { total: data.total, avgRating: data.avg_rating }, bubbles: true, composed: true, })); } catch (err) { this.renderError(); } } private renderRating(avg: number, total: number) { const ratingEl = this.shadow.querySelector('.rating')!; const stars = Array.from({ length: 5 }, (_, i) => `<span class="star ${i < Math.round(avg) ? '' : 'empty'}">★</span>` ).join(''); ratingEl.innerHTML = `${stars} <span>${avg.toFixed(1)} (${total} reviews)</span>`; ratingEl.setAttribute('aria-label', `Rating ${avg.toFixed(1)} out of 5, ${total} reviews`); } private renderReviews(reviews: Review[], reset: boolean) { const list = this.shadow.querySelector('.reviews-list')!; if (reset) list.innerHTML = ''; const locale = this.dataset.locale ?? 'en'; const dateFormatter = new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long', day: 'numeric', }); reviews.forEach(r => { const li = document.createElement('li'); li.className = 'review-item'; li.innerHTML = ` <div class="review-author">${r.author} <time datetime="${r.date}" style="font-weight:400;color:#6b7280;margin-left:8px"> ${dateFormatter.format(new Date(r.date))} </time> </div> <div class="review-text">${r.text}</div> `; list.appendChild(li); }); } private renderError() { this.shadow.querySelector('.reviews-list')!.innerHTML = '<li style="color:#ef4444;padding:8px 0">Failed to load reviews</li>'; } } customElements.define('review-widget', ReviewWidget); 

The loader script is included once and handles existing instances in the DOM:

// src/loader.ts import { ReviewWidget } from './ReviewWidget'; if (!customElements.get('review-widget')) { customElements.define('review-widget', ReviewWidget); } if (!window.customElements) { console.warn('[review-widget] Custom Elements not supported'); } 

Publishing via CDN with an SRI hash ensures resource integrity. The client will not update the widget accidentally — only by intentionally changing the version in the URL.

When iframe is unavoidableFor IE11 and browsers older than 2017, Custom Elements require a polyfill. If your site's audience includes such users (share >5%), we include a polyfill or offer a hybrid scheme: for old browsers – iframe with postMessage, for modern – Custom Elements. Development time increases by 1–2 weeks.

How Development and Integration Work

  1. Analysis: We study the client's environment: browsers, libraries, SEO requirements. We measure current LCP and CLS.
  2. Design: Define API attributes, events, data structure. Design Shadow DOM.
  3. Implementation: Write Custom Element, styles, API client. Configure build (Rollup + Terser) and CDN.
  4. Testing: Test on real host sites: Chrome 54+, Firefox 63+, Safari 10.1+, Edge 79+. Measure LCP (< 1.5s), CLS (< 0.1), INP (< 200ms).
  5. Deployment: Upload to CDN with SRI hash, provide client with script and documentation.

What's Included

  • Development of a custom HTML element with Shadow DOM isolation.
  • Integration documentation (script, attributes, events).
  • Testing in target browsers.
  • CDN hosting with SRI hash and versioning.
  • One month of post-launch support (bug fixes, consultations).

Estimated Timeline

For a single widget of moderate complexity: 2 to 4 weeks. Complexity increases if iframe fallback is required: add up to 2 weeks for postMessage communication. Cost is calculated individually based on your stack and requirements. Request a preliminary audit — the audit fee is deducted from the development cost. We will analyze the environment and propose the optimal solution.

According to MDN Custom Elements, elements must be registered via customElements.define.

We have over 5 years of web development experience and have completed more than 30 widget embedding projects, delivering consistent LCP under 1.5 seconds and traffic savings of up to 70% (over $2,000 per month). Contact us for a project evaluation — we will analyze the environment and propose the optimal solution. Get a consultation or order widget development today.