I’m using the free Shopify Horizon theme and have encountered an issue with the Spotlight section on the homepage. When I hover over the product card, the product link is not clickable, so users can’t open the product page. I’ve attached a screen recording for reference. Could anyone help me identify the cause and suggest a solution?
website: https://barcaan.in/
pw: fds1
@siva_fds ,
This is usually caused by a CSS or JavaScript overlay rather than the Spotlight section itself. A few things I’d check first:
Temporarily disable any recently installed apps especially quick view, animations, or popup apps to see if one is blocking the click.
Inspect the product card in your browsers Developer Tools and check if another element with a higher z-index is sitting on top of the product link.
Switch to an unmodified copy of the Horizon theme and test the Spotlight section there. If it works its likely a customization issue rather than a theme bug.
If the issue only happens on hover i’s almost always an overlapping element intercepting the click events.
I don’t know how to Inspect the product card in your browsers Developer Tools and check if another element with a higher z-index is sitting on top of the product link.
Hi @siva_fds
I checked the issue, but I don’t think it can be fixed with CSS alone. It looks like the JavaScript handling the hover/click behavior will also need to be updated. Could you please share the Collaborator Code so I can investigate further and implement the appropriate fix?
Moeed
July 20, 2026, 10:41am
6
Hey @siva_fds
Here’s a quick workaround:
Follow these Steps:
Go to Online Store
Edit Code
Find theme.liquid file
Add the following code in the bottom of the file above </ body> tag
<style>
@media screen and (min-width: 768px) {
.hotspot .hotspot-dialog[data-placement*=right] {
left: 50% !important;
}
.hotspot .hotspot-dialog[data-placement*=left] {
right: 50% !important;
}
}
</style>
RESULT:
Hope that helps! If it did, a Like and Marking it as Solution goes a long way and helps others find the fix faster too.
Best,
Moeed
One option is to update your theme version – they’ve added a fix for this in 4.1.3
This can be problematic though since they’ve also made transition from color schemes to color palettes, which can complicate things.
CSS fix suggested by Moeed works, but is not very stable.
Better fix would be to apply the same Javascript change.
You need to go to “Edit theme code”, find assets/product-hotspot.js and replace its contents from the newer version of this file:
import { Component } from '@theme/component';
import { QuickAddComponent } from '@theme/quick-add';
import { isClickedOutside, isMobileBreakpoint, isTouchDevice, mediaQueryLarge } from '@theme/utilities';
/**
* A custom element that manages a dialog.
*
* @typedef {object} Refs
* @property {HTMLDialogElement} dialog - The dialog element.
* @property {HTMLButtonElement} trigger - The button element.
* @property {HTMLAnchorElement} productLink - The product link element.
*
* @extends Component<Refs>
*/
export class ProductHotspotComponent extends Component {
requiredRefs = ['trigger', 'dialog'];
/** @type {(() => void) | null} */
#pointerenterHandler = null;
timer = /** @type {number | null} */ (null);
This file has been truncated. show original
import { Component } from '@theme/component';
import { QuickAddComponent } from '@theme/quick-add';
import { isClickedOutside, isMobileBreakpoint, isTouchDevice, mediaQueryLarge } from '@theme/utilities';
/**
* A custom element that manages a dialog.
*
* @typedef {object} Refs
* @property {HTMLDialogElement} dialog - The dialog element.
* @property {HTMLButtonElement} trigger - The button element.
* @property {HTMLAnchorElement} productLink - The product link element.
*
* @extends Component<Refs>
*/
export class ProductHotspotComponent extends Component {
requiredRefs = ['trigger', 'dialog'];
/** @type {(() => void) | null} */
#pointerenterHandler = null;
timer = /** @type {number | null} */ (null);
connectedCallback() {
super.connectedCallback();
// Set up initial event listeners based on current breakpoint
this.#handleBreakpointChange();
// Listen for breakpoint changes
mediaQueryLarge.addEventListener('change', this.#handleBreakpointChange);
}
disconnectedCallback() {
super.disconnectedCallback();
// Clean up listeners
this.#removeDesktopListeners();
mediaQueryLarge.removeEventListener('change', this.#handleBreakpointChange);
}
/**
* Open the quick-add modal
* @returns {void}
*/
#openQuickAddModal() {
const quickAddComponent = /** @type {QuickAddComponent | null} */ (this.querySelector('quick-add-component'));
if (!quickAddComponent) return;
quickAddComponent.handleClick(new MouseEvent('click', { bubbles: true, cancelable: true }));
}
/**
* Set up desktop event listeners (hover)
* @returns {void}
*/
#setupDesktopListeners() {
const { trigger, dialog } = this.refs;
/** @type {() => void} */
const pointerenterHandler = () => {
if (dialog.open) return;
this.timer = setTimeout(() => {
this.showDialog();
}, 120);
// Add pointerleave listener when entering trigger
trigger.addEventListener('pointerleave', this.#handlePointerLeave);
};
this.#pointerenterHandler = pointerenterHandler;
trigger.addEventListener('pointerenter', pointerenterHandler);
}
/**
* Remove desktop event listeners from trigger
* @returns {void}
*/
#removeDesktopListeners() {
const { trigger } = this.refs;
if (this.#pointerenterHandler) {
trigger.removeEventListener('pointerenter', this.#pointerenterHandler);
trigger.removeEventListener('pointerleave', this.#handlePointerLeave);
this.#pointerenterHandler = null;
}
// Clear any pending timer
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
/**
* Handle breakpoint changes
* @returns {void}
*/
#handleBreakpointChange = () => {
// Remove existing listeners
this.#removeDesktopListeners();
// Set up desktop hover listeners only (mobile uses on:click in template)
if (!isMobileBreakpoint()) {
this.#setupDesktopListeners();
}
};
/**
* Calculate the placement of the dialog.
* @returns {Promise<void> | undefined}
*/
#calculateDialogPlacement() {
const { trigger, dialog } = this.refs;
const hotspotsContainer = this.parentElement;
if (!hotspotsContainer) {
return;
}
// Spacing constants
const BUTTON_GAP = 10; // Gap between button and dialog
const CONTAINER_GAP = 10; // Gap from container edges
const TOTAL_GAP = BUTTON_GAP + CONTAINER_GAP;
// Get container bounds
const containerRect = hotspotsContainer?.getBoundingClientRect();
// Get button dimensions
const triggerRect = trigger.getBoundingClientRect();
// To get dialog dimensions, we need to temporarily show it invisibly
// Show dialog invisibly to measure it
dialog.style.visibility = 'hidden';
dialog.style.display = 'block';
dialog.style.transform = 'none';
dialog.removeAttribute('data-placement');
const { width: dialogWidth, height: dialogHeight } = dialog.getBoundingClientRect();
// Reset dialog state
dialog.style.removeProperty('display');
dialog.style.removeProperty('visibility');
dialog.style.removeProperty('transform');
// Calculate button position relative to container
const buttonLeft = triggerRect.left - containerRect.left;
const buttonRight = triggerRect.right - containerRect.left;
const buttonTop = triggerRect.top - containerRect.top;
const buttonBottom = triggerRect.bottom - containerRect.top;
// Calculate available space
const spaceRight = containerRect.width - buttonRight - CONTAINER_GAP;
const spaceLeft = buttonLeft - CONTAINER_GAP;
// Determine horizontal placement
let x = 'right';
if (spaceRight >= dialogWidth + BUTTON_GAP) {
x = 'right';
} else if (spaceLeft >= dialogWidth + BUTTON_GAP) {
x = 'left';
} else {
x = 'center';
}
// Determine vertical placement
let y = 'bottom';
let verticalOffset = 0;
if (x !== 'center') {
let dialogStartY = buttonTop; // Default to top-aligned
let dialogEndY = buttonTop + dialogHeight;
if (dialogEndY > containerRect.height - CONTAINER_GAP) {
// If top-aligned overflows bottom
dialogStartY = buttonBottom - dialogHeight;
dialogEndY = buttonBottom;
y = 'top';
if (dialogStartY < CONTAINER_GAP) {
// If bottom-aligned overflows top
verticalOffset = CONTAINER_GAP - dialogStartY;
} else if (dialogEndY > containerRect.height - CONTAINER_GAP) {
// If bottom-aligned overflows bottom
verticalOffset = -(dialogEndY - (containerRect.height - CONTAINER_GAP));
}
} else {
if (dialogStartY < CONTAINER_GAP) {
// If top-aligned overflows top
if (dialogStartY < CONTAINER_GAP) {
verticalOffset = CONTAINER_GAP - dialogStartY;
}
y = 'bottom';
}
}
} else {
// For center horizontal: position below or above button
if (containerRect.height - buttonBottom >= dialogHeight + TOTAL_GAP) {
y = 'bottom';
} else if (buttonTop >= dialogHeight + TOTAL_GAP) {
y = 'top';
} else {
// If neither fits well, choose based on button position
y = buttonTop < containerRect.height / 2 ? 'bottom' : 'top';
}
}
// Set placement data attribute
dialog.dataset.placement = `${x},${y}`;
// Apply vertical offset if needed to keep dialog in bounds
if (verticalOffset !== 0) {
dialog.style.setProperty('--dialog-vertical-offset', `${verticalOffset}px`);
} else {
dialog.style.removeProperty('--dialog-vertical-offset');
}
// Return a promise that resolves after a few ticks to ensure styles are applied
return new Promise((resolve) => setTimeout(resolve, 100));
}
/**
* Handle pointer leave.
* @param {PointerEvent} e - The event.
* @returns {void}
*/
#handlePointerLeave = (e) => {
const { dialog, trigger } = this.refs;
// Clear open timer if leaving trigger before dialog opens
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (!dialog.open) return;
const isLeavingTrigger = e.target === trigger;
const isLeavingDialog = e.target === dialog;
const isGoingToDialog = e.relatedTarget instanceof Element && dialog.contains(e.relatedTarget);
const isGoingToTrigger = e.relatedTarget === trigger;
if (isGoingToDialog || (isLeavingDialog && isGoingToTrigger)) return;
if (isLeavingTrigger || isLeavingDialog) this.closeDialog();
};
/**
* Get the product link for the hotspot product.
* @returns {HTMLAnchorElement | null} The product link or null.
*/
getHotspotProductLink() {
return this.refs.productLink || null;
}
/**
* Handle hotspot click - on mobile/touch devices opens quick-add, on desktop opens dialog
* @param {MouseEvent} e - The click event
* @returns {void}
*/
handleHotspotClick = (e) => {
// Check if it's a touch device (tablets) or mobile breakpoint
if (isMobileBreakpoint() || isTouchDevice()) {
e.preventDefault();
e.stopPropagation();
this.#openQuickAddModal();
} else {
this.showDialog();
}
};
showDialog = async () => {
const { dialog } = this.refs;
await this.#calculateDialogPlacement();
dialog.dataset.showing = 'true';
dialog.show();
document.body.addEventListener('click', this.lightDismissMouse);
document.body.addEventListener('keydown', this.lightDismissKeyboard);
document.body.addEventListener('keyup', this.lightDismissKeyboard);
// Add pointerleave listener to dialog when it opens
dialog.addEventListener('pointerleave', this.#handlePointerLeave);
};
/**
* Close the dialog.
* @returns {Promise<void>}
*/
closeDialog = async () => {
const { dialog, trigger } = this.refs;
dialog.dataset.closing = 'true';
dialog.close();
document.body.removeEventListener('click', this.lightDismissMouse);
document.body.removeEventListener('keydown', this.lightDismissKeyboard);
document.body.removeEventListener('keyup', this.lightDismissKeyboard);
// Remove pointerleave listeners when closing
dialog.removeEventListener('pointerleave', this.#handlePointerLeave);
trigger.removeEventListener('pointerleave', this.#handlePointerLeave);
// we need to use a data-attribute to keep transition-behavior working only when open
const animations = dialog.getAnimations({ subtree: true });
await Promise.allSettled(animations.map((a) => a.finished));
if (!dialog.open) {
delete dialog.dataset.showing;
delete dialog.dataset.closing;
delete dialog.dataset.placement;
}
};
/**
* Light dismiss the dialog.
* @param {MouseEvent} event - The event.
* @returns {void}
*/
lightDismissMouse = (event) => {
const { dialog } = this.refs;
if (isClickedOutside(event, dialog)) {
this.closeDialog();
}
};
/**
* Light dismiss the dialog.
* @param {KeyboardEvent} event - The event.
* @returns {void}
*/
lightDismissKeyboard = (event) => {
const { dialog } = this.refs;
if (
(event.type === 'keydown' && event.key === 'Escape') ||
(event.type === 'keyup' && !dialog.matches(':is(:focus, :focus-visible, :focus-within)'))
) {
this.closeDialog();
}
};
}
// Register custom element
customElements.define('product-hotspot-component', ProductHotspotComponent);
Replace product-hotspot.js code with the above
{% liquid
assign hotspot_product = closest.product
assign placeholder_product_title = 'placeholders.product_title' | t
assign hotspot_product_title = hotspot_product.title | default: placeholder_product_title
%}
<product-hotspot-component
class="hotspot{% if hotspot_product == blank and request.design_mode == false %} hotspot--hidden-touch hidden--mobile{% endif %}"
style="
left: calc({{ block.settings.x-position }}% - var(--button-size) / 2);
top: calc({{ block.settings.y-position }}% - var(--button-size) / 2);
--hotspot-bg: {{ section.settings.hotspot_color }};
--hotspot-bullseye: {{ section.settings.bullseye_color }};
"
data-product-url="{{ block.settings.product.url }}"
data-product-title="{{ block.settings.product.title | escape }}"
data-id="{{ block.id }}"
{{ block.shopify_attributes }}
>
<button
class="hotspot-trigger"
aria-label="{{ 'accessibility.show_product_details' | t: product_name: hotspot_product_title }}"
ref="trigger"
on:click="/handleHotspotClick"
style="anchor-name: --hotspot-trigger-{{ block.id }};"
></button>
<dialog
aria-label="{{ 'accessibility.product_details' | t }}"
class="hotspot-dialog"
ref="dialog"
>
{% if hotspot_product != blank %}
<a
href="{{ hotspot_product.url }}"
class="hotspot-dialog__link"
aria-label="{{ hotspot_product.title | escape }}"
ref="productLink"
>
</a>
{% endif %}
<div class="hotspot-dialog__product">
{% if hotspot_product.featured_image != blank %}
{% render 'image', image: hotspot_product.featured_image, class: 'hotspot-dialog__product-image' %}
{% else %}
{{ 'product-apparel-1' | placeholder_svg_tag: 'hotspot-dialog__placeholder-product-image' }}
{% endif %}
<div class="hotspot-dialog__product-content">
<div class="hotspot-dialog__product-info">
<h2
class="hotspot-dialog__product-title {{ section.settings.product_title_preset | default: 'h5' }}"
>
{{ hotspot_product_title }}
</h2>
<div
class="hotspot-dialog__product-price {{ section.settings.product_price_preset | default: 'h6' }}"
>
{% render 'price', product_resource: hotspot_product %}
</div>
</div>
{% if hotspot_product != blank %}
{% if hotspot_product.available %}
{% render 'quick-add', product: hotspot_product, section_id: section.id, block: block %}
{% else %}
<div class="hotspot-dialog__sold-out-badge">
{{ 'content.product_badge_sold_out' | t }}
</div>
{% endif %}
{% endif %}
</div>
</div>
</dialog>
</product-hotspot-component>
{% schema %}
{
"name": "t:names.hotspot_product",
"tag": null,
"settings": [
{
"type": "product",
"id": "product",
"label": "t:settings.product"
},
{
"type": "range",
"id": "x-position",
"label": "t:settings.x_position",
"min": 0,
"max": 100,
"step": 1,
"default": 50
},
{
"type": "range",
"id": "y-position",
"label": "t:settings.y_position",
"min": 0,
"max": 100,
"step": 1,
"default": 50
}
],
"presets": [
{
"name": "t:names.hotspot_product"
}
]
}
{% endschema %}
Replace _hotspot-product.lilquid code with the above
import { Component } from '@theme/component';
import { morph } from '@theme/morph';
import { DialogComponent, DialogCloseEvent } from '@theme/dialog';
import { mediaQueryLarge, isMobileBreakpoint, getIOSVersion } from '@theme/utilities';
import VariantPicker from '@theme/variant-picker';
import { StandardEvents, ProductSelectEvent, CartLinesUpdateEvent } from '@shopify/events';
export class QuickAddComponent extends Component {
/** @type {AbortController | null} */
#abortController = null;
/** @type {Map<string, Element>} */
#cachedContent = new Map();
/** @type {AbortController} */
#cartUpdateAbortController = new AbortController();
get productPageUrl() {
const productCard = /** @type {import('./product-card').ProductCard | null} */ (this.closest('product-card'));
if (productCard) return productCard.productPageUrl;
const hotspotProduct = /** @type {import('./product-hotspot').ProductHotspotComponent | null} */ (
this.closest('product-hotspot-component')
);
const productLink = hotspotProduct?.getHotspotProductLink();
return productLink?.href || '';
}
/**
* Gets the currently selected variant ID from the product card
* @returns {string | null} The variant ID or null
*/
#getSelectedVariantId() {
const productCard = /** @type {import('./product-card').ProductCard | null} */ (this.closest('product-card'));
return productCard?.getSelectedVariantId() ?? null;
}
connectedCallback() {
super.connectedCallback();
mediaQueryLarge.addEventListener('change', this.#closeQuickAddModal);
document.addEventListener(StandardEvents.cartLinesUpdate, this.#handleCartUpdate, {
signal: this.#cartUpdateAbortController.signal,
});
document.addEventListener(StandardEvents.productSelect, this.#handleProductSelectUpdate);
}
disconnectedCallback() {
super.disconnectedCallback();
mediaQueryLarge.removeEventListener('change', this.#closeQuickAddModal);
this.#abortController?.abort();
this.#cartUpdateAbortController.abort();
document.removeEventListener(StandardEvents.productSelect, this.#handleProductSelectUpdate);
}
/**
* Updates quick-add button state when product variant is selected
* @param {ProductSelectEvent} event - The product select event
*/
#handleProductSelectUpdate = (event) => {
if (!(event.target instanceof HTMLElement)) return;
if (event.target.closest('product-card') !== this.closest('product-card')) return;
if (this.dataset.usesSellingPlans === 'true') return;
// Only flip choose <-> add when both buttons were rendered.
// Otherwise the flip would hide the sole rendered button and reveal nothing.
if (this.dataset.rendersBothButtons !== 'true') return;
const productOptionsCount = this.dataset.productOptionsCount;
const quickAddButton = productOptionsCount === '1' ? 'add' : 'choose';
this.setAttribute('data-quick-add-button', quickAddButton);
};
/**
* Clears the cached content when cart is updated
*/
#handleCartUpdate = () => {
this.#cachedContent.clear();
};
/**
* Re-renders the variant picker in the quick-add modal.
* @param {Element} newHtml - The element to re-render.
*/
#updateVariantPicker(newHtml) {
const modalContent = document.getElementById('quick-add-modal-content');
if (!modalContent) return;
const variantPicker = /** @type {VariantPicker | null} */ (modalContent.querySelector('variant-picker'));
if (!variantPicker) return;
variantPicker.updateVariantPicker(newHtml);
}
/**
* Handles quick add button click
* @param {Event} event - The click event
*/
handleClick = async (event) => {
event.preventDefault();
const currentUrl = this.productPageUrl;
if (this.dataset.usesSellingPlans === 'true') {
if (currentUrl) window.location.href = currentUrl;
return;
}
// Check if we have cached content for this URL
let productGrid = this.#cachedContent.get(currentUrl);
if (!productGrid) {
// Fetch and cache the content
const html = await this.fetchProductPage(currentUrl);
if (html) {
const gridElement = html.querySelector('[data-product-grid-content]');
if (gridElement) {
// Cache the cloned element to avoid modifying the original
productGrid = /** @type {Element} */ (gridElement.cloneNode(true));
this.#cachedContent.set(currentUrl, productGrid);
}
}
}
if (productGrid) {
// Use a fresh clone from the cache
const freshContent = /** @type {Element} */ (productGrid.cloneNode(true));
await this.updateQuickAddModal(freshContent);
this.#updateVariantPicker(productGrid);
}
this.#openQuickAddModal();
};
#resetScroll() {
const dialogComponent = document.getElementById('quick-add-dialog');
if (!(dialogComponent instanceof QuickAddDialog)) return;
const productDetails = dialogComponent.querySelector('.product-details');
const productMedia = dialogComponent.querySelector('.product-information__media');
productDetails?.scrollTo({ top: 0, behavior: 'instant' });
productMedia?.scrollTo({ top: 0, behavior: 'instant' });
}
/** @param {QuickAddDialog} dialogComponent */
#stayVisibleUntilDialogCloses(dialogComponent) {
this.toggleAttribute('stay-visible', true);
dialogComponent.addEventListener(DialogCloseEvent.eventName, () => this.toggleAttribute('stay-visible', false), {
once: true,
});
}
#openQuickAddModal = () => {
const dialogComponent = document.getElementById('quick-add-dialog');
if (!(dialogComponent instanceof QuickAddDialog)) return;
this.#stayVisibleUntilDialogCloses(dialogComponent);
dialogComponent.showDialog();
// is nondeterministic when the open attribute is set on the dialog element after .showDialog() is called.
// Waiting until the open animation starts seemed to be the most reliable metric here.
const dialog = dialogComponent.refs?.dialog;
if (!dialog) return;
dialog.addEventListener('animationstart', this.#resetScroll.bind(this), { once: true });
};
#closeQuickAddModal = () => {
const dialogComponent = document.getElementById('quick-add-dialog');
if (!(dialogComponent instanceof QuickAddDialog)) return;
dialogComponent.closeDialog();
};
/**
* Fetches the product page content
* @param {string} productPageUrl - The URL of the product page to fetch
* @returns {Promise<Document | null>}
*/
async fetchProductPage(productPageUrl) {
if (!productPageUrl) return null;
// We use this to abort the previous fetch request if it's still pending.
this.#abortController?.abort();
this.#abortController = new AbortController();
try {
const response = await fetch(productPageUrl, {
signal: this.#abortController.signal,
});
if (!response.ok) {
throw new Error(`Failed to fetch product page: HTTP error ${response.status}`);
}
const responseText = await response.text();
const html = new DOMParser().parseFromString(responseText, 'text/html');
return html;
} catch (error) {
if (error.name === 'AbortError') {
return null;
} else {
throw error;
}
} finally {
this.#abortController = null;
}
}
/**
* Re-renders the variant picker.
* @param {Element} productGrid - The product grid element
*/
async updateQuickAddModal(productGrid) {
const modalContent = document.getElementById('quick-add-modal-content');
if (!productGrid || !modalContent) return;
if (isMobileBreakpoint()) {
const productDetails = productGrid.querySelector('.product-details');
const productFormComponent = productGrid.querySelector('product-form-component');
const variantPicker = productGrid.querySelector('variant-picker');
const productPrice = productGrid.querySelector('product-price');
const productTitle = document.createElement('a');
productTitle.textContent = this.dataset.productTitle || '';
// Make product title as a link to the product page
productTitle.href = this.productPageUrl;
const productHeader = document.createElement('div');
productHeader.classList.add('product-header');
productHeader.appendChild(productTitle);
if (productPrice) {
productHeader.appendChild(productPrice);
}
productGrid.appendChild(productHeader);
if (variantPicker) {
productGrid.appendChild(variantPicker);
}
if (productFormComponent) {
productGrid.appendChild(productFormComponent);
}
productDetails?.remove();
}
// Sync the view-event-payload attribute and morph children into the modal's product-component
const payload = productGrid.getAttribute('view-event-payload') || '';
modalContent.setAttribute('view-event-payload', payload);
morph(modalContent, productGrid);
this.#syncVariantSelection(modalContent);
}
/**
* Syncs the variant selection from the product card to the modal
* @param {Element} modalContent - The modal content element
*/
#syncVariantSelection(modalContent) {
const selectedVariantId = this.#getSelectedVariantId();
if (!selectedVariantId) return;
// Find and check the corresponding input in the modal
const modalInputs = modalContent.querySelectorAll('input[type="radio"][data-variant-id]');
for (const input of modalInputs) {
if (input instanceof HTMLInputElement && input.dataset.variantId === selectedVariantId && !input.checked) {
input.checked = true;
input.dispatchEvent(new Event('change', { bubbles: true }));
break;
}
}
}
}
if (!customElements.get('quick-add-component')) {
customElements.define('quick-add-component', QuickAddComponent);
}
class QuickAddDialog extends DialogComponent {
#abortController = new AbortController();
connectedCallback() {
super.connectedCallback();
this.addEventListener(StandardEvents.cartLinesUpdate, this.handleCartUpdate, {
signal: this.#abortController.signal,
});
this.addEventListener(StandardEvents.productSelect, this.#handleProductSelect);
this.addEventListener(DialogCloseEvent.eventName, this.#handleDialogClose);
}
disconnectedCallback() {
super.disconnectedCallback();
this.#abortController.abort();
this.removeEventListener(DialogCloseEvent.eventName, this.#handleDialogClose);
}
/**
* Closes the dialog on successful cart update
* @param {CartLinesUpdateEvent} event - The cart lines update event
*/
handleCartUpdate = (event) => {
event.promise
?.then(({ detail }) => {
if (detail?.didError) return;
this.closeDialog();
})
.catch((error) => {
if (error?.name !== 'AbortError') console.warn('[quick-add] Event promise rejected:', error);
});
};
/** @param {ProductSelectEvent} event - The product select event */
#handleProductSelect = (event) => {
// Wait for variant update data
event.promise
.then(({ detail }) => {
if (!detail?.html) return;
const { html } = detail;
const anchorElement = /** @type {HTMLAnchorElement} */ (html.querySelector('.view-product-title a'));
const viewMoreDetailsLink = /** @type {HTMLAnchorElement} */ (this.querySelector('.view-product-title a'));
const mobileProductTitle = /** @type {HTMLAnchorElement} */ (this.querySelector('.product-header a'));
if (!anchorElement) return;
if (viewMoreDetailsLink) viewMoreDetailsLink.href = anchorElement.href;
if (mobileProductTitle) mobileProductTitle.href = anchorElement.href;
})
.catch((error) => {
if (error?.name !== 'AbortError') console.warn('[quick-add] Event promise rejected:', error);
});
};
#handleDialogClose = () => {
const iosVersion = getIOSVersion();
/**
* This is a patch to solve an issue with the UI freezing when the dialog is closed.
* To reproduce it, use iOS 16.0.
*/
if (!iosVersion || iosVersion.major >= 17 || (iosVersion.major === 16 && iosVersion.minor >= 4)) return;
requestAnimationFrame(() => {
/** @type {HTMLElement | null} */
const grid = document.querySelector('#ResultsList [product-grid-view]');
if (grid) {
const currentWidth = grid.getBoundingClientRect().width;
grid.style.width = `${currentWidth - 1}px`;
requestAnimationFrame(() => {
grid.style.width = '';
});
}
});
};
}
if (!customElements.get('quick-add-dialog')) {
customElements.define('quick-add-dialog', QuickAddDialog);
}
Replace quick-add.js code with the above
Your site’s source code says that Your theme is not Horizon. It is Atelier 3.4.0 . But It is built on Horizon. This is a bug in both the themes.
I tested the fix it on horizon theme in my store. I also injected the given javascript into your site from the frontend, the fix works
Move onto the card and it is gone:
after applying my Fix
Steps
In the theme editor, under Template, click Add section.
Choose custom liquid
Paste the snippet given below into Liquid code, then Save.
<script>
document.addEventListener('pointerleave', function (event) {
var dialog = event.target;
if (!(dialog instanceof HTMLDialogElement)) return;
if (!dialog.classList.contains('hotspot-dialog')) return;
// moving onto the card's own child is not "leaving the card"
if (event.relatedTarget instanceof Node && dialog.contains(event.relatedTarget)) {
event.stopImmediatePropagation();
}
}, true);
</script>
Hey @siva_fds
Hope you’re doing well!
This often cause by a custom code change or an element over lapping the product card and blocking clicks. Try testing the Spotlight section without custom code or third-party apps and inspect the element to see if another layer is covering the product link.