Skip to content
Blocks

Customize blocks

All commerce blocks in the boilerplate work out of the box, but you can customize them to match your business requirements. This page covers the main customization approaches.

Most files in the boilerplate can be modified. The table below lists the usual layers you change for branding, blocks, Commerce wiring, page scripts, and build-time queries. Order follows a common path: global styles and blocks first, then initializer and script layers, then build.mjs for query shaping.

LayerStrategy
styles/Change colors, fonts, spacing, and layout here. Tokens in styles/styles.css apply across the whole site.
blocks/Edit block appearance and behavior. Keep block folder names unchanged because authors reference them by name in documents.
scripts/initializers/Configure the API endpoint and UI labels for each drop-in. Each drop-in has one initializer file, such as cart.js or checkout.js. If you remove a drop-in, also remove its initializer file and all imports of it in the codebase. See Connect a drop-in.
scripts/commerce.jsEdit this file to add Commerce-specific logic such as consent handling (getConsent) or custom page type detection. It reads your storefront configuration and sets up Commerce API connections on every page.
scripts/scripts.jsAdd automatically generated blocks to buildAutoBlocks (the boilerplate uses this for the hero block). Load third-party scripts, tag managers, or analytics tools in scripts/delayed.js. That file runs 3 seconds after page load so these scripts do not affect the initial page render.
build.mjsEdit this file to customize what data drop-ins fetch from Commerce. For example, you can add a custom product field to the PDP query or skip query fragments your project doesn’t need. Use the existing entries as patterns. Changes take effect after running npm install.
  • scripts/aem.js: The core AEM runtime. This file comes from the original AEM Boilerplate . Local edits can conflict with future boilerplate updates.
  • package.json lifecycle scripts (postinstall, postupdate, install:dropins): These scripts run during npm install to install and configure drop-ins. Changing them can break drop-in installation.

When you customize blocks or initializers, put project-specific code in new files when possible. For example, you can add slot customizations to a new blocks/commerce-cart/slots.js file and import it at the top of commerce-cart.js. Your customizations stay in a file you own. When a boilerplate update arrives, your changes to the original file stay small.

Modify block styles by editing the CSS file in the block directory:

blocks/commerce-cart/commerce-cart.css
.commerce-cart {
padding: var(--spacing-large) 0;
position: relative;
}
.cart__wrapper {
display: flex;
flex-direction: column;
gap: var(--grid-4-gutters);
}

Use CSS variables from your theme for consistent styling:

/* Common design tokens */
var(--spacing-small) /* 16px */
var(--spacing-medium) /* 24px */
var(--spacing-large) /* 64px */
var(--color-brand-500) /* #454545 - Brand color */
var(--color-neutral-100) /* #fafafa - Light background */
var(--type-body-1-default-font) /* 16px/24px - Body text */

Modify block logic by editing the JavaScript file in the block directory:

blocks/commerce-cart/commerce-cart.js
import { render as provider } from '/@dropins/storefront-cart/render.js';
import CartSummaryList from '/@dropins/storefront-cart/containers/CartSummaryList.js';
import { readBlockConfig } from '/scripts/aem.js';
export default async function decorate(block) {
// Read configuration from document authoring
const config = readBlockConfig(block);
// Your custom logic here
const maxItems = parseInt(config['max-items'], 10) || 10;
// Create container element
const $list = document.createElement('div');
$list.className = 'cart__list';
block.appendChild($list);
// Render drop-in container to the list element
await provider.render(CartSummaryList, {
maxItems,
enableRemoveItem: true,
})($list);
}

Use drop-in slots and events for advanced customization.

Inject custom content into drop-in containers by passing slot functions in the configuration:

// Inside the decorate function for your block
import { render as provider } from '/@dropins/storefront-cart/render.js';
import CartSummaryList from '/@dropins/storefront-cart/containers/CartSummaryList.js';
// Create container element
const $list = document.createElement('div');
$list.className = 'cart__list';
block.appendChild($list);
// Render cart with custom slot
await provider.render(CartSummaryList, {
slots: {
Footer: (ctx) => {
// Add custom content to cart item footer
const customElement = document.createElement('div');
customElement.className = 'custom-promotion';
customElement.textContent = 'Free shipping over $50!';
ctx.appendChild(customElement);
},
},
})($list);

Listen to and respond to drop-in events using the event bus. See How drop-ins coordinate if you’re not yet familiar with how the event bus works:

// Inside the decorate function for your block or at the module level
import { events } from '/@dropins/tools/event-bus.js';
events.on('cart/data', (cartData) => {
console.log('Cart updated:', cartData);
// Example: Show notification when cart has items
if (cartData.totalQuantity > 0) {
console.log(`You have ${cartData.totalQuantity} items in your cart`);
}
});

Before your block’s decorate() function runs, Edge Delivery Services has already converted the block’s document table into nested <div> elements. The table’s first row names the block — for example, “Commerce Cart” — and becomes the block’s class name and data-block-name attribute; it does not become a div itself. Every row after that becomes a row div, and each cell in that row becomes a nested cell div inside it.

