Skip to content

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

Commerce Drop-Ins

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.

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 insteadWhen
ExtendA drop-in container already covers the feature, and you only need to change its look, add content, or react to its events.
CreateThe 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.

Three elements, already installed in your storefront, let you build a new feature without creating a drop-in.

Call the function a drop-in already exports from its api.js. It’s the same function the drop-in’s own containers use internally, so you get the same data from the same backend without reimplementing any Commerce logic.

import { search } from '@dropins/storefront-product-discovery/api.js';
const result = await search({ phrase: 'hoodie', pageSize: 5 });

Render shared UI components, such as Button, Image, Input, and PriceRange, from @dropins/tools/components.js. These components are the same building blocks drop-in containers use, so your block gets the same styling and accessibility.

import { Button, provider } from '@dropins/tools/components.js';
provider.render(Button, { children: 'Compare' })(wrapper);

See the .render() reference for how the provider mounts a component into a DOM node. See the component overview for the full list.

Emit and listen for events with @dropins/tools/event-bus.js so your block and other blocks on the page can coordinate without importing each other.

import { events } from '@dropins/tools/event-bus.js';
events.emit('compare/products', { sku: 'MH01-XS-Black' });

Choose your own event name for a block-to-block signal that no drop-in emits yet. See Events for the shared naming conventions.

Loading diagram...
A plain block pulls data from an existing drop-in API function, renders it with shared SDK components, and coordinates with other blocks through the event bus.

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 search function that looks up products using keywords or SKUs.
  • Storefront SDK — Provides the UI primitives (Button, Image, Input, and PriceRange) 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.

  1. 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 a sku filter or a phrase:

    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 ?? [];
    }
  2. Render the UI with SDK components. Mount Image and PriceRange for each product column and a Button for 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);
  3. 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 button
    import { 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 block
    import { 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
    });
  4. 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);
  • 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/verb convention already used by drop-in events, such as cart/updated and search/result, so another developer can tell what your custom event does without reading its source.
  • Before adding localStorage or 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.