Skip to main content

Blocks

In EDS, blocks replace AEM components. A block is a folder in your GitHub repository with a JavaScript file and a CSS file. The author types a block name into a table (or selects one in Universal Editor) and EDS turns it into a <div> with the block name as its class. Your decorate() function then has free rein to restructure the DOM into whatever semantic markup the design needs.

/blocks/
/hero/
hero.js
hero.css
/cards/
cards.js
cards.css

From table to DOM​

A document like this:

| Hero |
|-------------------------|
| ![hero.jpg](hero.jpg) |
| Welcome to our site |
| [Get started](/start) |

Becomes (before decoration):

<div class="hero block" data-block-name="hero" data-block-status="initialized">
<div>
<div><picture><img src="hero.jpg" alt=""></picture></div>
</div>
<div>
<div>Welcome to our site</div>
</div>
<div>
<div><a href="/start">Get started</a></div>
</div>
</div>

Each row of the original table becomes a <div> (a row), and each cell becomes a nested <div> (a cell). Heading levels, links, and images are preserved verbatim from the source.

Block lifecycle​

Every block exports a default decorate(block) function. The runtime calls it once, after the block element is in the DOM. The block goes through three phases driven by scripts/aem.js:

PhaseWhenUsed for
EagerBefore LCPBlocks in the first section (hero, above-the-fold content)
LazyRight after LCPAll remaining sections and blocks, header, footer, lazy styles, fonts
DelayedAbout 3 seconds after loaddelayed.js: analytics, chat, marketing tags

You don't usually pick the phase per block - aem.js decides based on the section the block sits in. Blocks that should only do expensive work when visible (maps, videos, embeds) can add their own IntersectionObserver inside decorate(). You can override the phases in scripts/scripts.js if needed.

Writing a block​

/blocks/hero/hero.js
export default function decorate(block) {
// block is the <div class="hero block"> element
const image = block.querySelector('picture');
const heading = block.querySelector('h1, h2, h3');
const cta = block.querySelector('a');

// Restructure for semantic HTML
block.innerHTML = '';

const content = document.createElement('div');
content.classList.add('hero-content');
if (heading) content.append(heading);
if (cta) {
cta.classList.add('hero-cta', 'button', 'primary');
content.append(cta);
}

if (image) block.append(image);
block.append(content);
}
/blocks/hero/hero.css
.hero {
position: relative;
min-height: 400px;
display: flex;
align-items: center;
}

.hero picture img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}

.hero .hero-content {
position: relative;
z-index: 1;
padding: 2rem;
max-width: 600px;
}

Reading rows and cells​

For multi-row blocks (like cards or accordions), iterate the rows directly:

/blocks/cards/cards.js
export default function decorate(block) {
const list = document.createElement('ul');

[...block.children].forEach((row) => {
const li = document.createElement('li');
const cells = [...row.children];

// First cell: image; second cell: text content
const [imageCell, textCell] = cells;

if (imageCell) {
const img = imageCell.querySelector('picture');
if (img) li.append(img);
}
if (textCell) {
const body = document.createElement('div');
body.classList.add('cards-card-body');
body.append(...textCell.childNodes);
li.append(body);
}
list.append(li);
});

block.innerHTML = '';
block.append(list);
}

Content keys with readBlockConfig​

For configuration-style blocks where each row is a key | value pair, use the readBlockConfig helper from aem.js:

| Embed |
|---------------------------------------------|
| Source | https://www.youtube.com/watch?v=.. |
| Aspect | 16:9 |
| Lazy | true |
/blocks/embed/embed.js
import { readBlockConfig } from '../../scripts/aem.js';

export default function decorate(block) {
const config = readBlockConfig(block);
// config = { source: 'https://...', aspect: '16:9', lazy: 'true' }

const iframe = document.createElement('iframe');
iframe.src = config.source;
iframe.loading = config.lazy === 'true' ? 'lazy' : 'eager';
iframe.style.aspectRatio = config.aspect.replace(':', ' / ');

block.innerHTML = '';
block.append(iframe);
}

