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.
Where to customize
Section titled “Where to customize”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.
| Layer | Strategy |
|---|---|
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.js | Edit 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.js | Add 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.mjs | Edit 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. |
Files to keep unchanged
Section titled “Files to keep unchanged”scripts/aem.js: The core AEM runtime. This file comes from the original AEM Boilerplate . Local edits can conflict with future boilerplate updates.package.jsonlifecycle scripts (postinstall,postupdate,install:dropins): These scripts run duringnpm installto install and configure drop-ins. Changing them can break drop-in installation.
When to customize
Section titled “When to customize”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.
Ways to customize
Section titled “Ways to customize”Block DOM structure
Section titled “Block DOM structure”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.
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.
Creating DOM elements
Section titled “Creating DOM elements”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 elementsconst 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);When to use
Section titled “When to use”- Creating two or more elements at once.
- Setting up initial block structure with nested elements.
- HTML hierarchy visualization improves code readability.
Why this approach
Section titled “Why this approach”- More efficient than multiple
createElement()calls. - Clear visual representation of HTML structure.
- Easier to maintain complex layouts.
Boilerplate examples
Section titled “Boilerplate examples”document.createElement()
Section titled “document.createElement()”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 functionconst $wishlistToggle = document.createElement('div');$wishlistToggle.classList.add('cart__action--wishlist-toggle');
wishlistRender.render(WishlistToggle, { product: ctx.item, size: 'medium',})($wishlistToggle);
ctx.appendChild($wishlistToggle);When to use
Section titled “When to use”- Creating a single element (most common case).
- Adding elements inside drop-in slots.
- Building elements in event handlers or callbacks.
- Creating elements conditionally.
Why this approach
Section titled “Why this approach”- More explicit and type-safe than template literals.
- Cleaner for single elements (no unnecessary parsing).
- Easier to set properties programmatically.
Boilerplate examples
Section titled “Boilerplate examples”- commerce-cart.js (lines 221-232) - Creating wishlist container
- commerce-cart.js (lines 206-217) - Creating edit link container
Common customization patterns
Section titled “Common customization patterns”Adding block configuration
Section titled “Adding block configuration”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 |
Customizing empty states
Section titled “Customizing empty states”Customize what users see when a block has no content:
// Inside the decorate function for your blockconst $emptyCart = document.querySelector('.cart__empty-cart');
// Create custom empty stateconst 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);Adding custom analytics
Section titled “Adding custom analytics”Track custom events for analytics:
// Add to your block file or scripts/analytics.jsimport { 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, }); }});Using multiple drop-ins
Section titled “Using multiple drop-ins”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);}Block-specific configuration
Section titled “Block-specific configuration”Some blocks accept configuration options that change their behavior. These are defined in the block README and can be set through document authoring.
Commerce Cart
Section titled “Commerce Cart”| Option | Type | Default | Description |
|---|---|---|---|
hide-heading | string | false | Controls whether the cart heading is hidden |
max-items | string | — | Maximum number of items to display in cart |
hide-attributes | string | '' | Comma-separated list of product attributes to hide |
enable-item-quantity-update | string | false | Enables quantity update controls for cart items |
enable-item-remove | string | true | Enables remove item functionality |
enable-estimate-shipping | string | false | Enables shipping estimation functionality |
start-shopping-url | string | '' | URL for “Start Shopping” button when cart is empty |
checkout-url | string | '' | URL for checkout button |
enable-updating-product | string | false | Enables product editing via mini-PDP modal |
undo-remove-item | string | false | Enables undo functionality when removing items |
Commerce Checkout
Section titled “Commerce Checkout”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 blockimport { 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); }});Simple blocks
Section titled “Simple blocks”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.
Real-world examples
Section titled “Real-world examples”Next steps
Section titled “Next steps”-
Check the Blocks reference for the full list of blocks, their drop-ins, and page-type groupings.
-
Browse the commerce blocks source code to see implementation patterns.
-
Review the drop-in documentation for slots, events, and API functions.
-
Check individual block README files for configuration options and behavior details.
-
Test customizations locally before deploying to production.