Adding a Quick View Modal | Collections Page Add to Cart


Enhancing the shopping experience is at the heart of successful e-commerce design, and a quick view popup modal is a perfect way to achieve this. By letting customers preview product details and select variants without leaving the collection page, you streamline their journey and keep them engaged. In this blog, we’ll walk you through creating a quick view popup modal for your Shopify store. Utilizing the power of the Section Rendering API, we’ll enable functionality to dynamically load product details, allow variant selection, and provide a seamless “Add to Cart” experience—all within a sleek modal interface.

Code: https://github.com/ndrishinski/blogs/commit/e88123544d2c3eb5effb0d906adb74f0c43e4f1b#diff-f9fa50b02edba7e863550fca1ec8b944f2259506be43ae9730f130da7b066871

Prefer video?

Enjoy! Otherwise read on.

Adding Custom Button to Open Quick View Modal

The first step we’ll take is to add a button which will open our modal. When I added this button I had some issues regarding the z-index of the button, which is to say the button wasn’t appearing over the product card. Hopefully with these styles in place, you won’t face that issue. In the Dawn theme, we’ll open up ‘card-product.liquid’ in the snippets directory. (If you are not using Dawn, then you’ll want to find the snippet where your collections sections are referencing.) First we’ll add these styles to the top of the file:

{% style %}
  #modal-container {
      position: fixed; /* Change to fixed to cover the viewport */
      top: 0; /* Align to the top */
      left: 0; /* Align to the left */
      width: 100vw; /* Full width of the viewport */
      height: 100vh; /* Full height of the viewport */
      background-color: rgba(0, 0, 0, 0.5); /* Optional: semi-transparent background */ */
      z-index: 1000; /* Ensure it appears above other content */ */
      display: none;
  }
  .collection-add-to-cart {
    position: absolute;
    height: 40px;
    width: 40px;
    background: #1d438a;
    border-radius: 50%;
    top: 60%;
    right: 18px;
    z-index: 99;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
  }

  .collection-add-to-cart:hover {
    background: #778eb8;
  }
  .material-symbols-outlined {
    color: white;
    font-variation-settings:
    'FILL' 0,
    'wght' 400,
    'GRAD' 0,
    'opsz' 24
  }
{% endstyle %}

Then we’ll move down the file and insert the actual icon. Approximately line 610 we’ll add this code below the closing </div> tags:

    </div>
  </div>
<!-- insert here -->
  <div class="collection-add-to-cart" onclick="showModal('{{ card_product.handle }}')">
    <span class="material-symbols-outlined">
      <span class="material-symbols-outlined"> add_shopping_cart </span>
    </span>
  </div>
