Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Web Components Explained

By Kokil Thapa | Last reviewed: September 2026

You need UI that survives framework churn. Web Components Explained starts there: four browser standards that let you define your own HTML tags with encapsulated styles and behaviour. No build step is required for a basic component. That matters on Laravel, WordPress, and static pages where you cannot rewrite the whole front end. This guide walks through each standard, shows copy-paste code, and maps where native components fit beside Blade, Vue, and Alpine.

What are web components and why do they matter in 2026?

Web components are not a single library. They are four W3C-backed browser APIs that work together. You register a tag like <price-badge>. The browser renders it with your logic and scoped CSS. The same tag runs in a WordPress theme, a Shopify section, or a Laravel Blade layout.

Frameworks come and go. jQuery dominated for years. React owns large SPAs today. Server-driven stacks like Laravel Blade components and Livewire solve different problems. Web components sit below all of that. They are a portability layer.

On client projects I often inherit mixed stacks. A WooCommerce shop might need a custom product configurator. A legal portal might need a reusable document uploader. Dropping a self-contained custom element into existing markup beats rewriting the theme.

Web Components — Four StandardsCustomElementsNew HTML tagsShadowDOMStyle isolationHTMLTemplatesInert markupESModulesImport filesReusable ComponentWorks in any page or CMSLaravel Blade · WordPress · Shopify · Static HTMLNo framework lock-in required
Web Components Explained: four browser standards combine into portable, reusable UI elements.

The four pieces break down like this:

  • Custom Elements — register a tag name and attach a JavaScript class.
  • Shadow DOM — attach a hidden subtree with scoped CSS.
  • HTML Templates — store markup in <template> without rendering it.
  • ES Modules — load component files with import and type="module".

Browser support is solid in 2026. All evergreen browsers ship full support. Legacy IE is dead. If you still serve older Android WebViews, test once and polyfill only where analytics prove you need it. The MDN Web Components documentation tracks current API details.

How do custom elements work in practice?

Custom elements are the entry point. You extend HTMLElement, define a tag, and hook into the browser lifecycle. Autonomous custom elements create new tags. Customized built-in elements extend native tags like <button>. Most teams use autonomous elements because Safari support for customized built-ins remains limited.

Minimal working example

Save this as hello-badge.js and include it with a module script tag:

class HelloBadge extends HTMLElement {
  static get observedAttributes() {
    return ['label', 'variant'];
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (oldVal !== newVal) {
      this.render();
    }
  }

  render() {
    const label = this.getAttribute('label') ?? 'Hello';
    const variant = this.getAttribute('variant') ?? 'default';
    this.shadowRoot.innerHTML = `
      <style>
        .badge {
          display: inline-block;
          padding: 0.35rem 0.75rem;
          border-radius: 0.375rem;
          font-family: inherit;
          font-size: 0.875rem;
        }
        .badge--accent { background: #dc3545; color: #fff; }
        .badge--muted { background: #f8f9fa; color: #212529; }
      </style>
      <span class="badge badge--${variant}">${label}</span>
    `;
  }
}

customElements.define('hello-badge', HelloBadge);

Use it anywhere in HTML:

<script type="module" src="/js/hello-badge.js"></script>

<hello-badge label="Rs 1,500" variant="accent"></hello-badge>
<hello-badge label="Draft" variant="muted"></hello-badge>

The lifecycle callbacks matter in production. connectedCallback runs when the element enters the document. Use it for DOM setup and event listeners. disconnectedCallback runs on removal. Clean up timers and observers there. attributeChangedCallback reacts to attribute changes. List only the attributes you watch in observedAttributes.

Custom Element LifecycleconstructorconnectedCallbackattributeChangeddisconnectedCallbackSetup shadow root in constructorRender and bind events in connectedCallbackDo hereDOM renderFetch dataDo hereRemove listenersClear intervals
Custom element lifecycle: constructor, connected, attribute changes, and cleanup on disconnect.

A common mistake is fetching data inside the constructor. The element is not in the DOM yet. Wait for connectedCallback. Another mistake is re-binding click handlers on every render. That leaks memory. Use event delegation on the shadow root instead.

For typed properties versus attributes, expose both. Attributes work in plain HTML. Properties work when JavaScript sets values programmatically. Reflect simple string attributes. Keep complex objects as properties only.

