Compose a custom block
Sometimes the feature you need doesn’t match any drop-in container. Before you use the Storefront SDK to create a new drop-in, check whether the elements already exist. Most installed drop-ins export API functions you can call directly, and the SDK provides a library of shared UI components. Combine those elements with the same event busA shared in-memory channel that lets drop-in components on the same page publish and subscribe to events without depending directly on each other. drop-ins use to communicate, and you can build the feature yourself in a plain block.
When to use this pattern
Section titled “When to use this pattern”Use this pattern when no drop-in container covers the feature, an existing drop-in already exports the data as an API function, and you can build the UI from shared SDK components.
| Choose instead | When |
|---|---|
| Extend | A drop-in container already covers the feature, and you only need to change its look, add content, or react to its events. |
| Create | The feature needs its own data model, containers, and events that no installed drop-in exposes, and you’re prepared to maintain a new drop-in package long-term. |
Composing in a plain block costs less than creating a drop-in: no new package to publish, version, or maintain, and no drop-in SDK dependency to add. The tradeoff is that you maintain the composition logic yourself, and it won’t appear in any drop-in’s release notes or upgrade path.
The three elements
Section titled “The three elements”Three elements, already installed in your storefront, let you build a new feature without creating a drop-in.
Walkthrough: product comparison
Section titled “Walkthrough: product comparison”Picture a table showing up to three products side by side, plus a persistent tray that collects the products a shopper picks while browsing. No drop-in container covers that feature, but everything the feature needs is already installed:
- Existing drop-in — The Product Discovery drop-in already exports a
searchfunction that looks up products using keywords or SKUs. - Storefront SDK — Provides the UI primitives (
Button,Image,Input, andPriceRange) for the feature’s UI. - Event bus — Lets a Compare button on the product list page signal your comparison tray to add a product, without either block importing the other.
The steps below show how this feature can be built as two plain blocks: product-compare for the table and product-compare-bar for the tray.
-
Fetch data using existing API functions. To look up the exact products a shopper picked or to power a search-to-add field, call
search()with askufilter or aphrase:import { search } from '@dropins/storefront-product-discovery/api.js';async function fetchProductsBySkus(skus) {const result = await search({ filter: [{ attribute: 'sku', in: skus }], pageSize: skus.length },);return result?.items ?? [];} -
Render the UI with SDK components. Mount
ImageandPriceRangefor each product column and aButtonfor the remove action — the same components a drop-in container uses:import {Button, Icon, Image, PriceRange, provider,} from '@dropins/tools/components.js';import { h } from '@dropins/tools/preact.js';await provider.render(Image, { src: product.images[0].url, alt: product.name })(imageWrap);await provider.render(PriceRange, { minimumAmount, maximumAmount, currency })(priceWrap);await provider.render(Button, {icon: h(Icon, { source: 'Close' }),'aria-label': `Remove ${product.name}`,onClick: () => removeProduct(product.sku),})(removeWrap); -
Coordinate blocks with the event bus. The product list page block doesn’t know the comparison tray exists, and the tray doesn’t know which block added a product. They only agree on an event name and a payload shape:
// In the product list page block, on the Compare buttonimport { events } from '@dropins/tools/event-bus.js';events.emit('compare/products', { sku: product.sku, img: product.images?.[0]?.url, name: product.name });// In the comparison tray blockimport { events } from '@dropins/tools/event-bus.js';events.on('compare/products', ({ sku, img, name }) => {// add sku to the tray's in-memory list and re-render}); -
Store state in the URL. The comparison table’s state — which SKUs to compare — doesn’t need a drop-in or
localStorage. Reading and writing that state to a URL query parameter means shoppers can share or bookmark the comparison page:const skus = new URLSearchParams(window.location.search).get('compare')?.split(',') ?? [];// After adding or removing a product:const url = new URL(window.location.href);url.searchParams.set('compare', updatedSkus.join(','));window.history.replaceState({}, '', url);
Best practices
Section titled “Best practices”- Reuse the drop-in’s existing filters, scopes, and pagination options instead of writing your own data-fetching code, since you’re calling the same function a container calls.
- Follow the
noun/verbconvention already used by drop-in events, such ascart/updatedandsearch/result, so another developer can tell what your custom event does without reading its source. - Before adding
localStorageor a new global variable, check whether the URL or an event’s last payload (events.lastPayload()) already has what you need. - Use this pattern only when no container covers the job. If a drop-in container already handles most of the feature, extend it with slots or events instead of rebuilding its API function and components yourself.
Next steps
Section titled “Next steps”- Review Extend, substitute, or create? to place this pattern alongside your other options.
- Read the drop-in Functions reference for the full
search()signature and its data models. - Browse the SDK component library to see all available components.
- Read Events for event-naming conventions and the full
events.on()/events.emit()API. - Read Blocks customization for
readBlockConfig()and other block-authoring patterns.