Web Component Development: A Beginner’s Practical Guide to Custom Elements, Shadow DOM & Templates

Updated on
7 min read

Introduction

Web Components are a powerful set of web standards that allow developers to create reusable, framework-agnostic UI elements. In this beginner-friendly guide, we will explore the essentials of Web Components, including Custom Elements, Shadow DOM, HTML templates, and ES Modules. If you’re familiar with basic HTML, CSS, and JavaScript, you’ll find the concepts easy to grasp. Even if you’re new to frameworks like React, Vue, or Angular, you’ll be able to follow along seamlessly. By the end of the article, you will be equipped to build a reusable <user-card> web component and understand its applications in modern web development.


Why Web Components?

Problems Web Components Solve

  • Reusability: Package markup, styles, and behavior in a single unit for use in any web project.
  • Encapsulation: Protect internal implementation details (DOM and CSS) from the host page, minimizing style leakage.
  • Interoperability: Ensure compatibility across various frameworks and libraries, leveraging a web standard.

Comparison to Framework Components

While framework components (e.g., React, Vue, Angular) offer rich features and state management, they can also lock you into a specific ecosystem. Conversely, Web Components are a web platform standard, usable both in framework applications and plain HTML with minimal coupling.

Real-World Use Cases

  • Design systems across teams
  • Embeddable widgets for third-party publishers
  • Micro-frontend UIs and cross-project libraries
  • Progressive enhancement of server-rendered pages

For a brief overview of standards and browser support, visit MDN’s Web Components overview and Google’s web.dev guide.


Core Technologies

Custom Elements API

To define a custom element, create a class that extends HTMLElement and register it with:

customElements.define('my-element', MyElement);

Lifecycle Callbacks

  • constructor(): Set initial state, attach shadow root (avoid heavy DOM work).
  • connectedCallback(): Executed when inserted into the document.
  • disconnectedCallback(): Cleanup when removed.
  • attributeChangedCallback(name, oldValue, newValue): Respond to attribute updates.

Define observed attributes with:

static get observedAttributes() {
    return ['name'];
}

Example:

class MyElement extends HTMLElement {
    static get observedAttributes() { return ['name']; }
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
    }
    connectedCallback() { this.render(); }
    attributeChangedCallback() { this.render(); }
    render() { this.shadowRoot.innerHTML = `<p>Hello ${this.getAttribute('name')}</p>`; }
}
customElements.define('my-element', MyElement);

Shadow DOM: Encapsulation and Style Scoping

Attach a shadow root with:

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

This isolates styles and markup, preventing leakage.

Templates & Slots

  • <template> holds inert DOM, which can be cloned at runtime for markup reuse.
  • <slot> allows content projection, similar to framework slots.

Attributes vs Properties

  • Attributes: Strings serialized into HTML.
  • Properties: JavaScript values (e.g., objects, booleans). Best practice is to sync attributes with properties when appropriate.

ES Modules & Import Maps

Use ES Modules to keep code modular and tree-shakeable. For complex applications, employ import maps or bundlers for module resolution.

For more API references, see MDN’s Web Components pages.


Tooling, Libraries & Polyfills

Official Browser Support and Polyfills

Most modern browsers provide native support for Custom Elements, Shadow DOM, HTML templates, and ES Modules. Check compatibility on Can I Use.

For older browsers (e.g., legacy versions of IE), consider using the @webcomponents/webcomponentsjs polyfills but only load them as necessary to maintain performance.

LibraryFeaturesProsCons
Lit (lit.dev)Lightweight templating + reactive updatesConcise syntax, beginner-friendlyAdds a small library dependency
Stencil (stenciljs.com)Generates standards-based componentsOptimized bundles, framework bindingsOpinionated build-time toolchain
SkateJSLow-level toolkitLightweight, standards-firstSmaller community than Lit

Which Library to Choose?

  • For learning, create raw Web Components to understand the primitives.
  • For production use and easier developer experience, Lit is a great choice.
  • For extensive design systems supporting multiple frameworks, consider Stencil.

Build Tools and Dev Server Tips

Utilize tools like Vite, esbuild, or Rollup for fast development and optimized builds. Prefer unbundled development (serve ES modules directly) to speed up code iteration.

Example quick-start commands (Vite + Lit):

npm init vite@latest my-wc -- --template vanilla
cd my-wc
npm install lit
npm run dev

Building a Simple Web Component (Step-by-Step)

Goal

