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
| Event | When | Cancelable | Key Payload Fields |
|---|---|---|---|
marqo:ready | Widget initialized | No | {} |
marqo:destroy | Widget being torn down | No | {} |
marqo:search.start | Search request about to fire | No | { query, collection? } |
marqo:search.results | Results received and rendered | No | { products, query, total, facets, container } |
marqo:search.empty | Zero results returned | No | { query } |
marqo:search.error | Search request failed | No | { error } |
marqo:card.render | Each product card rendered (fires per card) | No | { product, element, index } |
marqo:cta.click | CTA button clicked | Yes | { product, handle, productUrl, shopifyProductId, element } |
marqo:filter.change | Filter value changed | No | { field, type, value } |
marqo:filter.clear | All filters cleared | No | {} |
marqo:sort.change | Sort option changed | No | { sort } |
marqo:page.change | Page navigation | No | { page } |
marqo:grid.injected | Grid injections rendered | No | { 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">×</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)
- Go to Settings > Grid Injections in the admin UI
- Click Add Content Block
- 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 →</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 viainnerHTMLdoes 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.ts—dispatchMarqoEvent()helper +MarqoEventNametypestorefront_search/src/ui-rendering.ts—marqo:search.results,marqo:search.empty,marqo:card.renderstorefront_search/src/ui-rendering/event-handlers.ts—marqo:filter.change,marqo:filter.clear,setupCtaClickHandlerstorefront_search/src/event-manager.ts—marqo:sort.change,marqo:page.changestorefront_search/src/marqo-search-app.ts—marqo:ready,marqo:destroystorefront_search/src/search.ts—marqo:search.start,marqo:search.error
Grid Injections:
admin_server/models/ui_settings.py—grid_injectionsUIComponentecom_admin/app/lib/types.ts—GridInjectionItem,GridInjectionsSettingsecom_admin/app/lib/settings-converter.ts— frontend/backend conversionecom_admin/app/components/settings/grid-injections-section.tsx— admin UIstorefront_search/src/ui-rendering.ts—spliceGridInjections()
Test Files
storefront_search/tests/ui-knobs/dom-events.test.ts— unit tests fordispatchMarqoEventstorefront_search/tests/dom-events-integration.test.ts— integration tests for CTA handler + all event payloadsstorefront_search/tests/grid-injections.test.ts— splice logic testsstorefront_search/tests/grid-injections-comprehensive.test.ts— edge cases, CSS, config, template wiringecom_admin/app/lib/__tests__/settings-converter.test.ts— converter round-trip tests