Using Web Components in Adobe Edge Delivery Services Blocks

Web components have changed how developers create reusable UI elements. When you combine them with Adobe Edge Delivery Services (EDS), you get a solid foundation for building interactive, maintainable blocks. Here's how to make the most of web components in your EDS projects.

All code is available in Github : https://github.com/ddttom/webcomponents-with-eds

What are Web Components?

Web Components are standardised APIs that let you create custom HTML elements. They bring together four main technologies that work in harmony. Custom Elements allow you to define new HTML elements, while Shadow DOM encapsulates your styles and markup. HTML Templates provide reusable markup patterns, and ES Modules handle the packaging and distribution of your components.

The great thing about web components? They work natively in modern browsers without any external frameworks.

Why Use Web Components in EDS?

EDS blocks and web components work brilliantly together. The natural encapsulation that web components provide means your blocks stay self-contained and predictable. Since they're framework-agnostic, you won't find yourself locked into any particular technology stack. They fit perfectly with EDS's block-based architecture, keeping concerns cleanly separated while remaining highly reusable across your projects.

A Practical Example, without libraries - The Counter Block

Let's look at a working example: a counter block that uses web components. This block creates an interactive counter with increment and decrement functionality.

How It Works

The counter block uses a custom element called counter-element. The implementation starts with a simple table in your document:


Live Example, click buttons

5

The web component itself is defined in the block's JavaScript file, including the CSS, remember to create a blank css file to keep EDS happy :

/*
 * Counter Block
 * A simple counter component using web components
 */

// Configuration object for the counter block

const COUNTER_CONFIG = {
  CLASS_NAMES: {
    COUNTER: 'counter-component',
    BUTTON: 'counter-button',
    DISPLAY: 'counter-display',
  },

  ARIA_LABELS: {
    INCREMENT: 'Increment counter',
    DECREMENT: 'Decrement counter',
    DISPLAY: 'Current count',
  },

  ERROR_MESSAGES: {
    INVALID_INITIAL: 'Invalid initial value provided',
  },
};