<!-- insert above here -->
{%- else -%}
  {%- liquid
    assign ratio = 1

Now we need to import the library for this icon from Google Icons. Open up ‘theme.liquid’ and paste the following line in the <head> section:

<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />

Now if we save this code, we should see our button appearing on the collection cards:

Create our Quick View Modal

Now that we have our button in place, we will first create the popup modal. To do this, create a file under the ‘sections’ directory and call it ‘product-popup.liquid’. Then we can paste in this code:

<style>
/* Modal styles */
.product-popup-modal-overlay {
    position: fixed;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
}
.modal-container-collection .product-popup-modal {
    background-color: white;
    border-radius: 0.5rem;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
    max-width: 64rem;
    width: 100%;
    max-height: 90vh;
    overflow-y: auto;
  z-index: 9999999999999999999999;
}
.modal-container-collection .product-popup-modal-content {
    display: grid;
    gap: 1.5rem;
    padding: 1.5rem;
}
@media (min-width: 768px) {
    .modal-container-collection .product-popup-modal-content {
        grid-template-columns: repeat(2, 1fr);
    }
}
/* Product images */
.modal-container-collection .product-popup-product-images {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}
.modal-container-collection .product-popup-main-image {
    position: relative;
    aspect-ratio: 1;
    background-color: #f3f4f6;
    border-radius: 0.5rem;
    overflow: hidden;
}
.modal-container-collection .product-popup-main-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.modal-container-collection .product-popup-image-badge {
    position: absolute;
    top: 0.5rem;
    left: 0.5rem;
    background-color: white;
    padding: 0.25rem 0.5rem;
    border-radius: 0.25rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: #3b82f6;
}
.modal-container-collection .product-popup-image-thumbnails {
    display: flex;
    gap: 1rem;
    justify-content: center;
    align-items: center;
}
.modal-container-collection .product-popup-thumbnail {
    width: 5rem;
    height: 5rem;
    border-radius: 0.375rem;
    background-color: #f3f4f6;
    cursor: pointer;
    transition: ring 0.3s ease;
}
.modal-container-collection .product-popup-thumbnail:hover {
    box-shadow: 0 0 0 2px #3b82f6;
}
.modal-container-collection .product-popup-thumbnail img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 0.375rem;
}
/* Product details */
.modal-container-collection .product-popup-product-details {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}
.modal-container-collection .product-popup-product-title {
    font-size: 1.875rem;
    font-weight: 700;
    color: #111827;
}
.modal-container-collection .product-popup-product-subtitle {
    font-size: 1.125rem;
    color: #6b7280;
}
.modal-container-collection .product-popup-rating {
    display: flex;
    align-items: center;
}
.modal-container-collection .product-popup-stars {
    display: flex;
    color: #fbbf24;
}
.modal-container-collection .product-popup-review-count {
    margin-left: 0.5rem;
    color: #6b7280;
}
.modal-container-collection .product-popup-price {
    font-size: 1.875rem;
    font-weight: 700;
    color: #111827;
}
.modal-container-collection .product-popup-original-price {
    margin-left: 0.5rem;
    font-size: 1.125rem;
    color: #6b7280;
    text-decoration: line-through;
}
.modal-container-collection .product-popup-discount {
    margin-left: 0.5rem;
    font-size: 1.125rem;
    font-weight: 600;
    color: #059669;
}
.modal-container-collection .product-popup-product-description {
    color: #4b5563;
    margin: 0;
}
/* Color and size selectors */
.modal-container-collection .product-popup-color-selector, .product-popup-size-selector {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
.modal-container-collection .product-popup-selector-title {
    font-size: 1.125rem;
    font-weight: 600;
    color: #111827;
}
.modal-container-collection .product-popup-color-options, .product-popup-size-options {
    display: flex;
    gap: 0.5rem;
}
.modal-container-collection .product-popup-color-option, .product-popup-size-option {
    width: 2rem;
    height: 2rem;
    border-radius: 9999px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
  min-height: 35px;
    min-width: 61px;
  font-size: 12px;
}
.modal-container-collection .product-popup-color-option.selected {
    box-shadow: 0 0 0 2px white, 0 0 0 4px #3b82f6;
}
.modal-container-collection .product-popup-size-option {
    width: auto;
    padding: 0.5rem 0.75rem;
    font-size: 0.875rem;
    font-weight: 500;
    background-color: #f3f4f6;
    color: #111827;
    border-radius: 0.375rem;
}
.modal-container-collection .product-popup-size-option.selected {
    background-color: #3b82f6;
    color: white;
}
/* Quantity and add to cart */
.modal-container-collection .product-popup-quantity-cart {
    display: flex;
    align-items: center;
    gap: 1rem;
}
.modal-container-collection .product-popup-quantity-selector {
    display: flex;
    align-items: center;
    border: 1px solid #d1d5db;
    border-radius: 0.375rem;
}
.modal-container-collection .product-popup-quantity-btn {
    padding: 0.5rem;
    background: none;
    border: none;
    cursor: pointer;
    color: #6b7280;
}
.modal-container-collection .product-popup-quantity-display {
    padding: 0.5rem 1rem;
    font-size: 1.125rem;
    font-weight: 600;
}
.modal-container-collection .product-popup-add-to-cart {
    flex: 1;
    padding: 0.75rem 1rem;
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 0.375rem;
    font-size: 1rem;
    font-weight: 500;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
}
.modal-container-collection .product-popup-add-to-cart:hover {
    background-color: #2563eb;
}
/* Product details and related products */
.modal-container-collection .product-popup-separator {
    height: 1px;
    background-color: #e5e7eb;
    margin: 1.5rem 0;
}
.modal-container-collection .product-popup-details-title {
    font-size: 1.125rem;
    font-weight: 600;
    color: #111827;
    margin-bottom: 1rem;
}
.modal-container-collection .product-popup-details-list {
    list-style-position: inside;
    color: #4b5563;
}
.modal-container-collection .product-popup-related-products {
    background-color: #f9fafb;
    padding: 1.5rem;
    border-bottom-left-radius: 0.5rem;
    border-bottom-right-radius: 0.5rem;
}
.modal-container-collection .product-popup-related-title {
    font-size: 1.25rem;
    font-weight: 600;
    color: #111827;
    margin-bottom: 1rem;
}
.modal-container-collection .product-popup-related-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
}
.modal-container-collection @media (min-width: 640px) {
    .modal-container-collection .product-popup-related-grid {
        grid-template-columns: repeat(4, 1fr);
    }
}
.modal-container-collection .product-popup-related-item {
    background-color: white;
    border-radius: 0.5rem;
    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
    padding: 1rem;
}
.modal-container-collection .product-popup-related-image {
    aspect-ratio: 1;
    background-color: #f3f4f6;
    border-radius: 0.375rem;
    margin-bottom: 0.5rem;
}
.modal-container-collection .product-popup-related-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 0.375rem;
}
.modal-container-collection .product-popup-related-name {
    font-size: 0.875rem;
    font-weight: 600;
    color: #111827;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.modal-container-collection .product-popup-related-price {
    font-size: 0.875rem;
    color: #6b7280;
}
</style>

