Shopify Recently Viewed Products Snippet

Products a shopper looked at, rendered with your theme's own card markup through the Section Rendering API.

Describe the result you need. AI will set up the tool and ask for any missing details.

Configure with AI

Enter to submit · Shift + Enter for a new line

Sent to AI. Leave out passwords and private customer data.

Your details

Display
Style

Your section file (sections/recently-viewed.liquid)

recently-viewed.liquid
{%- comment -%}
  Recently viewed products, tracked in localStorage and rendered through the Section Rendering
  API so each card uses this section's own markup, fetched from that product's own page (where
  Shopify populates "product" for us). Add this section to your product template (so it can
  record views) and to any other page where you want the grid to show, like your cart or home
  page.

  IMPORTANT: this file must be named exactly recently-viewed.liquid. The script below fetches
  other pages with ?section_id=recently-viewed, a fixed name (not this instance's own section.id,
  which differs by template), so the file name and that string must match.
{%- endcomment -%}
{%- style -%}
  #RecentlyViewed-{{ section.id }} .ea-rv__heading{margin:0 0 .75rem;font-size:1.2em;font-weight:600}
  #RecentlyViewed-{{ section.id }} .ea-rv__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:1rem}
  #RecentlyViewed-{{ section.id }} .ea-rv__grid[hidden]{display:none}
  .ea-rv__card{display:block;background:#ffffff;border-radius:8px;overflow:hidden;text-decoration:none;color:inherit}
  .ea-rv__image{aspect-ratio:1/1;background:#f2f2f2;overflow:hidden}
  .ea-rv__image img{width:100%;height:100%;object-fit:cover;display:block}
  .ea-rv__title{margin:.5rem 0 .2rem;font-size:.9em;padding:0 .1rem}
  .ea-rv__price{margin:0;font-size:.85em;font-weight:600;color:#1a1a1a;padding:0 .1rem}
{%- endstyle -%}

<div
  id="RecentlyViewed-{{ section.id }}"
  data-current-handle="{{ product.handle }}"
  data-max="{{ section.settings.max }}"
  data-hide-if-empty="{{ section.settings.hide_if_empty }}"
  {% if section.settings.hide_if_empty %}hidden{% endif %}
>
  {%- if section.settings.heading != blank -%}<p class="ea-rv__heading">{{ section.settings.heading | escape }}</p>{%- endif -%}
  <div class="ea-rv__grid" data-grid hidden></div>
</div>

{%- if product -%}
  <template data-ea-rv-card="{{ product.handle }}">
    <a href="{{ product.url }}" class="ea-rv__card">
      <div class="ea-rv__image">
        {%- if product.featured_image -%}
          <img src="{{ product.featured_image | image_url: width: 300 }}" alt="{{ product.featured_image.alt | escape }}" loading="lazy" width="300" height="300">
        {%- endif -%}
      </div>
      <p class="ea-rv__title">{{ product.title | escape }}</p>
      <p class="ea-rv__price">{{ product.price | money }}</p>
    </a>
  </template>
{%- endif -%}

<script>
  (function () {
    var KEY = 'ea-recently-viewed';
    var root = document.getElementById('RecentlyViewed-{{ section.id }}');
    var grid = root ? root.querySelector('[data-grid]') : null;
    var currentHandle = root ? root.getAttribute('data-current-handle') : '';
    if (currentHandle) {
      try {
        var list = JSON.parse(localStorage.getItem(KEY) || '[]').filter(function (h) { return h !== currentHandle; });
        list.unshift(currentHandle);
        localStorage.setItem(KEY, JSON.stringify(list.slice(0, 30)));
      } catch (e) {}
    }
    if (!root || !grid) return;
    var handles = [];
    try { handles = JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) {}
    handles = handles.filter(function (h) { return h && h !== currentHandle; }).slice(0, Number(root.getAttribute('data-max')) || 4);
    if (!handles.length) {
      if (root.getAttribute('data-hide-if-empty') === 'true') root.hidden = true;
      return;
    }
    var rootUrl = (window.Shopify && window.Shopify.routes && window.Shopify.routes.root) || '/';
    // A fixed file name, not this instance's section.id: dynamic sections get a different id per
    // template, but the Section Rendering API can still fetch a section by its file name from any
    // page, rendering it fresh with that page's own product in context.
    var sectionId = 'recently-viewed';
    handles.forEach(function (handle) {
      fetch(rootUrl + 'products/' + handle + '?section_id=' + sectionId)
        .then(function (r) { return r.ok ? r.text() : Promise.reject(); })
        .then(function (html) {
          var doc = new DOMParser().parseFromString(html, 'text/html');
          var tpl = doc.querySelector('template[data-ea-rv-card="' + handle + '"]');
          if (!tpl) return;
          grid.appendChild(tpl.content.cloneNode(true));
          grid.hidden = false;
          root.hidden = false;
        })
        .catch(function () {
          try {
            var list = JSON.parse(localStorage.getItem(KEY) || '[]').filter(function (h) { return h !== handle; });
            localStorage.setItem(KEY, JSON.stringify(list));
          } catch (e) {}
        });
    });
  })();
</script>

{% schema %}
{
  "name": "Recently viewed",
  "tag": "section",
  "class": "ea-recently-viewed-section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Recently viewed"
    },
    {
      "type": "range",
      "id": "max",
      "label": "Products to show",
      "min": 2,
      "max": 12,
      "step": 1,
      "default": 4
    },
    {
      "type": "checkbox",
      "id": "hide_if_empty",
      "label": "Hide when there's nothing to show",
      "default": true
    }
  ],
  "presets": [
    {
      "name": "Recently viewed"
    }
  ]
}
{% endschema %}

Name the section file exactly recently-viewed.liquid. The script fetches other product pages with ?section_id=recently-viewed, a fixed name, since a section's own section.id changes per template and can't be relied on across pages.

To install: in Shopify admin go to Online Store → Themes, click … next to your theme → Edit code. Under Sections choose Add a new section, name it as shown above, replace the contents with this code and save. Then open Customize and add the section where you want it. Try it on a duplicate of your theme first. Add it to your product template so it can track views, and to any other page (cart, home) where you also want the grid to appear.

Each card is fetched with its own request to that product's page, so a shopper with many recently viewed products makes several small requests. Keep "Products to show" modest (4 to 6) for the best load time.

If a stored product was deleted or unpublished, its card fetch fails and it's quietly dropped from localStorage, so the list stays clean.

Live preview

Tracked in the browser, rendered by your theme

Every time a shopper opens a product page, this section records that product's handle in localStorage. Elsewhere on your store, it reads that list back and, for each handle, fetches that exact product's own page with ?section_id={{ section.id }} appended, the Section Rendering API, which returns just this section's markup, rendered with product set to that page's product.

Tucked inside the response is a hidden <template> holding a small product card for that page's own product. The JS pulls that card out and drops it into the grid, so recently viewed cards use the exact same Liquid, image filter and money formatting as the rest of this section, instead of a separate hand-built card that might drift out of sync.

  • Add this section to your product template so it can record views, even on pages where you don't want the grid to show.
  • Each card is its own small network request, so keep "Products to show" to 4 to 6 for the best load time.
  • If a stored product was deleted or unpublished, its fetch fails quietly and it's dropped from localStorage automatically.

Adding it to your theme

  1. 01

    Add the section

    Online Store → Themes → Edit code → Sections → Add a new section named recently-viewed, paste the code, then add it to your product template (for tracking) and to any other page, like your cart or home page, where you want the grid to appear.

Questions people ask

Does this need an app or Shopify's Recommendations API?

No app, and it's separate from Shopify's product recommendations feature (which suggests related products, not ones a shopper already viewed). It only uses localStorage and your own section.

Will it show on a shopper's first visit?

No, there's nothing to show until they've viewed at least one product on this device. It stays hidden (or shows nothing) until then, unless you turn off "Hide when there's nothing to show".

Does it sync between a shopper's phone and laptop?

No, localStorage is per browser and device, so recently viewed products won't carry over between devices or after clearing browser data.

Why do the cards take a moment to appear?

Each one is fetched individually from that product's page. This keeps the cards visually identical to your theme, at the cost of a brief delay compared to instant static content.

Want this builtinto your store?

I'm Emmanuel, a Shopify and WooCommerce developer. If this tool pointed at something you'd rather have fixed for you (a faster store, cleaner product data, a migration, a custom feature), send me a note.

Emmanuel Asika

Emmanuel Asika

Shopify & WooCommerce developer, Dublin

I build and fix online stores: new builds, migrations, speed, SEO and the custom bits apps can't do.

hello@emmanuelasika.com

Last reviewed 30 September 2026.

Sources: Shopify: Section Rendering API