Skip to main content

Storefront Extensibility — DOM Events & Grid Injections

Developer guide for the two extensibility features in the Marqo storefront search widget.

DOM Events

The storefront widget emits CustomEvents on document at key lifecycle points. Merchants listen from a <script> tag in their Shopify theme to add custom behavior without code changes.

Event Reference

EventWhenCancelableKey Payload Fields
marqo:readyWidget initializedNo{}
marqo:destroyWidget being torn downNo{}
marqo:search.startSearch request about to fireNo{ query, collection? }
marqo:search.resultsResults received and renderedNo{ products, query, total, facets, container }
marqo:search.emptyZero results returnedNo{ query }
marqo:search.errorSearch request failedNo{ error }
marqo:card.renderEach product card rendered (fires per card)No{ product, element, index }
marqo:cta.clickCTA button clickedYes{ product, handle, productUrl, shopifyProductId, element }
marqo:filter.changeFilter value changedNo{ field, type, value }
marqo:filter.clearAll filters clearedNo{}
marqo:sort.changeSort option changedNo{ sort }
marqo:page.changePage navigationNo{ page }
marqo:grid.injectedGrid injections renderedNo{ injections }

All events have bubbles: true. Only marqo:cta.click is cancelable.

Listening for Events

Add a <script> tag to the merchant's theme.liquid before </body>:

<script>
document.addEventListener('marqo:search.results', function(e) {
console.log('Search complete:', e.detail.total, 'results for', e.detail.query);
});
</script>

Cancelable Events

marqo:cta.click is the only cancelable event. Calling e.preventDefault() stops the default navigation to the product detail page:

<script>
document.addEventListener('marqo:cta.click', function(e) {
e.preventDefault(); // Stop navigation to PDP
// Do something else instead (e.g., open a quickshop modal)
console.log('CTA clicked for:', e.detail.handle);
});
</script>

Event Payload Details

marqo:search.start

{
query: string; // User's search query (empty string for collection pages)
collection?: string; // Collection name (only on collection pages)
}

marqo:search.results

{
products: MarqoSearchHit[]; // Array of product result objects
query: string; // Search query
total: number; // Total result count
facets: object; // Facet/filter data
container: HTMLElement; // The results grid DOM element
}

marqo:card.render

{
product: MarqoSearchHit; // Product data for this card
element: HTMLElement; // The card's DOM element
index: number; // Position in the results (0-indexed)
}

marqo:cta.click

{
product: MarqoSearchHit; // Full product data
handle: string; // Product handle (e.g., "baked-starter-kit")
productUrl: string; // "/products/baked-starter-kit"
shopifyProductId: string; // Shopify product ID
element: HTMLElement; // The CTA button DOM element
}

Use Cases

Quickshop Modal (theme-specific)

Bridge the CTA click to the merchant's existing theme quickshop:

<script>
document.addEventListener('marqo:cta.click', function(e) {
e.preventDefault();

// Create reusable quickshop modal (theme-specific web component)
var modal = document.querySelector('#MarqoQuickAdd');
if (!modal) {
document.body.insertAdjacentHTML('beforeend',
'<quick-add-modal id="MarqoQuickAdd" class="quick-add-modal">' +
'<div role="dialog" class="quick-add-modal__content global-settings-popup" tabindex="-1">' +
'<button type="button" class="quick-add-modal__toggle" aria-label="Close">&times;</button>' +
'<div class="quick-add-modal__content-info"></div>' +
'</div>' +
'</quick-add-modal>'
);
modal = document.querySelector('#MarqoQuickAdd');
}

e.detail.element.setAttribute('data-product-url', e.detail.productUrl);
modal.show(e.detail.element);
});
</script>

Analytics Integration

Track search and click events:

<script>
document.addEventListener('marqo:search.results', function(e) {
analytics.track('search', {
query: e.detail.query,
resultCount: e.detail.total,
});
});

document.addEventListener('marqo:cta.click', function(e) {
analytics.track('product_click', {
handle: e.detail.handle,
position: e.detail.product._score,
});
});
</script>

Third-Party Widget Hydration (Okendo, Videowise, etc.)

Hydrate third-party widgets after Marqo renders product cards:

<script>
document.addEventListener('marqo:search.results', function() {
// Re-initialize Okendo widgets on new cards
if (window.oke) window.oke.initWidget();
});

document.addEventListener('marqo:grid.injected', function() {
// Hydrate Videowise containers injected into the grid
if (window.Videowise) window.Videowise.init();
});
</script>

Grid Injections

Insert non-product content (promo banners, video tiles, editorial blocks) at configurable positions within the product results grid. Each injection is a standalone grid item configured per-shop via the admin UI.

How It Works

+----------+ +----------+ +----------+ +----------+
|Product 1 | |Product 2 | |Product 3 | |Product 4 |
+----------+ +----------+ +----------+ +----------+
+-----------------------------------------------------+
| Grid Injection (position: 4, span: 4) |
| "Complete Your Routine -- Shop Routines" |
+-----------------------------------------------------+
+----------+ +----------+ +----------+ +----------+
|Product 5 | |Product 6 | |Product 7 | |Product 8 |
+----------+ +----------+ +----------+ +----------+

Injections are siblings of product cards in the CSS grid, not nested inside them. The span property controls how many grid columns each injection stretches across.

Configuration (Admin UI)

  1. Go to Settings > Grid Injections in the admin UI
  2. Click Add Content Block
  3. Configure:
    • Position: Where to insert (0 = before first product, 4 = after 4th product)
    • Column Span: How many grid columns to span (1-4)
    • HTML: The content HTML
    • CSS: Styling for the content
    • Enabled: Toggle on/off

