
September 12, 2026
14 min read
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.
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
importandtype="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.
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.
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.
- Pick one isolated UI piece. A star rating input, a copy-to-clipboard block, or a currency formatter works well.
- Define the public API. List attributes, properties, events, and slots. Document them in a comment block.
- Create the class file. Extend
HTMLElement, attach an open shadow root, and register withcustomElements.define. - Move markup into a template. Use
<template id="...">and clone it in the constructor for cleaner code. - Dispatch custom events. Use
this.dispatchEvent(new CustomEvent('change', { detail, bubbles: true }))so parent pages react without tight coupling. - 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.
- 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.
| Criteria | Web Components | Vue / Alpine / Livewire | Full SPA framework |
|---|---|---|---|
| Portability across CMS and static sites | Excellent | Good with build step | Poor unless entire app is SPA |
| Style encapsulation | Built-in via Shadow DOM | Scoped CSS or conventions | Varies by setup |
| Server-rendered first pages | Needs JS to render | Livewire/Blade SSR native | SSR adds complexity |
| Complex app state | Manual or add a store | Framework reactivity | Strong ecosystem |
| Bundle size for one widget | Small as native module | Small for Alpine | Often larger |
| Team familiarity on typical PHP projects | Moderate learning curve | High for Blade shops | Depends 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.
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
connectedCallbackfor setup anddisconnectedCallbackfor 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
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.

