Blog Ecommerce 8 min read

Shopify Product Image Placeholder for Missing Catalog Photos

Add a shopify product image placeholder using Liquid conditionals, the fallback.pics API, and proper aspect-ratio containers to prevent layout shift in your theme.

Shopify product image placeholderShopify LiquidProduct catalogEcommerce imagesPlaceholder API
Shopify Product Image Placeholder for Missing Catalog Photos

Shopify themes display a broken icon or collapse the image container when a product has no photos. A proper shopify product image placeholder fills that space with a correctly-sized, labeled image so the catalog grid stays visually consistent during photo uploads or for products without images.

This guide covers the Liquid conditional approach, replacing Shopify's built-in placeholder SVG with a URL-based placeholder, and handling the aspect-ratio requirements that prevent layout shift in modern theme sections.

Default behavior

What Shopify renders for products without images

Shopify provides a placeholder_svg_tag helper that outputs a generic gray SVG placeholder when a product has no featured image. This works but it has no product context—all placeholder slots look identical regardless of which product they represent. In a catalog with hundreds of partially-uploaded products, that makes QA difficult.

The built-in placeholder also has a fixed SVG shape that may not match your theme's aspect ratio requirements. If your product cards use 1:1 square images but the placeholder SVG has a different ratio, the card layout jumps when the real image loads.

An external URL-based placeholder lets you specify exact dimensions, include a product name label, and match your brand colors. All of this is achievable through the fallback.pics API without any app installs or admin changes.

Liquid conditional

The Shopify Liquid product image fallback pattern

Check product.featured_image in Liquid. If it is truthy, use the standard img_url filter to size it. If it is nil, build a fallback URL using the product title as the text parameter. The product.title filter url_encode handles spaces and special characters.

Set explicit width and height attributes on both the real image and the placeholder. This is important for Core Web Vitals—the browser needs to know the image dimensions before the image loads to reserve the right space and avoid CLS.

Implementation text
{% comment %} product-card.liquid {% endcomment %}
{% if product.featured_image %}
  <img
    src="{{ product.featured_image | img_url: '400x400', crop: 'center' }}"
    alt="{{ product.featured_image.alt | escape }}"
    width="400"
    height="400"
    loading="lazy"
  />
{% else %}
  <img
    src="https://fallback.pics/api/v1/400x400/F3F4F6/9CA3AF?text={{ product.title | url_encode }}"
    alt="{{ product.title | escape }}"
    width="400"
    height="400"
    loading="lazy"
  />
{% endif %}

Aspect ratio

Preserving aspect ratio to prevent CLS

The most common source of layout shift in Shopify product grids is images with different intrinsic sizes. Some products have portrait photos, some landscape, some square. Without an aspect-ratio container, the grid reflows each time a new image loads at a different size.

Wrap images in a container with a fixed aspect ratio using CSS. The image fills the container with object-fit: cover, and the placeholder URL matches the container dimensions exactly. This makes the grid stable regardless of what images are loaded or missing.

Implementation text
{% comment %} Use consistent aspect ratio container {% endcomment %}
<div class="product-image" style="aspect-ratio: 1/1; overflow: hidden;">
  {% if product.featured_image %}
    <img
      src="{{ product.featured_image | img_url: '400x400', crop: 'center' }}"
      alt="{{ product.featured_image.alt | escape }}"
      width="400" height="400"
      style="width:100%; height:100%; object-fit:cover;"
      loading="lazy"
    />
  {% else %}
    <img
      src="https://fallback.pics/api/v1/400x400/F3F4F6/9CA3AF?text={{ product.title | url_encode }}"
      alt="{{ product.title | escape }}"
      width="400" height="400"
      style="width:100%; height:100%; object-fit:cover;"
      loading="lazy"
    />
  {% endif %}
</div>

Product page

Handling the main product page hero image

On product detail pages, the main hero image is typically above the fold and should not be lazy loaded. Use loading="eager" and consider adding a fetchpriority="high" attribute so the browser prioritizes it in the resource queue. The fallback URL should be larger here—1000x1000 or your theme's standard product image size.

The product page often shows multiple variant images. If a variant has no image, product.selected_or_first_available_variant.image will be nil. Check this separately from the main product.featured_image so variant-specific fallbacks match variant context.

Implementation text
{% assign hero_img = product.selected_or_first_available_variant.image
                        | default: product.featured_image %}

{% if hero_img %}
  <img
    src="{{ hero_img | img_url: '1000x1000', crop: 'center' }}"
    alt="{{ hero_img.alt | escape }}"
    width="1000" height="1000"
    loading="eager"
    fetchpriority="high"
  />
{% else %}
  <img
    src="https://fallback.pics/api/v1/1000x1000/F3F4F6/6B7280?text={{ product.title | url_encode }}"
    alt="{{ product.title | escape }}"
    width="1000" height="1000"
    loading="eager"
    fetchpriority="high"
  />
{% endif %}

Key takeaways

What to standardize before shipping

  • Check product.featured_image in Liquid before rendering; build a URL-based placeholder in the else branch.
  • Set explicit width and height attributes on placeholder images to prevent CLS in the product grid.
  • Wrap images in an aspect-ratio container with object-fit: cover for a consistent grid layout.
  • Use loading="eager" and fetchpriority="high" on the above-the-fold hero image, not on thumbnails.
  • Match placeholder URL dimensions to the image slot exactly so real images and placeholders occupy the same space.

Production fallback layer

Use fallback.pics anywhere an image URL is accepted.

Start with one deterministic URL and standardize fallback behavior across your design system.

Read the docs