Реалізація кастомних HTML-елементів для вбудовування віджетів

Ви вбудовуєте сторонній віджет на свій сайт і за годину виявляєте, що кнопки пофарбувалися в червоний, а скрипти почали конфліктувати з jQuery. Знайома ситуація? Ми пропонуємо рішення: кастомні HTML-елементи з **Shadow DOM** назавжди усувають цей головний біль. Клієнт вставляє три рядки коду й отрим

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Реалізація кастомних HTML-елементів для вбудовування віджетів
Середній
~3-5 днів

Наші компетенції:

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1419
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1287
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    983
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1245
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    983
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    998

Ви вбудовуєте сторонній віджет на свій сайт і за годину виявляєте, що кнопки пофарбувалися в червоний, а скрипти почали конфліктувати з jQuery. Знайома ситуація? Ми пропонуємо рішення: кастомні HTML-елементи з Shadow DOM назавжди усувають цей головний біль. Клієнт вставляє три рядки коду й отримує працюючий віджет, ізольований від оточення хост-сайту. Сторонній віджет на чужому сайті — це чужа DOM, чужі стилі, потенційно конфліктуючі версії бібліотек. Custom Elements v1 вирішують цю проблему кардинально: жодних конфліктів CSS, жодних зламаних скриптів. Час завантаження такого віджета — 0.5–1.5 с (LCP), тоді як iframe-альтернатива потребуватиме 2–3 с. Це реалізація веб-компонентів, яка дає безшовну інтеграцію та повну ізоляцію.

Як кастомні HTML-елементи вирішують проблему конфліктів на сайті?

  • Конфлікт стилів. Внутрішні стилі віджета можуть випадково перевизначити стилі сайту (наприклад, button { background: red }). Shadow DOM гарантує ізоляцію в обидва боки.
  • Управління версіями. Віджет підключається одним скриптом з SRI-хешем. Клієнт не оновить віджет випадково — лише навмисно змінивши версію в URL.
  • Кросбраузерність. Shadow DOM працює у всіх сучасних браузерах. Для IE11 підключаємо поліфіл. Тестуємо на реальних пристроях.
  • SEO. На відміну від iframe, контент у Custom Element індексується пошуковиками. Це критично для віджетів відгуків, рейтингів, каталогів.

Що дає ізоляція Shadow DOM?

Shadow DOM — єдине надійне рішення. Ми створюємо <template> з інкапсульованими стилями та структурою. Ось мінімальний приклад:

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

Елемент сам завантажує дані через API та рендерить відгуки. Стилі ізольовані — жодні * { all: unset } хост-сторінки не зламають віджет.

Чому Custom Elements кращі за iframe для віджетів?

Критерій Custom Elements iframe
Час завантаження (LCP) < 1.5 с ~3 с
SEO-індексація Так Ні
Адаптивність Автоматична Потрібна обгортка
Розмір скрипта ~15 КБ (мініфікований) > 50 КБ
Ізоляція стилів Повна (Shadow DOM) Повна (окремий документ)
Складність інтеграції 3 рядки коду Налаштування розмірів та postMessage
Розмір бандла після tree-shaking ~8 КБ ~30 КБ
Економія трафіку (на міс.) до 2 000 $ 0 $

Custom Elements в 2–3 рази швидші за iframe за часом завантаження і не приховують контент від пошуковиків. Економія трафіку — до 70% (до 2 000 $ на місяць).

Браузер Підтримка Custom Elements Поліфіл
Chrome 54+ Повна Ні
Firefox 63+ Повна Ні
Safari 10.1+ Повна Ні
Edge 79+ Повна Ні
IE11 Відсутня Потрібен

Як влаштований віджет зсередини

Архітектура включає три шари: Custom Element (реєструє тег, керує життєвим циклом), Shadow DOM (ізольовані стилі та розмітка), API-клієнт (завантажує дані з пагінацією). Нижче — повний код 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="Рейтинг товара"></div> <ul class="reviews-list" role="list"></ul> <button class="load-more" style="display:none">Показати ще</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 = 'https://api.example.com/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}`); url.searchParams.set('page', String(this.page)); url.searchParams.set('per_page', '5'); url.searchParams.set('locale', this.dataset.locale ?? 'ru'); 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++; // інформуємо хост-сторінку 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} відгуків)</span>`; ratingEl.setAttribute('aria-label', `Рейтинг ${avg.toFixed(1)} з 5, ${total} відгуків`); } private renderReviews(reviews: Review[], reset: boolean) { const list = this.shadow.querySelector('.reviews-list')!; if (reset) list.innerHTML = ''; const locale = this.dataset.locale ?? 'ru'; 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">Не вдалося завантажити відгуки</li>'; } } customElements.define('review-widget', ReviewWidget); 

Скрипт завантаження підключається один раз і обробляє вже існуючі в 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 не підтримуються'); } 

Публікація через CDN з SRI-хешем гарантує цілісність ресурсу. Клієнт не оновить віджет випадково — лише навмисно змінивши версію в URL.

Коли без iframe не обійтисяДля IE11 та браузерів старших за 2017 рік Custom Elements потребують поліфіл. Якщо аудиторія вашого сайту включає таких користувачів (частка >5%), ми підключаємо поліфіл або пропонуємо гібридну схему: для старих браузерів — iframe з postMessage, для сучасних — Custom Elements. Термін розробки збільшується на 1–2 тижні.

Як відбувається розробка та інтеграція?

  1. Аналітика. Вивчаємо оточення клієнта: браузери, бібліотеки, вимоги до SEO. Заміряємо поточний LCP та CLS.
  2. Проектування. Визначаємо API атрибутів, подій, структуру даних. Проектуємо Shadow DOM.
  3. Реалізація. Пишемо Custom Element, стилі, API-клієнт. Налаштовуємо збірку (Rollup + Terser) та CDN.
  4. Тестування. Перевіряємо на реальних хост-сайтах: Chrome 54+, Firefox 63+, Safari 10.1+, Edge 79+. Заміряємо LCP (< 1.5 с), CLS (< 0.1), INP (< 200 мс).
  5. Деплой. Завантажуємо на CDN з SRI-хешем, надаємо клієнту скрипт та документацію.

Що входить у роботу

  • Розробка кастомного HTML-елемента з ізоляцією через Shadow DOM.
  • Документація з інтеграції (скрипт, атрибути, події).
  • Тестування в цільових браузерах.
  • Розміщення на CDN з SRI-хешем та версіонуванням.
  • Підтримка протягом 1 місяця після запуску (виправлення багів, консультації).

Терміни орієнтовно

Для одного віджета середньої складності — від 2 до 4 тижнів. Складність зростає, якщо потрібен iframe-режим як fallback: додається до 2 тижнів на postMessage-комунікацію. Вартість розраховується індивідуально на основі вашого стеку та вимог. Замовте попередній аудит — його вартість 15 000 ₽ списується при замовленні розробки. Ми проаналізуємо оточення та запропонуємо оптимальне рішення.

Згідно з MDN Custom Elements, елементи повинні бути зареєстровані через customElements.define.

Ми займаємося веб-розробкою 5+ років, реалізували 30+ проектів з вбудовування віджетів. Зв'яжіться з нами для оцінки вашого проекту — ми проаналізуємо оточення та запропонуємо оптимальне рішення. Отримайте консультацію або замовте розробку віджета вже сьогодні.