Create a themeable <user-card> that displays an avatar, name, role, and emits a user-selected event when clicked.

Project Structure

/user-card/
  index.html
  components/user-card.js
  styles.css (optional)

1) Create the Template and Styles

<!-- index.html -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>User Card Demo</title>
    <script type="module" src="./components/user-card.js"></script>
  </head>
  <body>
    <user-card name="Ada Lovelace" role="Engineer" avatar="https://i.pravatar.cc/80?img=3"></user-card>
  </body>
</html>
// components/user-card.js
const template = document.createElement('template');
template.innerHTML = `
  <style>
    :host { display: inline-block; font-family: system-ui, sans-serif; }
    .card { border: 1px solid var(--uc-border, #ccc); padding: 12px; border-radius: 8px; display:flex; gap:12px; align-items:center; background:var(--uc-bg, #fff); }
    img { width:48px; height:48px; border-radius:50%; object-fit:cover; }
    .info { display:flex; flex-direction:column; }
    .name { font-weight:600; }
    .role { font-size: 0.9em; color: #666; }
    button { background:transparent; border:none; color:var(--uc-accent,#0366d6); cursor:pointer; padding:4px; }
  </style>
  <div class="card" tabindex="0">
    <img part="avatar" />
    <div class="info">
      <div class="name" part="name"></div>
      <div class="role" part="role"></div>
    </div>
    <button part="select-btn">Select</button>
  </div>
`;

class UserCard extends HTMLElement {
    // User card implementation...
}

customElements.define('user-card', UserCard);

2) Expose API and Events

Define properties such as name, role, avatar, and ensure the user-selected event bubbles.

3) Theming and Slots

Utilize CSS custom properties for theming. Integrate slots for custom content insertion as necessary.

Usage Example

To listen to the user-selected event:

<script>
  document.addEventListener('user-selected', (e) => {
    console.log('Selected user:', e.detail);
  });
</script>

Troubleshooting Tips

  • If your element renders as plain text, ensure the script with customElements.define loads before using the tag.
  • Verify the tag name includes a hyphen to avoid issues.
  • Ensure observedAttributes includes the attribute names for updates to reflect.

Best Practices

Naming Conventions

  • Utilize scoped prefixes (e.g., acme-user-card) to prevent name collisions.
  • Remember the hyphen rule in custom element names.

Accessibility

  • Confirm keyboard accessibility (use tabindex, handle Enter/Space keys).
  • Incorporate ARIA roles appropriately.
  • Provide alt text for images and ensure elements are clearly labeled.

Performance and Security

  • Optimize component loading using lazy-loading techniques.
  • Avoid using innerHTML with untrusted content; utilize textContent and sanitize data where necessary.

Documentation

Clearly document properties, events, and theming strategies for users of your component.


Framework Interoperability

Using Web Components with Frameworks

  • React: Treat web components as DOM elements.
  • Angular: Include CUSTOM_ELEMENTS_SCHEMA in the module to allow unknown elements.
  • Vue 3: Handle custom elements natively with v-on for events and v-bind for attributes.

Common Pitfalls

Note that React does not use properties via JSX attributes by default, and framework change detection won’t automatically run on property changes in Web Components.

Packaging & Distribution

Publish components on npm and ensure compatibility. Consider including a Custom Elements Manifest to facilitate tooling and design systems.


Testing & Debugging

Unit Testing

Utilize web-test-runner for accurate tests in real browsers. Combine Jest with jsdom for logic-only testing.

End-to-End Testing

Employ Playwright or Cypress for browser automation.

DevTools Tips

Investigate shadow DOM using the Elements panel, and access it via the console if the shadow root is open.


Deployment, Distribution & Versioning

Publish Strategies

Choose between npm for team integrations or CDN for embeddable widgets. Maintain a compatibility matrix to manage browser support and conditional loading of polyfills as needed.

Versioning

Utilize semantic versioning for clear change management, and publish migration notes for consumers.


Resources & Conclusion

Learning Path

Begin with small projects to practice building components. Publish examples to npm or via a CDN, and create demo pages to showcase your work.

Explore more resources:

In conclusion, Web Components provide a standardized approach to creating reusable UI components. Start by building a Custom Element to master the basics, and then explore libraries like Lit or Stencil to enhance your development workflow. Remember to emphasize accessibility and clear documentation for your users.

TBO Editorial

About the Author

TBO Editorial writes about the latest updates about products and services related to Technology, Business, Finance & Lifestyle. Do get in touch if you want to share any useful article with our community.