← Back to Blog

Web Components Explained: Framework-Free Custom Elements

Build reusable, encapsulated UI components using native browser APIs—no framework required

Web Components are a set of native browser APIs that let you create reusable custom elements with encapsulated functionality. Unlike framework-specific components that only work in React or Vue, Web Components work everywhere—vanilla JavaScript, any framework, or no framework at all. They're part of the web platform itself, supported by all modern browsers, and provide a standards-based way to build modular, maintainable interfaces without the overhead of a framework.

The Three Core Technologies

Web Components are built on three main browser standards that work together to create powerful, encapsulated elements:

For more insights on this topic, see our guide on Progressive Enhancement Guide: Building Resilient Websites.

Custom Elements: Define your own HTML tags with custom behavior. Instead of generic div elements with classes, you create semantic tags like user-card or date-picker that have built-in functionality. The browser treats these exactly like native elements—they have lifecycle hooks, can be styled, and work with all standard DOM APIs.

Shadow DOM: Provides true encapsulation by creating a separate DOM tree for your component. Styles and scripts inside the shadow DOM don't leak out, and outside styles don't leak in. This prevents the CSS conflicts and specificity wars that plague large codebases.

HTML Templates: The template element lets you define chunks of markup that aren't rendered until cloned by JavaScript. This provides a performant way to define component structure without parsing and creating nodes until needed.

Creating Your First Custom Element

Let's build a simple user card component to demonstrate the basics. This will be a reusable element that displays user information with consistent styling:

Start by defining a class that extends HTMLElement. This base class provides all the standard DOM functionality. Add a constructor that calls super(), then create a shadow root for encapsulation. Attach your component's HTML and CSS to this shadow root.

The component can have attributes like any HTML element. Use attributeChangedCallback to respond when these change. This lifecycle method runs whenever an observed attribute is modified, letting you update the component's display reactively.

Finally, register your element using customElements.define(). Choose a name with a hyphen—this distinguishes custom elements from standard HTML. Now you can use user-card in your HTML just like any built-in element.

Lifecycle Callbacks

Custom elements have lifecycle hooks that run at specific times, similar to React or Vue component lifecycles:

  • connectedCallback — Runs when the element is added to the DOM. Use this for setup tasks, fetching data, or adding event listeners.
  • disconnectedCallback — Runs when removed from the DOM. Clean up timers, remove event listeners, cancel network requests.
  • attributeChangedCallback — Runs when observed attributes change. Update the component's display based on new attribute values.
  • adoptedCallback — Runs when moved to a new document. Rarely used but important for advanced scenarios like moving elements between iframes.

Shadow DOM Encapsulation

Shadow DOM is what makes Web Components truly powerful for building isolated, reusable components. It creates a boundary that prevents style and script conflicts:

Style encapsulation: CSS defined inside shadow DOM only applies to that component. You can use simple class names without worrying about conflicts. Outside styles can't accidentally break your component's layout. This eliminates the need for CSS methodologies like BEM or CSS Modules—encapsulation is built in.

DOM encapsulation: JavaScript queries like querySelector won't cross the shadow boundary. Code outside your component can't accidentally modify its internal structure. This makes components more robust and easier to maintain.

Slots for content projection: The slot element lets you define insertion points where users of your component can provide custom content. This works like React's children prop or Vue's slots. You provide the structure and styling, users provide the content.

Attributes and Properties

Web Components use attributes for configuration, just like native HTML elements. Understanding the difference between attributes and properties is crucial:

Attributes: String values set in HTML or via setAttribute(). These reflect what's in the markup. Use these for configuration that should be visible in the HTML and serializable.

Properties: JavaScript values set directly on the element object. These can be any type—objects, arrays, functions. Use properties for complex data or callbacks.

Best practice is to provide both: attributes for simple configuration, with corresponding properties that parse the string values into appropriate types. Sync them in attributeChangedCallback so changes to attributes update properties and vice versa.

Events and Communication

Components need to communicate with the outside world. Web Components use standard DOM events for this:

Dispatch custom events when important things happen in your component. Use the CustomEvent constructor with a descriptive name and pass data via the detail property. Make events bubble if parents need to catch them, but keep them contained if they're only relevant to direct listeners.

Users of your component can listen for these events using addEventListener, just like native events. This creates a clean, decoupled API—your component doesn't know or care who's listening.

When to Use Web Components

Web Components shine in specific scenarios where their unique characteristics provide real advantages:

Design systems and component libraries: Web Components work in any framework, making them perfect for shared components used across multiple applications with different tech stacks. Design teams can provide a single set of components that work everywhere.

Micro-frontends: When different teams build different parts of an application using different frameworks, Web Components provide a common interface layer. A React team can use components built by a Vue team without any integration issues.

Third-party widgets: If you're building embeddable components for other sites—chat widgets, payment forms, social feeds—Web Components ensure your code won't conflict with the host page.

Progressive enhancement: Web Components can enhance existing HTML without requiring a build step or framework. They're great for adding interactivity to static sites or server-rendered pages.

Limitations and Tradeoffs

Web Components aren't always the right choice. Understand the tradeoffs before committing:

No reactivity system: Unlike React or Vue, Web Components don't automatically re-render when state changes. You must manually update the DOM or use a library like Lit that adds reactivity.

Verbose vanilla implementation: Writing Web Components with vanilla JavaScript involves a lot of boilerplate. Libraries like Lit, Stencil, or FAST reduce this but add dependencies.

Server-side rendering: Web Components are client-side by nature. Server rendering requires additional tooling and isn't as mature as framework SSR solutions.

Developer experience: Frameworks provide better tooling, debugging, and developer experience. Web Components work but don't have the ecosystem of React DevTools or Vue DevTools.

Getting Started

Start simple. Build a basic component with vanilla JavaScript to understand the fundamentals. Don't try to replicate framework features—embrace the platform and keep components focused and small.

Once comfortable, explore libraries like Lit that reduce boilerplate while staying close to the standard. They provide reactivity and better ergonomics without hiding the underlying Web Components APIs.

Web Components work best alongside frameworks, not instead of them. Use them for shared components, design systems, or progressive enhancement. Use frameworks for full applications where their ecosystems and tooling provide value.

Related Reading

Build Reusable Component Systems

We create framework-agnostic component libraries using Web Components. Build once, use everywhere—React, Vue, or vanilla JavaScript.

Discuss Your Project