What is the Shadow DOM and when should you use it?

Shadow DOM creates an encapsulated subtree attached to your element. Styles inside the shadow tree do not leak out. Global page CSS does not pierce in, unless you use :host or CSS custom properties intentionally.

That isolation solves real pain. Bootstrap utility classes on the page will not break your component internals. Your component CSS will not accidentally restyle the navbar. On a multi-currency WooCommerce storefront, a price widget with its own layout rules stays predictable inside any theme.

Open versus closed shadow roots

mode: 'open' exposes element.shadowRoot for debugging and testing. mode: 'closed' hides it. Closed mode is rarely worth the debugging cost. Use open mode unless you have a strict encapsulation requirement.

Slots for composable markup

Slots let consumers pass HTML into your component:

render() {
  this.shadowRoot.innerHTML = `
    <style>
      .card { border: 1px solid #dee2e6; border-radius: 0.5rem; padding: 1rem; }
      ::slotted(h3) { margin: 0 0 0.5rem; font-size: 1.1rem; }
    </style>
    <article class="card">
      <slot name="title"></slot>
      <slot>Default body text</slot>
    </article>
  `;
}
<info-card>
  <h3 slot="title">Court fee estimate</h3>
  <p>Use our calculator before filing.</p>
</info-card>

Named slots give you layout control. The default slot catches everything else. This pattern mirrors Blade component slots, but runs entirely in the browser.

Shadow DOM EncapsulationLight DOM — Page CSS (.btn, .card, h1)<price-widget> Host ElementShadow Root — Scoped CSS<slot name="label">Internal markup + eventsBlockedBlocked
Shadow DOM keeps page styles out and component styles in — core to Web Components Explained.

CSS custom properties pierce the boundary by design. Define tokens on :host and override them from outside:

:host {
  --badge-color: #2b6cff;
  display: inline-block;
}
.badge { background: var(--badge-color); }

That gives theme flexibility without losing encapsulation. It pairs well with Bootstrap 5 CSS variables on the host page.

How do you build a production-ready web component step by step?

Start small. Ship one widget. Wrap it. Test it in every target environment before you build a whole library.

  1. Pick one isolated UI piece. A star rating input, a copy-to-clipboard block, or a currency formatter works well.
  2. Define the public API. List attributes, properties, events, and slots. Document them in a comment block.
  3. Create the class file. Extend HTMLElement, attach an open shadow root, and register with customElements.define.
  4. Move markup into a template. Use <template id="..."> and clone it in the constructor for cleaner code.
  5. Dispatch custom events. Use this.dispatchEvent(new CustomEvent('change', { detail, bubbles: true })) so parent pages react without tight coupling.
  6. Bundle or ship raw modules. Vite 8.x works well for library builds. Plain ES modules are fine for a single tag on a CMS page.
  7. Test in target stacks. Drop the script into WordPress, Laravel, and any SPA shell you support.

Here is a template-based variant that scales better than string templates:

const template = document.createElement('template');
template.innerHTML = `
  <style>
    button { font-family: inherit; cursor: pointer; }
  </style>
  <button part="trigger" type="button">
    <slot>Copy</slot>
  </button>
`;

class CopyButton extends HTMLElement {
  constructor() {
    super();
    this._shadow = this.attachShadow({ mode: 'open' });
    this._shadow.appendChild(template.content.cloneNode(true));
    this._btn = this._shadow.querySelector('button');
  }

  connectedCallback() {
    this._btn.addEventListener('click', () => {
      const text = this.getAttribute('text') ?? '';
      navigator.clipboard.writeText(text);
      this.dispatchEvent(new CustomEvent('copied', { bubbles: true, detail: { text } }));
    });
  }
}

customElements.define('copy-button', CopyButton);

The part attribute exposes inner nodes to external CSS via ::part(trigger). Use it sparingly. Too many parts defeats encapsulation. For debugging component markup during development, a JSON formatter helps when your widget emits structured API payloads alongside UI events.

Validate attribute values. If max must be a positive integer, coerce and fallback in attributeChangedCallback. Do not silently accept garbage. The same discipline applies to REST API input validation on the server.

When should you choose web components over a JavaScript framework?

Web components are not a React replacement. They solve a narrower problem: portable, encapsulated UI primitives. Choose them when the component must outlive the host framework or run in many hosts.

