Published

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 TodayZero‑Lib Web Component AnswerLit Bonus
Framework churn — learning curves, rewritesNative API that stays stable for decadesAdds ergonomic templating without a virtual DOM
Design‑system drift across appsShip one component once, reuse everywhereIntegrate slots, styling, and accessibility helpers easily
Bundle bloat from duplicated codeBrowser parses a single class definition+6 kB for nicer DX, nothing more
Accessibility regressionsEncapsulation keeps ARIA and semantics intactBuilt‑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

  1. 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)
    
  2. Shadow DOM — CSS and markup live in a sealed subtree—guaranteed styles, no leaks.
  3. HTML Templates<template> and <slot> give you declarative markup and content projection.
  4. 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, use ref + property assignment.
  • Vue 3defineCustomElement bridges reactivity; slots project as‑is.
  • Svelte<svelte:options tag="my-svelte-card" /> compiles to a custom element. Lit not required, but compatible.
  • AngularCUSTOM_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)

ChallengeMitigation
Server‑side rendering & hydrationUse partial hydration (Astro, Eleventy islands) or progressive enhancement.
TypeScript ergonomicsLit provides excellent TypeScript support out of the box
Theming across Shadow DOMRely on CSS Custom Properties; expose parts with the ::part() selector.
TestingUse 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

  1. Write one without Lit first. Experience the raw APIs to understand the platform.
  2. Add Lit for ergonomics: npm i lit (head over to the Lit documentation for guidance)
  3. 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! 🚀

Back to Blog