Loading diagram...
How a document table maps to a block's DOM. Row 1 names the block and becomes its class and data-block-name attribute rather than a row div. Row 2, and every row after it, becomes a row div, with each cell becoming a nested cell div.

Your decorate(block) function receives the outer block element as block, with that row-and-cell structure already inside it.

Edge Delivery Services also adds two data attributes to the block element for you. data-block-name holds the block’s name, taken from its first CSS class. data-block-status tracks the block’s load state and moves through initialized, loading, and loaded as the block’s CSS and JavaScript load. You don’t set either attribute yourself.

Whether decorate() should remove that original row-and-cell markup depends on the block. Some blocks clear it — the block.innerHTML = '' call in the promotional banner example below removes the original divs before appending new markup. Other blocks read values from the original divs and update them in place, without clearing anything. Check whether the block you’re customizing depends on its original markup before you clear it.

The boilerplate uses two approaches for creating DOM elements. The key distinction: use createContextualFragment for multiple elements, createElement() for single elements.

Template literals with createContextualFragment

Section titled “Template literals with createContextualFragment”

Use this approach when creating two or more elements or complex nested structures.

// Best for: Complex layout structures with multiple nested elements
const fragment = document.createRange().createContextualFragment(`
<div class="cart__notification"></div>
<div class="cart__wrapper">
<div class="cart__left-column">
<div class="cart__list"></div>
</div>
<div class="cart__right-column">
<div class="cart__order-summary"></div>
</div>
</div>
`);
const $list = fragment.querySelector('.cart__list');
block.appendChild(fragment);
  • Creating two or more elements at once.
  • Setting up initial block structure with nested elements.
  • HTML hierarchy visualization improves code readability.
  • More efficient than multiple createElement() calls.
  • Clear visual representation of HTML structure.
  • Easier to maintain complex layouts.

Use this approach when creating a single element. This is cleaner and more explicit than template literals for one element.

// Best for: Single elements created dynamically
// Inside a drop-in slot function
const $wishlistToggle = document.createElement('div');
$wishlistToggle.classList.add('cart__action--wishlist-toggle');
wishlistRender.render(WishlistToggle, {
product: ctx.item,
size: 'medium',
})($wishlistToggle);
ctx.appendChild($wishlistToggle);
  • Creating a single element (most common case).
  • Adding elements inside drop-in slots.
  • Building elements in event handlers or callbacks.
  • Creating elements conditionally.
  • More explicit and type-safe than template literals.
  • Cleaner for single elements (no unnecessary parsing).
  • Easier to set properties programmatically.

Enable merchants to configure blocks through document authoring by reading configuration values:

import { readBlockConfig } from '/scripts/aem.js';
export default async function decorate(block) {
const {
'custom-option': customOption = 'default',
'max-items': maxItems = '10',
'enable-feature': enableFeature = 'false',
} = readBlockConfig(block);
// Use configuration values
if (enableFeature === 'true') {
// Enable the feature
}
}

Merchants can then configure the block in their documents:

Commerce Cart
custom-option
max-items
enable-feature

Customize what users see when a block has no content:

// Inside the decorate function for your block
const $emptyCart = document.querySelector('.cart__empty-cart');
// Create custom empty state
const emptyState = document.createElement('div');
emptyState.className = 'cart__empty-message';
emptyState.innerHTML = `
<h3>Your cart is empty</h3>
<p>Start shopping to add items to your cart.</p>
<a href="/products" class="button">Browse Products</a>
`;
$emptyCart.appendChild(emptyState);

Track custom events for analytics:

// Add to your block file or scripts/analytics.js
import { events } from '/@dropins/tools/event-bus.js';
events.on('cart/data', (cartData) => {
// Custom analytics tracking
if (window.dataLayer) {
window.dataLayer.push({
event: 'cart_updated',
cart_total: cartData.totalQuantity,
cart_value: cartData.total.includingTax.value,
currency: cartData.total.includingTax.currency,
});
}
});

Combine multiple drop-ins for complex functionality:

import { render as provider } from '/@dropins/storefront-cart/render.js';
import { render as wishlistRender } from '/@dropins/storefront-wishlist/render.js';
import CartSummaryList from '/@dropins/storefront-cart/containers/CartSummaryList.js';
import { WishlistToggle } from '/@dropins/storefront-wishlist/containers/WishlistToggle.js';
export default async function decorate(block) {
// Create container element
const $list = document.createElement('div');
$list.className = 'cart__list';
block.appendChild($list);
// Render cart with wishlist integration
await provider.render(CartSummaryList, {
slots: {
Footer: (ctx) => {
// Add wishlist toggle to each cart item
const wishlistContainer = document.createElement('div');
wishlistContainer.className = 'cart__action--wishlist-toggle';
wishlistRender.render(WishlistToggle, {
product: ctx.item,
size: 'medium',
})(wishlistContainer);
ctx.appendChild(wishlistContainer);
},
},
})($list);
}

Some blocks accept configuration options that change their behavior. These are defined in the block README and can be set through document authoring.

