ProductGridItem renders the grid-view product card used in listing, category, and search result pages.Components/FilterList/Default.liquidComponents/LastVisitedProducts/Default.liquidComponents/ProductComparison/Default.liquidComponents/ProductsBlock/Default.liquidservicesreusabledefault for cart/quantity-related operations (see Reusables/ProductGridItem/Default.js).servicesreusabledefault.hydrateProductPrices(products)servicesreusabledefault.hydrateProductConstraints(product)servicesreusabledefault._applyDefaultConstraints(product)hydrateProductConstraints fails, keeping the card orderable with permissive defaults (min: 1, step: 1, max: 999999).Components/RelatedProducts/Default.liquidComponents/Wishlist/Default.liquidComponents/FilterList/Default.liquid:{% render 'Reusables\\ProductGridItem\\Default',
product: product,
modelId: id,
colorExists: colorExists,
listName: listName,
position: forloop.index0,
description: descriptionField
%}productmodelIdmodelId-product.id) for DOM scoping.colorExistslistNamepositionproductImage from product.mediaItems[0] when available./product/{{ product.alias }}x-data='productgriditemreusabledefault.initComponent({{ product | serialize | escape }}, "{{ Reusables.ProductGridItem.Translations.AddToCartErrorMessage }}", "{{ Reusables.ProductGridItem.Translations.UpdateProductQuantityErrorMessage }}")'$store.productModal (for variant selection flows when needed).Reusables/ProductComparisonButtonReusables/WishlistButtonReusables/ShoppingListsButtonGlobalData.Settings.ShowProductComparisonGlobalData.Settings.EnableWishlistGlobalData.Settings.EnableShoppingListsproduct.brand.* exists, the card renders a brand badge (image when available, otherwise text).product.categoryName / product.categoryLink (when present).product.title and links to /product/{{ product.alias }}.product.isMpnVisible and/or product.isSkuVisible.product.productVariants.size > 1.product.dimension1 / product.dimension2 (and their .type, .items).GlobalData.Settings.noImage.link or /Assets/images/no-image.svg.{
"id": "Product Id",
"title": "Sample title",
"alias": "sample",
"categoryName": "Sample text",
"categoryLink": "/category/sample",
"isMpnVisible": true,
"isSkuVisible": true,
"mpn": "Sample text",
"sku": "Sample text",
"mediaItems": [
{
"id": "Media Id",
"link": "https://example.com/sample",
"position": 0,
"alt": "Sample text",
"mediaType": "Image"
},
...
],
"icoTags": [
{
"id": "Tag Id",
"name": "Sample text",
"backgroundColor": "Sample text",
"textColor": "Sample text",
"icon": { "link": "https://example.com/sample" }
},
...
],
"brand": {
"name": "Sample text",
"link": "/brand/sample",
"image": { "link": "https://example.com/sample", "alt": "Sample text" }
},
"dimension1": {
"type": "Color",
"name": "Sample text",
"items": [ { "id": "Dim Item Id", "value": "Sample text", "textColor": "Sample text" }, ... ]
},
"dimension2": {
"type": "Size",
"name": "Sample text",
"items": [ { "id": "Dim Item Id", "value": "Sample text" }, ... ]
},
"productVariants": [
{
"id": "Variant Id",
"dimension1ItemId": "Dim Item Id",
"dimension2ItemId": "Dim Item Id",
"finalPrice": 25,
"finalPriceText": "25,00 €",
"originalPrice": 35,
"originalPriceText": "35,00 €",
"canOrder": true,
"sellOutOfStock": false,
"quantity": 2,
"minOrderQuantity": 1,
"orderQuantityStep": 1
},
...
],
"startPrice": 25,
"startPriceText": "25,00 €",
"hasPriceRange": false,
"inWishlist": false
}Default.liquid):product.id, product.title, product.aliasproduct.categoryName, product.categoryLinkproduct.mediaItems[0].link, product.mediaItems[0].altproduct.icoTags[] (label rendering)product.brand.name, product.brand.link, product.brand.image.link, product.brand.image.altproduct.isMpnVisible, product.mpnproduct.isSkuVisible, product.skuproduct.productVariants (size, iteration, JS state)product.dimension1, product.dimension2 and nested .type, .name, .items[]productgriditemreusabledefaultx-data="productgriditemreusabledefault.initComponent(product, addToCartErrorMessage, updateProductQuantityErrorMessage)"ResizeObserver until it gets a width, so Swiper does not measure zero and leave dead navigation. Returns null in that case.initSwiper that builds the Swiper (slidesPerView: "auto", spaceBetween: 8, prev/next navigation from refs). Called immediately, or by the observer once the container is laid out.sendSelectEvent(...) and sendSelectEventAndRedirect(...).{{ product | serialize | escape }}). Keep the product payload reasonably sized.servicesreusabledefault.hydrateProductPrices(this.product), since the server renders the card from a common/cached price. Clears pricePending in a finally block once the call settles (success or failure), and logs a console error on failure.runPriceHydration() immediately, unless hydrateOnViewport is enabled, in which case it defers the call until the card scrolls into view (elementInView(this.$el, ..., { rootMargin: "200px", threshold: 0, once: true })). No-ops if a price hydration isn't pending. Viewport mode suits unpaginated surfaces (carousels, blocks) where many cards are off-screen.servicesreusabledefault.hydrateProductConstraints(this.product), since the server renders the card from a cached, personalization-free view model.servicesreusabledefault._applyDefaultConstraints(this.product) to keep the card orderable with permissive defaults; the server still revalidates on the actual add-to-cart call.init()/updateSelectedVariant() may have already set a fallback of 1 before hydration resolved.runConstraintsHydration() immediately, unless hydrateOnViewport is enabled, in which case it defers the call until the card scrolls into view (same elementInView pattern as schedulePriceHydration).true when variants are picked inline on the card: variantDisplay is "Inline", the viewport is at least 1024px wide, the product has multiple variants and no group children.true for multi-variant products without group children that do not use inline variants, so a variant is always reachable through the modal at any viewport.usePopupVariants is false.usePopupVariants is true.servicesreusabledefault.addToCart(...) and updates quantity constraints from the response.dimension1 or dimension2) and sets colorDimKey / otherDimKey.updateSelectedVariant().handleClickEvents().isProductSection is true, also initializes sticky/scroll helpers for product single sections.window.innerWidth < 1440 to determine mobile behavior..product-card__title-cat height and sets the CSS variable --heightTitleCat.selectItem for the clicked product.selectItem and then navigates to /product/${product.alias}.mousedown handler to the card.[data-product-list-double] or [data-product-list])selectItemselectedDimColor* and the corresponding selectedDim1* / selectedDim2*).updateSelectedVariant().selectedDimOther* and the corresponding selectedDim1* / selectedDim2*).updateSelectedVariant().selectedDim1* / selectedDim2*.updateSelectedVariant().selectedVariant and selectedVariantIndex.updateQuantity(...).getActiveImage(...).quantityConstraints.additive.minimum) as the default quantity when a variant is selected.null when not enough dimensions are selected.product.productVariants.length.true.isAdding) and checks canAddToCart().servicesreusabledefault.addToCart(productId, variantId, quantity, finalPrice, originalPrice).$store.toast with addToCartErrorMessage.variant.quantityConstraintsupdateQuantity(...)true when (product.productVariants || []).length > 1.true when the product is a group master that spans a price range (product.hasGroupChildren and product.startPriceText are both set).startPriceText is not treated as a range; displayFinalPrice returns "" for it.displayStartPrice: true when isGroupPriceRange is set, or when the product has multiple variants, dimensions are not fully selected, and product.hasPriceRange is set.displayEndPrice: returns product.endPriceText when isGroupPriceRange is set, otherwise "". This is the upper bound of the "from X to Y" range shown on group-master cards.displayFinalPrice: returns the selected variant price text, or a fallback price text.displayOriginalPrice: returns the selected variant original price text, or a fallback.quantityConstraints.additive.isValid.true if any variant is orderable.true only when selectedVariant.quantityConstraints.additive.isValid.customColorActive when stockAvailability.label === 'Custom' and a color exists.'-' when displayNullLine is true and the value is missing.variantOrDefault('stockAvailability.label').isProductSection === true.activeButton.activeButton.quantity using quantityConstraints.additive.step and min/max.updateQuantity(...) per product id.checkQuantity is true, calls servicesreusabledefault.updateProductQuantity(...) to validate/normalize quantity server-side.checkQuantity is false, it updates this.quantity directly.$store.toast for error feedback when the API call fails.servicesreusabledefault.hydrateProductStock(product); with hydrateOnViewport it waits until the card is near the viewport (scheduleStockHydration / runStockHydration).stockPending — true until stock has loaded. The template shows a fixed-height placeholder meanwhile, so the card does not shift.currentStock() — the selected variant, else the only variant, else the product-level stockAvailability. Grouped parents (hasGroupChildren) never load stock and show nothing.stockText(availableText, outOfStockText) — Available / OutOfStock use the translations, Varies (multi-variant product, no selection) shows -, other labels show the merchant message. Built on the component-local stockTextFor and formatStockQuantity helpers.stockQuantityText() — the formatted stockQuantity of the selected variant (or the only variant), or "" when no quantity is available.{{ model.Translations.StockQuantity }}: …) render only after loading. The server-rendered stock is cleared before the request, so if loading fails neither the availability label nor the quantity is shown.$store.toast (used by related actions like add-to-cart errors)servicesreusabledefault for cart/quantity-related operations (see Reusables/ProductGridItem/Default.js).Reusables/ProductGridItem/Default.liquidReusables/ProductGridItem/Default.jsReusables/ProductGridItem/Default.jsonReusables/ProductComparisonButton/DefaultReusables/WishlistButton/DefaultReusables/ShoppingListsButton/Default/product/{{ product.alias }} on click and on Enter/Space when focused.product.mediaItems[0] is an image; in real data, the first item should be an Image mediaType for best results.