Recently Viewed Products

Shows each shopper the products they looked at, so they can get back to them in a tap.

One section file Product page, plus the homepage or cart page if you like Edit in the theme editor

Important: Before making any changes to your theme, duplicate it in Online Store > Themes so you have a backup.

Most shoppers look at several products before they buy one. Recently viewed products saves them hunting through menus to get back to the one they liked. Views are remembered in the shopper's browser, so it needs no app and no account.

Preview

What you'll build

Recently viewed

Wool Beanie

£22.00

Leather Gloves

£45.00

Cashmere Scarf

£79.00

Rain Jacket

£120.00

Preview with the default settings. Colours, text and layout can all be changed in the theme editor.

Install

Add it to your store in 4 steps

  1. 1

    Open your theme code

    In Shopify admin go to Online Store > Themes. Next to the theme you want to edit, click the three dots and choose Edit code.

  2. 2

    Create the section

    In the Sections folder click Add a new section, name it lt-recently-viewed-products, delete the placeholder code, paste in the code below and click Save.

  3. 3

    Add it in the theme editor

    Click Customize, go to Products > Default product, click Add section and choose Recently viewed.

  4. 4

    Make it yours

    Change the settings in the sidebar, drag the section where you want it, and click Save.

sections/lt-recently-viewed-products.liquid · 186 lines
{%- comment -%}
  Recently viewed products by LAUNCHTIP
  https://launchtip.com/free-shopify-sections-code
  Add it to the product template so views are recorded. You can also add it
  to the home, collection or cart templates to show the same list there.
{%- endcomment -%}
{%- style -%}
  #lt-{{ section.id }} { padding: {{ section.settings.padding }}px 0; }
  #lt-{{ section.id }}[hidden] { display: none; }
  #lt-{{ section.id }} .lt-rv__heading { margin: 0 0 16px; }
  #lt-{{ section.id }} .lt-rv__grid { display: grid; gap: 16px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  @media (min-width: 750px) { #lt-{{ section.id }} .lt-rv__grid { grid-template-columns: repeat({{ section.settings.columns }}, minmax(0, 1fr)); } }
  #lt-{{ section.id }} .lt-rv__card { color: inherit; text-decoration: none; display: block; }
  #lt-{{ section.id }} .lt-rv__media { aspect-ratio: {{ section.settings.ratio }}; background: #f5f5f5; border-radius: 8px; overflow: hidden; }
  #lt-{{ section.id }} .lt-rv__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
  #lt-{{ section.id }} .lt-rv__title { margin: 10px 0 2px; font-size: 0.95em; }
  #lt-{{ section.id }} .lt-rv__price { font-size: 0.9em; opacity: 0.8; }
  #lt-{{ section.id }} .lt-rv__price s { opacity: 0.6; margin-left: 6px; }
{%- endstyle -%}

<div
  id="lt-{{ section.id }}"
  class="page-width"
  data-current="{% if template.name == 'product' %}{{ product.handle }}{% endif %}"
  data-limit="{{ section.settings.limit }}"
  data-root="{{ routes.root_url }}"
  data-show-price="{{ section.settings.show_price }}"
  data-currency="{{ cart.currency.iso_code }}"
  hidden
>
  {%- if section.settings.heading != blank -%}
    <h2 class="lt-rv__heading h2">{{ section.settings.heading }}</h2>
  {%- endif -%}
  <div class="lt-rv__grid"></div>
</div>

