Revealing the Hidden Power of Web Components: Frontend Engineering's Undervalued Gem
It's cliche to talk about the break-neck speed of changes in the frontend landscape with new frameworks, build tools, and patterns arriving almost weekly. Progress is great, but the churn leaves teams juggling divergent conventions and brittle integrations while regularly questioning their choices.
I'm here to say that Web standards, especially Web Components, offer the steady footing we need. While they rarely give us the complete functionality we need (hence the aforementioned continual changes in the frontend landscape), these spec-level building blocks supply a common, future-proof vocabulary that transcends libraries and hype cycles, letting us ship UI that works everywhere and lasts longer than the tool du jour.
Inevitably, many teams prefer a small ergonomic layer on top, and I am no exception. I reach for Lit — about 6 kB min+gzip, no proprietary runtime, just thin sugar over the native APIs. (Side note: for larger, more complex apps I’m also a fan of Preact’s custom-element
helper, which lets you wrap full Preact components as standards-compliant elements; but that’s a topic for another post.)
W3C Web Standards: The "Home Button" of Frontend Engineering
In the mid–2000s mobile scene, hardware makers released phones in every conceivable shape and layout. Each device felt novel, but the experience was chaotic. When the iPhone landed, its single Home button gave users a safety net: "No matter where I am, one click gets me back."
Web standards play the same role on the web. They're the fixed point that keeps us grounded while libraries and build tools race ahead. Whenever we evaluate a new approach, the first question should be:
How closely does this align with established standards?
Vanilla Web Components answer with a resounding "perfectly."
Lit bonus: Lit keeps that answer intact; it simply strings the native APIs together with a terse syntax for templates and reactive state.
Why Web Components, Right Now?
Pain Today | Zero‑Lib Web Component Answer | Lit Bonus |
---|---|---|
Framework churn — learning curves, rewrites | Native API that stays stable for decades | Adds ergonomic templating without a virtual DOM |
Design‑system drift across apps | Ship one component once, reuse everywhere | Integrate slots, styling, and accessibility helpers easily |
Bundle bloat from duplicated code | Browser parses a single class definition | +6 kB for nicer DX, nothing more |
Accessibility regressions | Encapsulation keeps ARIA and semantics intact | Built‑in directives for ARIA, @aria* properties |
React, Vue, Svelte, and friends remain fantastic, but component logic tied tightly to one virtual DOM can't travel far. Web Components travel anywhere: WordPress, Rails, Astro islands, or a bare HTML file, with or without Lit.
The Four Pillars in 60 Seconds
- Custom Elements
/** * Define a custom <my-toggle> element, extending HTMLElement * and register it with the browser. */ class MyToggle extends HTMLElement { /* … */ } customElements.define('my-toggle', MyToggle)
- Shadow DOM — CSS and markup live in a sealed subtree—guaranteed styles, no leaks.
- HTML Templates —
<template>
and<slot>
give you declarative markup and content projection. - ES Modules — Load code natively with
<script type="module">
, no bundler required (though Vite makes it nicer).
Lit bonus: Lit does not replace these; it stitches them together with ergonomic helpers.
Zero Dependencies, Pure Browser APIs
Here's the beautiful truth: Web Components require zero third-party libraries, no build steps, no transpilation. You can write them with nothing but vanilla JavaScript and they'll work in any modern browser:
/**
* Define a simple <toggle-button> element as a custom web component
* and bind a click handler.
*/
class ToggleButton extends HTMLElement {
#on = false
connectedCallback() {
this.attachShadow({ mode: 'open' }).innerHTML = `
<button part="button">${this.#label()}</button>
<style>
:host { display: inline-block; }
button { padding: .4rem .8rem; border-radius: .25rem; cursor: pointer; }
</style>
`
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.#on = !this.#on
this.shadowRoot.querySelector('button').textContent = this.#label()
})
}
#label() {
return this.#on ? 'On' : 'Off'
}
}
customElements.define('toggle-button', ToggleButton)
Drop this in an HTML file, add <toggle-button></toggle-button>
to your HTML, open it in a browser, and you have a working toggle button element. No dependencies, no build step, no framework.
Upgrade with Lit: The Thinnest Possible Layer
It's already been referenced a few times, but let's dive into some of the details of how Lit enhances the developer experience. While you can go completely vanilla, a thin layer of developer experience can make the authoring story much more pleasant. Lit is remarkable for what it doesn't do. It's not a framework with its own component model, it's a tiny collection of utilities that make vanilla Web Components more ergonomic to write.
Here's the same component with Lit's decorators and templating:
import { LitElement, html, css } from 'lit'
import { customElement, state } from 'lit/decorators.js'
/**
* Define a custom <toggle-button> element with a simple click handler.
*/
@customElement('toggle-button')
export class ToggleButton extends LitElement {
static styles = css`
:host {
display: inline-block;
}
button {
padding: 0.4rem 0.8rem;
border-radius: 0.25rem;
cursor: pointer;
}
`
@state() private on = false
render() {
return html`
<button @click=${() => (this.on = !this.on)}>
${this.on ? 'On' : 'Off'}
</button>
`
}
}
What Lit adds:
- Declarative templates with
html
-tagged template literals - Reactive properties that trigger re-renders when changed, a la React/Vue
- Scoped CSS with
css
-tagged template literals - Event binding with the
@click
syntax - Efficient updates thanks to Lit's diff-based rendering
What Lit doesn't add:
- No virtual DOM (uses real DOM with smart updates)
- No runtime framework (compiles to standard custom elements)
- No proprietary component model (extends
HTMLElement
) - No bundle bloat (tree-shakes to ~6KB)
The output is still a pure Web Component that works everywhere. Lit just makes the authoring experience feel like modern JavaScript instead of 2015-style DOM manipulation.
Using Web Components with Your Framework
Now, here's where web components really shine: they play nicely with any framework because they're native browser technology. You can use them in React, Vue, Svelte, Angular, or even plain HTML without any special adapters.
- React / Preact – pass props as attributes (
<my-card title="Hello" />
). For booleans or objects, useref
+ property assignment. - Vue 3 –
defineCustomElement
bridges reactivity; slots project as‑is. - Svelte –
<svelte:options tag="my-svelte-card" />
compiles to a custom element. Lit not required, but compatible. - Angular –
CUSTOM_ELEMENTS_SCHEMA
lets you use them directly in templates.
The key: keep framework specifics at the edges. Let the component itself stay standard. Remember: Lit lives inside the component; consumers only interact with standard DOM properties, events, and slots.
Real‑World Proof
- GitHub uses vanilla
<details>
/<dialog>
, but also Lit for new design‑system pieces. - Adobe Spectrum, ING Lion Design System, Salesforce Lightning, and Vaadin all ship Lit‑powered Web Components consumed by React, Angular, and plain HTML apps alike.
- Google's Chrome DevTools UI is now a sea of Lit elements (
<devtools-*>
).
If billion‑dollar platforms can trust both vanilla and Lit, your project can too.
Caveats (and Practical Fixes)
Challenge | Mitigation |
---|---|
Server‑side rendering & hydration | Use partial hydration (Astro, Eleventy islands) or progressive enhancement. |
TypeScript ergonomics | Lit provides excellent TypeScript support out of the box |
Theming across Shadow DOM | Rely on CSS Custom Properties; expose parts with the ::part() selector. |
Testing | Use Web Test Runner or Playwright; treat custom elements as any DOM node. |
None of these are show‑stoppers, they're the same sort of trade‑offs we already navigate with SPA frameworks.
Getting Started Today
- Write one without Lit first. Experience the raw APIs to understand the platform.
- Add Lit for ergonomics:
npm i lit
(head over to the Lit documentation for guidance) - Convert a leaf component. Buttons, tooltips, or tag inputs are great starting points.
Wrapping Up…
Web Components alone are a rock‑solid standard that slot neatly into your favorite framework. They can help you future‑proof the bits of UI you don't want to rewrite every two years. They embody the same calm certainty the iPhone's Home button gave users: a standard you can press any time and know exactly what will happen.
Lit simply trims the boilerplate; a polite assistant, not a gatekeeper. Whether you stay 100% vanilla or sprinkle in Lit for comfort, you'll own UI you don't have to rewrite every two years.
If you haven't tried them recently, spin up vite
, scaffold a component twice (vanilla, then Lit), and feel the difference. You might discover an underrated gem hiding in plain sight.
Happy building! 🚀