// Define the Counter Web Component
class CounterElement extends HTMLElement {
  constructor() {
    super();
    this.count = parseInt(this.getAttribute('initial-value') || '0', 10);
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
    this.setupEventListeners();
  }
  render() {
    const { shadowRoot } = this;
    shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          --counter-button-bg: var(--color-primary, #007bff);
          --counter-button-color: var(--color-text-inverse, #ffffff);
          --counter-display-bg: var(--color-background, #f8f9fa);
          --counter-display-color: var(--color-text, #212529);
        }

        .counter-wrapper {
          display: flex;
          align-items: center;
          gap: 1rem;
          padding: 1rem;
          font-family: var(--font-family-base, system-ui);
        }

        .counter-button {
          background: var(--counter-button-bg);
          color: var(--counter-button-color);
          border: none;
          border-radius: 4px;
          padding: 0.5rem 1rem;
          cursor: pointer;
          font-size: 1rem;
          transition: opacity 0.2s;
        }

        .counter-button:hover {
          opacity: 0.9;
        }

        .counter-button:focus-visible {
          outline: 2px solid var(--counter-button-bg);
          outline-offset: 2px;
        }

        .counter-display {
          background: var(--counter-display-bg);
          color: var(--counter-display-color);
          padding: 0.5rem 1rem;
          border-radius: 4px;
          min-width: 3rem;
          text-align: center;
          font-size: 1.25rem;
          font-weight: bold;
        }
      </style>
        
      <div class="counter-wrapper">
        <button class="counter-button" aria-label="${COUNTER_CONFIG.ARIA_LABELS.DECREMENT}">-</button>
        <div class="counter-display" aria-label="${COUNTER_CONFIG.ARIA_LABELS.DISPLAY}">${this.count}</div>
        <button class="counter-button" aria-label="${COUNTER_CONFIG.ARIA_LABELS.INCREMENT}">+</button>
      </div>
    `;
  }

  setupEventListeners() {
    const { shadowRoot } = this;
    const incrementButton = shadowRoot.querySelector('.counter-button:last-child');
    const decrementButton = shadowRoot.querySelector('.counter-button:first-child');
    const display = shadowRoot.querySelector('.counter-display');
    incrementButton.addEventListener('click', () => {
      this.count += 1;
      display.textContent = this.count;
      this.dispatchEvent(new CustomEvent('count-change', { detail: { count: this.count } }));
    });
    decrementButton.addEventListener('click', () => {
      this.count -= 1;
      display.textContent = this.count;
      this.dispatchEvent(new CustomEvent('count-change', { detail: { count: this.count } }));
    });
  }
}

// Register the web component
customElements.define('counter-element', CounterElement);
/**
 * Decorates the counter block
 * @param {HTMLElement} block - The block element
 */

export default function decorate(block) {

  try {
    // Get initial value from the first cell of the table
    const cells = Array.from(block.children);
    const initialValue = cells[0]?.textContent.trim();

    // Create and configure counter before clearing block
    const counter = document.createElement('counter-element');

      

    if (initialValue) {
      const parsedValue = parseInt(initialValue, 10);
      if (Number.isNaN(parsedValue)) {
        throw new Error(COUNTER_CONFIG.ERROR_MESSAGES.INVALID_INITIAL);
      }

      // Set the initial value attribute
      counter.setAttribute('initial-value', parsedValue.toString());
      // Force a re-render of the counter with the new value
      counter.count = parsedValue;
      if (counter.shadowRoot) {
        const display = counter.shadowRoot.querySelector('.counter-display');
        if (display) {
          display.textContent = parsedValue.toString();
        }
      }
    }

    // Clear the block and append the counter
    block.textContent = '';
    block.appendChild(counter);
    // Add event listener for count changes
    counter.addEventListener('count-change', (event) => {
      // eslint-disable-next-line no-console
      console.log('Count changed:', event.detail.count);
    });
  } catch (error) {

    // eslint-disable-next-line no-console
    console.error('Error initializing counter:', error);
    block.textContent = error.message;
  }
}

Key Features

The counter block demonstrates what makes web components powerful. Shadow DOM encapsulation keeps styles within the component, preventing any leakage in or out. This creates clean, maintainable CSS that won't interfere with the rest of your page.

Custom properties make theming straightforward through CSS variables. Your components can adapt to different visual contexts while maintaining consistency with EDS theming. The event-based communication pattern means components emit events for state changes, creating loose coupling with other blocks and making integration with other features simple.

Accessibility comes built in through ARIA labels for screen readers, keyboard navigation support, and a semantic HTML structure. These aren't afterthoughts - they're fundamental to how the component works.

Accelerating Development with Component Libraries

While building custom web components gives you complete control, sometimes you need to move faster. This is where web component libraries like Shoelace https://shoelace.style/ come into play. These libraries provide professionally designed, ready-to-use components that work seamlessly with EDS blocks.

However, integrating external libraries in EDS requires a different approach than traditional web development. You can't simply add script tags to your HTML. Instead, EDS provides specific functions for loading external resources. This ensures proper loading order and maintains the performance benefits of EDS's architecture.

To include a library like Shoelace in your EDS blocks, you need to modify your block's JavaScript to load the resources programmatically. Here's how you'd typically do it in your block's decoration function:

import { loadCSS,loadScript } from '../../scripts/aem.js';

export default async function decorate(block) {
    // Load Shoelace CSS
    await loadCSS('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/themes/light.css');
    // Load Shoelace JavaScript
    await loadScript('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/shoelace-autoloader.js', {
      type: 'module'
    });

    // Now you can use Shoelace components
    const button = document.createElement('sl-button');
    button.textContent = 'Click me';
    block.appendChild(button);
  }

Create a table with one line

Live Example

This approach works particularly well because EDS's loadCSS and loadScript functions check if resources are already loaded, preventing duplicate requests. The functions return promises, allowing you to ensure resources are fully loaded before using them.

Best Practices for Web Components in EDS

Building effective web components for EDS blocks requires thoughtful implementation. Start with configuration by defining objects at the top of your files and using constants for repeated values. This makes components easily configurable and maintainable.

Error handling deserves special attention. Build proper error boundaries that provide clear messages when things go wrong. Handle edge cases gracefully rather than letting them break the user experience.

Performance matters too. Shadow DOM provides style isolation, but you still need to keep DOM operations minimal and handle events efficiently. Every interaction should feel instant and responsive.

Accessibility isn't optional. Include ARIA attributes throughout your components, support keyboard navigation from the start, and maintain proper contrast ratios. Your documentation should be equally thorough - write README files that include usage examples and explain customisation options clearly. Consider bundle size impact when choosing external libraries.

Real-World Benefits

Using web components in EDS blocks brings advantages to your development process. Isolated components are easier to maintain because they have clear boundaries and fewer unexpected interactions. The separation of concerns means you can work on individual components without worrying about breaking other parts of your site.

Reusability becomes natural when components can be shared across blocks. You get consistent behaviour and styling without code duplication. Since these are native browser features, there's no framework overhead - just efficient rendering and solid performance.

The developer experience improves too. You're working with familiar web standards and clear component boundaries. Testing and debugging become straightforward when you can isolate issues to specific components.

Getting Started

Starting with web components in your EDS blocks is straightforward. Create a new block directory and build your custom element using standard web APIs. Register it in your block's JavaScript, then write documentation and examples that others can follow. Test across different browsers to ensure compatibility.

Wrapping Up

Web components offer a standards-based approach to creating reusable UI elements in EDS blocks. They balance encapsulation, reusability, and performance perfectly - making them ideal for building interactive blocks.

Following the patterns shown here, you can create strong, maintainable blocks that use web components effectively while staying compatible with EDS's block-based architecture. Whether you build custom components, or use libraries like Shoelace for general UI elements, you have powerful options at your disposal.

The key is starting small, following standards, and building gradually. Each component you create becomes a building block for more complex interactions, all while maintaining the simplicity that makes EDS powerful.

About the Author

Tom Cranstoun specializes in modern web development and content management solutions. Digital Domain Technologies Ltd provides CMS consultancy with 13+ years of experience helping organizations implement robust, scalable web solutions.

Found this helpful? Star the https://github.com/ddttom/webcomponents-with-eds on GitHub!

/fragments/ddt/proposition

Related Articles

path=*
path=*
Back to Top