OptionTypeDefaultDescription
hide-headingstringfalseControls whether the cart heading is hidden
max-itemsstringMaximum number of items to display in cart
hide-attributesstring''Comma-separated list of product attributes to hide
enable-item-quantity-updatestringfalseEnables quantity update controls for cart items
enable-item-removestringtrueEnables remove item functionality
enable-estimate-shippingstringfalseEnables shipping estimation functionality
start-shopping-urlstring''URL for “Start Shopping” button when cart is empty
checkout-urlstring''URL for checkout button
enable-updating-productstringfalseEnables product editing via mini-PDP modal
undo-remove-itemstringfalseEnables undo functionality when removing items

The Checkout block uses events for customization rather than configuration options.

Example: Add custom validation before checkout

Section titled “Example: Add custom validation before checkout”
// Add to the decorate function for your checkout block
import { events } from '/@dropins/tools/event-bus.js';
events.on('checkout/values', (values) => {
// Custom validation logic
if (values.email && !values.email.includes('@')) {
console.error('Invalid email format');
}
// Log when payment method is selected
if (values.selectedPaymentMethod) {
console.log('Payment method:', values.selectedPaymentMethod.code);
}
});

Many blocks (Login, Create Account, Forgot Password, and other account management blocks) are thin wrappers around drop-ins with no document-based configuration options. These blocks do not use readBlockConfig() and cannot be configured through document authoring. Customize these blocks by modifying their JavaScript to change drop-in options, or by using CSS and drop-in slots.

blocks/commerce-cart/commerce-cart.js
import { render as provider } from '/@dropins/storefront-cart/render.js';
import CartSummaryList from '/@dropins/storefront-cart/containers/CartSummaryList.js';
import { readBlockConfig } from '/scripts/aem.js';
import { getProductLink } from '/scripts/commerce.js';
export default async function decorate(block) {
// Read block configuration
const {
'hide-heading': hideHeading = 'false',
'max-items': maxItems,
'enable-item-quantity-update': enableUpdateItemQuantity = 'false',
'enable-item-remove': enableRemoveItem = 'true',
} = readBlockConfig(block);
// Create cart layout with promotional banner
const fragment = document.createRange().createContextualFragment(`
<div class="cart__promo-banner">
<p>🎉 Free shipping on orders over $50!</p>
</div>
<div class="cart__list"></div>
`);
const $list = fragment.querySelector('.cart__list');
// Clear block and append new layout
block.innerHTML = '';
block.appendChild(fragment);
// Helper to create product links
const createProductLink = (product) => getProductLink(product.url.urlKey, product.topLevelSku);
// Render cart with full configuration
await provider.render(CartSummaryList, {
hideHeading: hideHeading === 'true',
routeProduct: createProductLink,
maxItems: parseInt(maxItems, 10) || undefined,
enableUpdateItemQuantity: enableUpdateItemQuantity === 'true',
enableRemoveItem: enableRemoveItem === 'true',
})($list);
}
blocks/commerce-cart/commerce-cart.css
.cart__promo-banner {
background: var(--color-positive-200);
padding: var(--spacing-medium);
text-align: center;
border-radius: 4px;
margin-bottom: var(--spacing-medium);
}
// Add to blocks/commerce-checkout/commerce-checkout.js
// Place this event listener in your decorate function
import { events } from '/@dropins/tools/event-bus.js';
// Listen for order placement
events.on('order/placed', (orderData) => {
// Send purchase event to Google Analytics
if (window.dataLayer) {
window.dataLayer.push({
event: 'purchase',
transaction_id: orderData.number,
value: orderData.grandTotal.value,
currency: orderData.grandTotal.currency,
items: orderData.items.map(item => ({
item_id: item.productSku,
item_name: item.productName,
quantity: item.quantityOrdered,
price: item.price.value,
})),
});
}
// Update page title
document.title = 'Order Confirmation';
});
blocks/product-details/product-details.js
// Imports (these should already exist at the top of the file)
import { render as pdpRendered } from '/@dropins/storefront-pdp/render.js';
import ProductGallery from '/@dropins/storefront-pdp/containers/ProductGallery.js';
import { tryRenderAemAssetsImage } from '/@dropins/tools/lib/aem/assets.js';
// Inside your decorate function
export default async function decorate(block) {
// Create container element
const $gallery = document.createElement('div');
$gallery.className = 'product-details__gallery';
block.appendChild($gallery);
// Define custom image slot with AEM Assets
const gallerySlots = {
CarouselMainImage: (ctx) => {
// Customize main carousel images
tryRenderAemAssetsImage(ctx, {
alias: ctx.data.sku,
imageProps: ctx.defaultImageProps,
params: {
width: ctx.defaultImageProps.width,
height: ctx.defaultImageProps.height,
},
});
},
};
// Render gallery with custom slots
await pdpRendered.render(ProductGallery, {
controls: 'thumbnailsColumn',
arrows: true,
imageParams: {
width: 960,
height: 1191,
},
slots: gallerySlots,
})($gallery);
}
  1. Check the Blocks reference for the full list of blocks, their drop-ins, and page-type groupings.

  2. Browse the commerce blocks source code to see implementation patterns.

  3. Review the drop-in documentation for slots, events, and API functions.

  4. Check individual block README files for configuration options and behavior details.

  5. Test customizations locally before deploying to production.