Configuration (Backend API)

Grid injections are stored as a UIComponent in the settings record:

{
"grid_injections": {
"name": "Grid Injections",
"enabled": true,
"value": {
"items": [
{
"id": "spring-promo",
"position": 4,
"span": 4,
"html": "<div class='promo'>Spring Sale — 20% off</div>",
"css": ".promo { text-align: center; padding: 24px; background: #fef3c7; border-radius: 8px; }",
"enabled": true
}
]
}
}
}

Position Behavior

  • Position 0: Before the first product card
  • Position N: After the Nth product card
  • Position > result count: Placed at the end of the grid
  • Negative positions: Clamped to 0
  • Positions are relative to the current page's result set, not the global result set

Column Span

  • span: 1: Same width as a single product card
  • span: 2: Spans 2 grid columns
  • span: 4: Full-width banner (assumes 4-column desktop grid)

CSS Scoping

Injection CSS is injected into a dedicated <style id="marqo-grid-injection-styles"> element. Use specific selectors scoped to your injection content to avoid affecting the rest of the page:

/* Good — scoped to injection content */
.marqo-grid-injection .my-banner { padding: 24px; }

/* Avoid — affects the entire page */
.banner { padding: 24px; }

Examples

Static Promo Banner

Position: 4 Span: 4

HTML:

<div class="routine-banner">
<img src="https://cdn.shopify.com/s/files/banner.jpg" alt="Complete Your Routine" />
<a href="/collections/routines">Shop Routines &rarr;</a>
</div>

CSS:

.routine-banner {
text-align: center;
padding: 32px 24px;
background: #faf5f0;
border-radius: 8px;
}
.routine-banner img {
max-width: 100%;
border-radius: 4px;
margin-bottom: 12px;
}
.routine-banner a {
color: #000;
font-weight: 600;
text-decoration: underline;
}

Video Tile (Videowise)

Position: 8 Span: 2

HTML:

<div class="videowise-container" data-videowise-id="abc123"></div>

Then in theme.liquid:

<script>
document.addEventListener('marqo:grid.injected', function() {
if (window.Videowise) window.Videowise.init();
});
</script>

Editorial Content Block

Position: 12 Span: 2

HTML:

<div class="editorial">
<h3>Expert Tips</h3>
<p>Our makeup artists recommend starting with primer for all-day wear.</p>
<a href="/blogs/tips">Read More</a>
</div>

Limitations

  • No Handlebars variables: Unlike product card templates, injection HTML is static — it does not have access to {{product}} data or search context. For dynamic content, use DOM events to populate containers after render.
  • No <script> execution: HTML inserted via innerHTML does not execute <script> tags. Use theme-level scripts with DOM event listeners instead.
  • CSS is global: Injection CSS is not automatically scoped. Use specific class selectors.

Debugging

Monitor All Marqo Events in Chrome DevTools

Paste this snippet in the browser console to log every Marqo event as it fires:

[
'marqo:ready', 'marqo:destroy',
'marqo:search.start', 'marqo:search.results', 'marqo:search.empty', 'marqo:search.error',
'marqo:card.render', 'marqo:cta.click',
'marqo:filter.change', 'marqo:filter.clear', 'marqo:sort.change', 'marqo:page.change',
'marqo:grid.injected'
].forEach(name => {
document.addEventListener(name, (e) => {
console.log(`%c${name}`, 'color: #3b82f6; font-weight: bold', e.detail);
});
});

Then interact with the search page — events appear in blue with their payloads.

Note: marqo:ready fires once on widget init. If you paste the listener after the page has loaded, you will not see it. Refresh the page after pasting to catch it.

Verify Grid Injections

After configuring grid injections in the admin UI, check that marqo:grid.injected fires after a search. The payload contains the injections array:

document.addEventListener('marqo:grid.injected', (e) => {
console.log('Injections rendered:', e.detail.injections);
});

Verify CTA Cancellation

To test that preventDefault() stops navigation:

document.addEventListener('marqo:cta.click', (e) => {
e.preventDefault();
console.log('Blocked navigation for:', e.detail.handle);
});

Implementation Details

Source Files

DOM Events:

  • storefront_search/src/ui-rendering/dom-manager.tsdispatchMarqoEvent() helper + MarqoEventName type
  • storefront_search/src/ui-rendering.tsmarqo:search.results, marqo:search.empty, marqo:card.render
  • storefront_search/src/ui-rendering/event-handlers.tsmarqo:filter.change, marqo:filter.clear, setupCtaClickHandler
  • storefront_search/src/event-manager.tsmarqo:sort.change, marqo:page.change
  • storefront_search/src/marqo-search-app.tsmarqo:ready, marqo:destroy
  • storefront_search/src/search.tsmarqo:search.start, marqo:search.error

Grid Injections:

  • admin_server/models/ui_settings.pygrid_injections UIComponent
  • ecom_admin/app/lib/types.tsGridInjectionItem, GridInjectionsSettings
  • ecom_admin/app/lib/settings-converter.ts — frontend/backend conversion
  • ecom_admin/app/components/settings/grid-injections-section.tsx — admin UI
  • storefront_search/src/ui-rendering.tsspliceGridInjections()

Test Files

  • storefront_search/tests/ui-knobs/dom-events.test.ts — unit tests for dispatchMarqoEvent
  • storefront_search/tests/dom-events-integration.test.ts — integration tests for CTA handler + all event payloads
  • storefront_search/tests/grid-injections.test.ts — splice logic tests
  • storefront_search/tests/grid-injections-comprehensive.test.ts — edge cases, CSS, config, template wiring
  • ecom_admin/app/lib/__tests__/settings-converter.test.ts — converter round-trip tests