<script>
  (function () {
    var root = document.getElementById('lt-{{ section.id }}');
    if (!root || root.dataset.ready) return;
    root.dataset.ready = '1';

    var KEY = 'lt_recently_viewed';
    var current = root.dataset.current;
    var limit = Number(root.dataset.limit);
    var grid = root.querySelector('.lt-rv__grid');
    var base = root.dataset.root.replace(/\/$/, '');

    function read() {
      try { return JSON.parse(localStorage.getItem(KEY)) || []; } catch (e) { return []; }
    }

    var handles = read();
    if (current) {
      handles = [current].concat(handles.filter(function (h) { return h !== current; })).slice(0, 20);
      try { localStorage.setItem(KEY, JSON.stringify(handles)); } catch (e) {}
    }

    var toShow = handles.filter(function (h) { return h !== current; }).slice(0, limit);
    if (!toShow.length) return;

    function money(cents) {
      var currency = (window.Shopify && Shopify.currency && Shopify.currency.active) || root.dataset.currency;
      return new Intl.NumberFormat(document.documentElement.lang || undefined, { style: 'currency', currency: currency }).format(cents / 100);
    }

    function sized(src, width) {
      if (!src) return '';
      return src + (src.indexOf('?') > -1 ? '&' : '?') + 'width=' + width;
    }

    function card(product) {
      var link = document.createElement('a');
      link.className = 'lt-rv__card';
      link.href = product.url;

      var media = document.createElement('div');
      media.className = 'lt-rv__media';
      if (product.featured_image) {
        var img = document.createElement('img');
        img.src = sized(product.featured_image, 480);
        img.alt = product.title;
        img.loading = 'lazy';
        media.appendChild(img);
      }
      link.appendChild(media);

      var title = document.createElement('p');
      title.className = 'lt-rv__title';
      title.textContent = product.title;
      link.appendChild(title);

      if (root.dataset.showPrice === 'true') {
        var price = document.createElement('span');
        price.className = 'lt-rv__price';
        price.textContent = money(product.price);
        if (product.compare_at_price > product.price) {
          var was = document.createElement('s');
          was.textContent = money(product.compare_at_price);
          price.appendChild(was);
        }
        link.appendChild(price);
      }
      return link;
    }

    Promise.all(toShow.map(function (handle) {
      return fetch(base + '/products/' + encodeURIComponent(handle) + '.js')
        .then(function (r) { return r.ok ? r.json() : null; })
        .catch(function () { return null; });
    })).then(function (products) {
      var found = products.filter(Boolean);
      if (!found.length) return;
      var stillThere = found.map(function (p) { return p.handle; });
      localStorage.setItem(KEY, JSON.stringify(read().filter(function (h) { return h === current || stillThere.indexOf(h) > -1; })));
      found.forEach(function (product) { grid.appendChild(card(product)); });
      root.hidden = false;
    });
  })();
</script>

{% schema %}
{
  "name": "Recently viewed",
  "tag": "section",
  "class": "lt-rv-section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Recently viewed"
    },
    {
      "type": "range",
      "id": "limit",
      "label": "Products to show",
      "min": 2,
      "max": 12,
      "step": 1,
      "default": 4
    },
    {
      "type": "range",
      "id": "columns",
      "label": "Columns on desktop",
      "min": 2,
      "max": 6,
      "step": 1,
      "default": 4
    },
    {
      "type": "select",
      "id": "ratio",
      "label": "Image shape",
      "options": [
        { "value": "1 / 1", "label": "Square" },
        { "value": "3 / 4", "label": "Portrait" },
        { "value": "4 / 3", "label": "Landscape" }
      ],
      "default": "1 / 1"
    },
    {
      "type": "checkbox",
      "id": "show_price",
      "label": "Show price",
      "default": true
    },
    {
      "type": "range",
      "id": "padding",
      "label": "Vertical padding",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "default": 40
    }
  ],
  "presets": [
    {
      "name": "Recently viewed"
    }
  ]
}
{% endschema %}
Customization

What you can change in the theme editor

Setting Default
Heading Recently viewed
Products to show 4
Columns on desktop 4
Image shape Square
Show price On
Vertical padding 40px
Good to know

Tips for setting it up

  • The section must be on the product template to record views. Add it to the homepage or cart as well to show the same list there.
  • The product being viewed is left out of the list, so shoppers only see other products.
  • Products that are deleted or unpublished drop out of the list automatically.
FAQ

Frequently asked questions

Where are the views stored?

In the shopper's own browser (localStorage). Nothing is sent to a server and no cookie banner is needed for it.

Does it work across devices?

No. Each browser keeps its own list.

Why is the section empty?

It stays hidden until the shopper has viewed at least one other product.

More free sections

New

Sticky Add-to-Cart Bar

A bar that slides up when the buy button scrolls off screen, with price and variant.

New

Variant-Aware Content

Text and images that change when the shopper picks a different colour or size.

New

Blog Posts Grid for Homepage

Your latest blog posts in a clean grid with images, dates and excerpts.

Explore More Free Sections

Discover more code snippets to enhance your Shopify store.

View All Sections
SellUp Increase AOV with Upsells