CriteriaWeb ComponentsVue / Alpine / LivewireFull SPA framework
Portability across CMS and static sitesExcellentGood with build stepPoor unless entire app is SPA
Style encapsulationBuilt-in via Shadow DOMScoped CSS or conventionsVaries by setup
Server-rendered first pagesNeeds JS to renderLivewire/Blade SSR nativeSSR adds complexity
Complex app stateManual or add a storeFramework reactivityStrong ecosystem
Bundle size for one widgetSmall as native moduleSmall for AlpineOften larger
Team familiarity on typical PHP projectsModerate learning curveHigh for Blade shopsDepends on hire market

My default on Laravel 12 and 13 projects remains Blade plus Alpine or Livewire for app UI. I reach for web components when the widget must ship into WordPress, a third-party site, or a design system shared across PHP and Node teams. On booking interfaces with supplier-facing embeds, a native component embeds cleanly without forcing the partner to run Vue.

Design systems published as web components — think Shoelace or generic corporate libraries — make sense at scale. A solo agency building one client site rarely needs that overhead. Match the tool to delivery scope and maintenance budget.

When to Use Web ComponentsNeed reusable UI?Same Laravel app only?YesMultiple hosts?CMS + app + embedUse Blade orLivewireUse WebComponentsNeed style isolation in legacy theme?Shadow DOM wins
Decision guide: same-stack UI vs cross-platform embeds — a practical Web Components Explained choice.

How do web components integrate with Laravel, WordPress, and existing stacks?

Integration is straightforward because custom elements are just HTML plus a module script. The hard part is asset loading and SEO expectations, not the component API itself.

Laravel and Vite

Place component files under resources/js/components/. Import them from app.js or load them as separate entry points in vite.config.js. Register tags once at boot. Use the tag inside Blade like any other element:

{{-- resources/views/booking/summary.blade.php --}}
@push('scripts')
  @vite('resources/js/components/date-picker.js')
@endpush

<booking-date-picker locale="ne" min="2026-01-01"></booking-date-picker>

For Nepali date fields, pair the component with server-side validation. Client widgets improve UX. They never replace server rules. See Nepali language support patterns for web apps for locale handling that spans both layers.

WordPress and WooCommerce

Enqueue the module script on pages that need it. WordPress 7.1 supports module scripts via wp_enqueue_script with array( 'strategy' => 'defer' ) and a type="module" filter. Drop the tag into a block HTML snippet or a shortcode template. On WooCommerce 11.1 product pages, a native configurator coexists with theme hooks.

For WordPress-heavy delivery, our WordPress development service often mixes classic PHP templates with small native widgets where plugins would be overkill.

Performance and SEO considerations

Web components render after JavaScript executes. Critical above-the-fold content should stay server-rendered HTML. Use components for interactive regions — filters, calculators, upload zones — not for the entire article body you want indexed immediately.

Keep module files small. Lazy-load heavy components with dynamic import() when the tag enters the viewport. That aligns with Core Web Vitals optimisation and speed optimisation work on production sites. Avoid layout shift by setting explicit dimensions on the host tag in HTML or CSS.

Structured data and meta tags remain server responsibilities. A custom element does not replace canonical URLs or Open Graph tags. Treat SEO architecture separately from component architecture, as outlined in technical SEO practice.

Testing and accessibility

Shadow DOM complicates unit tests slightly. Query through element.shadowRoot in open mode. For end-to-end tests, target visible text and ARIA roles. Native elements inside the shadow tree inherit focus behaviour. Custom widgets must set role, tabindex, and keyboard handlers explicitly.

Run accessibility checks before shipping. A star rating built as divs without keyboard support fails real users. Include focus styles inside the shadow stylesheet. External themes cannot fix what they cannot see.

For regex-heavy client validation inside a component, prototype patterns in a regex tester before embedding logic. Keep validation rules aligned with server Form Requests on Laravel apps.

Form participation and limitations

Custom elements do not automatically join HTML form submission. Use the ElementInternals API and the formAssociated static property when you need native form integration in supporting browsers. Otherwise, treat the component as a controlled widget and sync values into hidden inputs.

Server-side frameworks still own auth, CSRF, and business rules. Web components handle presentation and interaction. That split mirrors how I structure enterprise application front ends: thin client widgets, thick server validation.

