Using Web Components in Adobe Edge Delivery Services Blocks

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
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!
Related Articles