<div class="product-popup-modal-overlay modal-container-collection">
<div class="product-popup-modal">
    <div class="product-popup-modal-content">
        <!-- Product Images -->
        <div class="product-popup-product-images">
            <div class="product-popup-main-image">
              <img src="{{ product.featured_image | image_url }}" alt="Product image 1">
                <span class="product-popup-image-badge">New</span>
            </div>
            <div class="product-popup-image-thumbnails">
              {%- for media in product.media -%}
                <div class="product-popup-thumbnail" onclick="window.changeImage('{{ media.image | image_url }}')">
                  <img src="{{ media.image | image_url }}" alt="Product image 1" height="400" width="400">
                </div>
              {%- endfor -%}              
            </div>
        </div>

        <!-- Product Details -->
        <div class="product-popup-product-details">
            <div>
                <h2 class="product-popup-product-title">{{ product.title }}</h2>
            </div>

            <div class="product-popup-rating">
                <a href="{{ product.url }}" style="text-decoration: none;">
                  <span style="color: #3b82f6; text-decoration: underline;" class="product-popup-review-count">See more</span>
                  <span style="vertical-align: middle; text-decoration: none; margin-left: 3px;">
                    <svg fill="#3b82f6" width="17px" height="17px" viewBox="0 0 35 35" data-name="Layer 2" id="Layer_2" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M21.81,34.75H21.5A3.8,3.8,0,0,1,18,31.58L16,20.12A1.36,1.36,0,0,0,14.88,19L3.42,17a3.84,3.84,0,0,1-.56-7.42L29.66.46h0a3.84,3.84,0,0,1,4.88,4.88l-9.11,26.8A3.79,3.79,0,0,1,21.81,34.75ZM30.47,2.83,3.66,11.94a1.34,1.34,0,0,0,.2,2.59l11.46,2a3.85,3.85,0,0,1,3.11,3.11l2,11.46a1.34,1.34,0,0,0,2.59.2L32.17,4.53a1.34,1.34,0,0,0-1.7-1.7Z"></path></g></svg>
                  </span>
                </a>
            </div>

            <div>
                <span class="product-popup-price">{{ product.price | money_with_currency }}</span>
            </div>

            <p class="product-popup-product-description">
                {{ product.description }}
            </p>


          {% render 'custom-variant-stuff',
            product: product
          %}

            <div class="product-popup-quantity-cart">
                <div class="product-popup-quantity-selector">
                    <button class="product-popup-quantity-btn" onclick="changeQuantity(-1)" aria-label="Decrease quantity">-</button>
                    <span class="product-popup-quantity-display" id="quantity">1</span>
                    <button class="product-popup-quantity-btn" onclick="changeQuantity(1)" aria-label="Increase quantity">+</button>
                </div>
                <button class="product-popup-add-to-cart" onclick="window.addToCart('{{ product.handle }}')">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <circle cx="9" cy="21" r="1"></circle>
                        <circle cx="20" cy="21" r="1"></circle>
                        <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
                    </svg>
                    Add to Cart
                </button>
            </div>
        </div>
    </div>
</div>
  <script type="application/json" data-selected-variant>{{ product.selected_or_first_available_variant | json }}</script>
</div>


{% schema %}
{
"name": "Section name",
"class": "shopify-section-product-popup",
"settings": []
}
{% endschema %}

Phew that is a lot of code. The first part is styles for our modal and the second piece is the HTML / Liquid code to render our product. We are also rendering a snippet for the variants that we need to create next.