readBlockConfig lower-cases the keys and handles multi-line values gracefully.

Block variations​

Variations are extra class names appended in parentheses after the block name:

| Hero (dark, centered) |
|------------------------|
| ... |

This produces <div class="hero block dark centered">, allowing CSS-only variants:

.hero.dark { background: #1a1a1a; color: white; }
.hero.centered .hero-content { text-align: center; margin: 0 auto; }

Variations are the preferred way to swap visual treatments. Avoid creating a dark-hero block when hero (dark) will do.

Block options (key-value)​

Some blocks accept key=value options inside the parentheses:

| Carousel (autoplay=true, interval=5000) |
|------------------------------------------|
| ... |

Read them by parsing the variation classes or, more cleanly, by storing options as explicit Option | value rows and using readBlockConfig.

Auto-blocks​

Some blocks aren't authored as tables - they're synthesised from page metadata or specific markup. Examples:

  • hero - automatically built from the first <h1> plus the <picture> that precedes it
  • breadcrumb - built from the URL path
  • embed - built from a bare YouTube / Vimeo link in a paragraph

Auto-blocks are wired in scripts/scripts.js via the buildAutoBlocks decoration hook. See Customizing.

Accessing block metadata​

Blocks can be enriched with metadata authored in a "Section Metadata" or "Page Metadata" sub-table:

| Section Metadata | |
|------------------|---------------|
| style | dark, centered|
| audience | mobile |

The runtime applies style values as space-separated classes on the containing section element, and exposes any other keys as dataset on that same section. From a block inside the section:

const section = block.closest('div.section');
const audience = section?.dataset.audience; // "mobile"

Handy for blocks that need to adapt to surrounding context.

Common patterns​

Decorate then progressively enhance​

Start with semantic HTML that works without JS, then layer interactivity:

export default function decorate(block) {
// 1. Build static markup synchronously (works without further JS)
buildStatic(block);

// 2. Lazy-load interactive behaviour
if (window.matchMedia('(min-width: 900px)').matches) {
import('./carousel-slider.js').then(({ enhance }) => enhance(block));
}
}

Treat the source HTML as untrusted​

Authors will surprise you. Always check that a <picture> or <a> actually exists before reaching for it.

Scope your CSS​

Every selector should start with the block class. Nothing leaks if you keep this discipline:

/* Good */
.hero .hero-content { ... }

/* Bad - leaks to other blocks */
.hero-content { ... }
h1 { ... }

Use CSS variables, not hard-coded colours​

Theme tokens live in styles/styles.css as CSS variables. Reference them in block CSS so theming flows through:

.hero { background: var(--background-color-dark); color: var(--text-color-on-dark); }

The block library (boilerplate)​

The aem-boilerplate (a handful of starter blocks) and the Block Collection (source: aem-block-collection) ship production-ready reference implementations. Copy, don't fork.

BlockPurpose
heroFull-width hero banner with image and CTA
cardsCard grid layout
columnsMulti-column layout
tabsTabbed content
accordionExpandable sections
carouselImage / content slider
embedYouTube, Vimeo, or iframe embeds
fragmentInclude another page as a fragment
header / footerSite-wide navigation
formAdaptive form (see Forms)

Anti-patterns​

Anti-patternWhy it hurtsWhat to do instead
One "super-block" with 10 layoutsHard to author, hard to maintainSplit into focused blocks or use variations
Decorating before the block is in DOMblock.querySelector returns nothingThe runtime calls decorate after insertion - trust it
Side-effects in module top-levelRuns even when the block is absentPut work inside decorate
Bundling everything into scripts.jsInflates LCP-blocking JSEach block owns its JS / CSS
Global CSS rules in block filesLeaks to other blocksScope under the block class
block.innerHTML = '...' with author contentXSS riskManipulate elements, or sanitise

See also​