Key Takeaways

  • Web components combine Custom Elements, Shadow DOM, HTML Templates, and ES Modules into framework-agnostic UI.
  • Use connectedCallback for setup and disconnectedCallback for cleanup — never fetch data in the constructor.
  • Shadow DOM gives real style isolation; expose theming through CSS custom properties on :host.
  • Reach for native components when widgets must embed across CMS, Laravel, and third-party sites — not for entire app state.
  • Keep critical SEO content server-rendered; use components for interactive regions to protect Core Web Vitals.
  • Validate attributes, dispatch custom events with bubbles: true, and test accessibility inside the shadow tree.

People Also Ask

Are web components supported in all modern browsers?

Yes. Chrome, Firefox, Safari, and Edge all support Custom Elements and Shadow DOM in 2026. Internet Explorer is unsupported. If you must target very old embedded WebViews, test on real devices and add polyfills only where analytics show meaningful traffic.

Do web components work with React or Vue?

Yes, with caveats. React passes primitive attributes but historically struggled with custom property binding and synthetic events. Vue 3 generally handles custom elements well when you configure compilerOptions.isCustomElement. Treat the component as a leaf node. Do not expect the SPA framework to manage its internal DOM.

Are web components good for SEO?

They are neutral. Search engines render JavaScript, but delayed rendering can slow indexing of critical content. Server-render headlines, product copy, and FAQ text. Use web components for interactive extras. Pair with solid canonical tags, sitemaps, and internal linking as described in Laravel SEO optimisation guides.

Should I use Lit, Stencil, or vanilla web components?

Vanilla APIs teach the fundamentals and stay dependency-free for one or two tags. Lit adds a thin reactive layer with less boilerplate. Stencil compiles to multiple framework wrappers for large design systems. For a typical agency deliverable, start vanilla or Lit. Adopt Stencil when you publish a shared library to many teams.

Ship reusable UI that lasts beyond the next framework rewrite

Web Components Explained is ultimately about durability. Custom elements give you a stable HTML contract. Shadow DOM protects that contract from theme chaos. Templates and modules keep the code maintainable. They will not replace Laravel Blade or Livewire on most PHP projects I deliver. They belong in your toolkit when portability and encapsulation matter more than framework ergonomics.

Start with one widget this week. Register it, shadow it, and drop it into an existing page. Measure load impact. If you want help embedding native components into a Laravel, WordPress, or multi-site delivery pipeline, see our web development services or contact us to talk through architecture. For related reading, compare this approach with caching strategies, review 2026 front-end trends, and browse the portfolio for mixed-stack projects that combine server templates with client-side widgets.

Frequently Asked Questions

Four W3C browser APIs—Custom Elements, Shadow DOM, HTML Templates, and ES Modules—that let you define reusable HTML tags with encapsulated styles and behaviour, without a single framework library.

Custom Elements register new HTML tags and attach a JavaScript class. Shadow DOM creates an encapsulated subtree with scoped CSS. HTML Templates store inert markup in a template element until cloned. ES Modules load component files with import and type="module". Together they produce framework-agnostic UI you can drop into Laravel Blade, WordPress themes, Shopify sections, or static pages. The same tag survives framework churn because it sits below Blade, Vue, Alpine, and full SPA stacks as a portability layer.

No. A basic custom element works as a plain ES module loaded with a script type="module" tag. Vite 8.x is optional for library builds or multiple tags.

The constructor runs first—attach your shadow root there, but do not fetch data because the element is not in the DOM yet. connectedCallback is where you render, bind listeners, and load data. attributeChangedCallback reacts to attribute changes; list watched attributes in observedAttributes only. disconnectedCallback cleans up timers and observers when the element is removed. A common production mistake is re-binding click handlers on every render, which leaks memory—use event delegation on the shadow root instead.

Shadow DOM attaches a hidden subtree to your element. Styles inside it do not leak out, and global page CSS—Bootstrap utility classes, theme rules—does not pierce in unless you intentionally use :host or CSS custom properties. That isolation keeps a price widget or document uploader predictable inside any WooCommerce theme or mixed CMS layout. Open mode exposes element.shadowRoot for debugging and testing; closed mode hides it and is rarely worth the debugging cost. Use open mode unless you have a strict encapsulation requirement.