Under the ‘snippets’ directory, let’s create a file called ‘custom-variant-stuff.liquid’ (feel free to name this whatever you’d like, just change it in the above code to match). Then I’ll paste in this code:

{%- unless product.has_only_default_variant -%}
  <div class="product-popup-size-selector">
    <h3 class="product-popup-selector-title">Variants</h3>
    {%- for option in product.options_with_values -%}
    <div class="product-popup-size-options">
      {%- for value in option.values -%}
      <div onclick="{% if option.position == 1 %}selectColor(this, '{{ value.id }}') {% else %}window.selectSize(this, '{{ value.id }}'){% endif %}" class="product-popup-size-option {% if option.position == 1 %}color-option {% else %}size-option{% endif %} {% if forloop.index0 == 0 %}selected{% endif %}" data-id="{{ value.id }}">
        {{ value }}
      </div>
      {% endfor %}
    </div>
    {% endfor %}

  </div>
  {%- endunless -%}

This code is liquid logic to loop through our different variants and render them grouped correctly, eg. Sizes and Colors grouped together.

After saving these files, we are now ready to open our modal. To do this we are going to add some JavaScript at the bottom of our ‘card-product.liquid’ file.

Adding JavaScript to Open Modal

Back in our ‘card-product.liquid’ file we are going to paste in the following script at the bottom of the page:


<script>
  async function getNewVariant(handle) {
    let selectedColorOption = document.querySelector('.color-option.selected')
    let selectedSizeOption = document.querySelector('.size-option.selected')
    const response = await fetch(
      `/products/${handle}?section_id=product-popup&option_values=${selectedColorOption?.dataset?.id || ''},${selectedSizeOption?.dataset?.id || ''}`
    );
    const html = await response.text();
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    const selectedVariantJson = doc.querySelector('script[data-selected-variant]').textContent;
    return JSON.parse(selectedVariantJson)
  }
  function changeQuantity(delta) {
      const currentQuantity = Number(document.getElementById('quantity').innerText);
      const newQuantity = Math.max(0, currentQuantity + delta);
      document.getElementById('quantity').textContent = newQuantity;
  }
  function closeModal() {
    let item = document.querySelector('.product-popup-modal-overlay')
    item.addEventListener('click', function(event) {
      if (event.target === this) {
        item.remove()
      }
    })
  }
  window.showModal = async function (handle) {
    let response = await fetch(`/products/${handle}?section_id=product-popup`);
    let html = await response.text();
    document.body.insertAdjacentHTML('beforeend', html); // Insert the fetched HTML into the body
    createListeners();
    closeModal()
  };
  function createListeners() {
    window.changeImage = function (src) {
      const mainImage = document.querySelector('.product-popup-main-image img');
      mainImage.src = src;
    };
    window.selectColor = function (element, color) {
      document.querySelectorAll('.color-option').forEach((el) => {
        if (el !== element) {
          el.classList.remove('selected');
        }
      });
      element.classList.add('selected');
    };
    window.selectSize = function (element, color) {
      document.querySelectorAll('.size-option').forEach((el) => {
        if (el !== element) {
          el.classList.remove('selected');
        }
      });
      element.classList.add('selected');
    };
    window.addToCart = async function (handle) {
      const variant = await getNewVariant(handle);
      const quantity = Number(document.getElementById('quantity').textContent) || 1
      let formData = {
       'items': [{
        'id': variant.id,
        'quantity': quantity
        }]
      };
      fetch(window.Shopify.routes.root + 'cart/add.js', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formData)
      })
      .then(response => {
          window.location.href = '/cart'
        
        return response.json();
      })
      .catch((error) => {
        console.error('Error:', error);
      });
    };
  }
</script>

This may look like a lot of code, but all we are doing is handling clicks for things like the add to cart, quantity updates, and variant updates. Most of the logic is being handled via the section rendering API which is giving us the HTML to display as well as the correct variant ID to add to our cart!

Now if we save these files we should see a working quick view modal (for up to 2 variants per product).

Conclusion

A quick view popup modal is a game-changer for user experience on Shopify collection pages. By integrating the Section Rendering API, you create a dynamic and interactive way for customers to explore and add products to their cart without navigating away. This feature not only improves usability but also boosts conversion rates by simplifying the path to purchase. With the steps in this guide, your Shopify store will deliver a modern, customer-friendly experience that keeps shoppers coming back for more.