Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

The Boilerplate

Customizing blocks

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

Boilerplate version: 4.0.1

Three ways to customize

1. CSS styling

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);
}

Design tokens

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 */

2. JavaScript behavior

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);
}

3. Drop-in customization

Use drop-in slots and events for advanced customization.

Slots

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);

Events

Listen to and respond to drop-in events using the event bus:

// 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`);
}
});

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

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);

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

  • More efficient than multiple createElement() calls.
  • Clear visual representation of HTML structure.
  • Easier to maintain complex layouts.

Boilerplate examples

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 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);

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

  • More explicit and type-safe than template literals.
  • Cleaner for single elements (no unnecessary parsing).
  • Easier to set properties programmatically.

Boilerplate examples

Common customization patterns

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

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);

Adding custom analytics

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,
});
}
});

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

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

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

Commerce Checkout

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

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);
}
});

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

Example 1: Add promotional banner to cart

// 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);
}

Example 2: Custom checkout success tracking

// 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);
}

Next steps

  1. Browse the Commerce blocks source code to see implementation patterns.

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

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

  4. Test customizations locally before deploying to production.