Slots let consumers pass HTML into your component from light DOM markup. Named slots like slot="title" give you layout control; the default slot catches everything else. Inside the shadow tree you declare slot elements where content should appear. The pattern mirrors Blade component slots but runs entirely in the browser. Combined with scoped shadow styles and ::slotted selectors, you get composable cards, info panels, or booking summaries without breaking encapsulation.

Web components are not a React replacement—they solve portable, encapsulated UI primitives. Choose them when the widget must outlive the host framework or run across WordPress, Laravel, third-party embeds, or a shared design system. On typical Laravel 12 and 13 projects, Blade plus Alpine or Livewire remains the default for app UI. Reach for native components when you need cross-CMS embeds, supplier-facing widgets on partner sites, or a corporate library at scale. A solo agency building one client site rarely needs design-system overhead.

Place component files under resources/js/components/ and import them from app.js or register separate Vite entry points in vite.config.js. Load the module with @vite inside a Blade @push('scripts') block, then use the custom tag like any HTML element with attributes such as locale or min. Register tags once at boot. Client widgets improve UX for date pickers or calculators, but they never replace server-side validation in Form Requests. Pair Nepali locale attributes with server rules for fields that need Bikram Sambat or locale-specific formatting.

Enqueue the module script only on pages that need it. WordPress 7.1 supports module scripts via wp_enqueue_script with strategy set to defer and a type="module" filter on the script tag. Drop the custom element into a block HTML snippet, shortcode template, or classic PHP template. On WooCommerce 11.1 product pages, a native configurator coexists with theme hooks without rewriting the entire theme. This approach suits small isolated widgets where a full plugin would be overkill for a single interactive region.

Start with one isolated widget—a star rating, copy-to-clipboard block, or currency formatter. Define the public API: attributes, properties, events, and slots, documented in a comment block. Extend HTMLElement, attach an open shadow root, clone markup from an HTML template in the constructor, and register with customElements.define. Dispatch custom events with bubbles: true so parent pages react without tight coupling. Validate attribute values in attributeChangedCallback rather than silently accepting garbage. Test the tag in every target stack—WordPress, Laravel Blade, any SPA shell—before expanding into a library.

Yes, if misused. Custom elements render after JavaScript executes, so critical above-the-fold content should stay server-rendered HTML. Use components for interactive regions—filters, calculators, upload zones—not for entire article bodies you want indexed immediately. Keep module files small and lazy-load heavy components with dynamic import() when the tag enters the viewport. Set explicit dimensions on the host tag to avoid layout shift. Structured data, canonical URLs, and Open Graph tags remain server responsibilities; a custom element does not replace technical SEO architecture.

Shadow DOM complicates unit tests slightly—query through element.shadowRoot in open mode. For end-to-end tests, target visible text and ARIA roles instead of internal shadow selectors. Native elements inside the shadow tree inherit focus behaviour, but custom widgets built from divs need explicit role, tabindex, and keyboard handlers. Include focus styles inside the shadow stylesheet because external themes cannot fix what they cannot see. Run accessibility checks before shipping; a star rating without keyboard support fails real users regardless of how clean the encapsulation is.

Not automatically. Custom elements do not join native form submission by default. Use the ElementInternals API and the formAssociated static property when you need native form integration in supporting browsers. Otherwise treat the component as a controlled widget and sync values into hidden inputs before submit. Server-side frameworks still own auth, CSRF tokens, and business rules. Web components handle presentation and interaction—a split that mirrors thin client, thick server architecture on production Laravel applications.

Fetching data inside the constructor fails because the element is not in the DOM yet—wait for connectedCallback. Re-binding click handlers on every render leaks memory; use event delegation on the shadow root. Exposing too many part attributes for external ::part() styling defeats encapsulation. Skipping attribute validation lets garbage values through silently. Building an entire page as custom elements when server-rendered HTML would index faster. Forgetting keyboard support and ARIA roles on custom widgets. Treat client validation as UX enhancement only—keep rules aligned with server Form Requests.

Yes. All evergreen browsers ship full support; legacy Internet Explorer is dead. Test once on older Android WebViews if analytics show significant traffic, and polyfill only where data proves you need it. MDN Web Components documentation tracks current API details.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: