JAVASCRIPTshopifybeginner
Shopify Recently Viewed Products
Track and display products customers have recently viewed
Faisal Yaqoob
November 26, 2025
#shopify#recently-viewed#localstorage#recommendations
Code
javascript
1 // Recently Viewed Products Class 2 class RecentlyViewed { 3 constructor(options = {}) { 4 this.storageKey = options.storageKey || 'recently_viewed_products'; 5 this.maxProducts = options.maxProducts || 8; 6 this.currentProduct = options.currentProduct || null; 7 this.container = document.querySelector(options.container || '[data-recently-viewed]'); 8
9 this.init(); 10 } 11
12 init() { 13 // Track current product 14 if (this.currentProduct) { 15 this.trackProduct(this.currentProduct); 16 } 17
18 // Render recently viewed products 19 if (this.container) { 20 this.render(); 21 } 22 } 23
24 trackProduct(product) { 25 let recentlyViewed = this.getRecentlyViewed(); 26
27 // Remove product if already exists 28 recentlyViewed = recentlyViewed.filter(p => p.id !== product.id); 29
30 // Add to beginning of array 31 recentlyViewed.unshift(product); 32
33 // Limit to maxProducts 34 if (recentlyViewed.length > this.maxProducts) { 35 recentlyViewed = recentlyViewed.slice(0, this.maxProducts); 36 } 37
38 // Save to localStorage 39 localStorage.setItem(this.storageKey, JSON.stringify(recentlyViewed)); 40 } 41
42 getRecentlyViewed() { 43 try { 44 const data = localStorage.getItem(this.storageKey); 45 return data ? JSON.parse(data) : []; 46 } catch (error) { 47 console.error('Error reading recently viewed:', error); 48 return []; 49 } 50 } 51
52 async render() { 53 let products = this.getRecentlyViewed(); 54
55 // Filter out current product 56 if (this.currentProduct) { 57 products = products.filter(p => p.id !== this.currentProduct.id); 58 } 59
60 if (products.length === 0) { 61 this.container.style.display = 'none'; 62 return; 63 } 64
65 // Fetch fresh product data 66 const freshProducts = await this.fetchProducts(products.map(p => p.handle)); 67
68 this.container.innerHTML = this.getTemplate(freshProducts); 69 this.container.style.display = 'block'; 70 } 71
72 async fetchProducts(handles) { 73 const products = []; 74
75 for (const handle of handles) { 76 try { 77 const response = await fetch(`/products/${handle}.js`); 78 const product = await response.json(); 79 products.push(product); 80 } catch (error) { 81 console.error(`Error fetching product ${handle}:`, error); 82 } 83 } 84
85 return products; 86 } 87
88 getTemplate(products) { 89 return ` 90 <div class="recently-viewed"> 91 <h2 class="recently-viewed-title">Recently Viewed</h2> 92
93 <div class="recently-viewed-grid"> 94 ${products.map(product => this.getProductCard(product)).join('')} 95 </div> 96 </div> 97 `; 98 } 99
100 getProductCard(product) { 101 return ` 102 <div class="recently-viewed-item"> 103 <a href="/products/${product.handle}" class="recently-viewed-link"> 104 <div class="recently-viewed-image"> 105 <img src="${product.featured_image}" alt="${product.title}"> 106 </div> 107
108 <div class="recently-viewed-info"> 109 <h3 class="recently-viewed-product-title">${product.title}</h3> 110
111 <div class="recently-viewed-price"> 112 ${this.formatPrice(product.price)} 113 </div> 114 </div> 115 </a> 116 </div> 117 `; 118 } 119
120 formatPrice(cents) { 121 return '$' + (cents / 100).toFixed(2); 122 } 123
124 clear() { 125 localStorage.removeItem(this.storageKey); 126 } 127 } 128
129 // Initialize on product pages 130 document.addEventListener('DOMContentLoaded', () => { 131 // Check if we're on a product page 132 const productData = document.querySelector('[data-product-json]'); 133
134 if (productData) { 135 try { 136 const product = JSON.parse(productData.textContent); 137
138 new RecentlyViewed({ 139 currentProduct: { 140 id: product.id, 141 handle: product.handle, 142 title: product.title, 143 image: product.featured_image, 144 price: product.price 145 } 146 }); 147 } catch (error) { 148 console.error('Error parsing product data:', error); 149 } 150 } else { 151 // Initialize for displaying recently viewed on other pages 152 new RecentlyViewed({ 153 container: '[data-recently-viewed]' 154 }); 155 } 156 });
Shopify Recently Viewed Products
Keep track of products customers have viewed and display them to encourage return visits and increase conversions.
// Recently Viewed Products Class
class RecentlyViewed {
constructor(options = {}) {
this.storageKey = options.storageKey || 'recently_viewed_products';
this.maxProducts = options.maxProducts || 8;
this.currentProduct = options.currentProduct || null;
this.container = document.querySelector(options.container || '[data-recently-viewed]');
this.init();
}
init() {
// Track current product
if (this.currentProduct) {
this.trackProduct(this.currentProduct);
}
// Render recently viewed products
if (this.container) {
this.render();
}
}
trackProduct(product) {
let recentlyViewed = this.getRecentlyViewed();
// Remove product if already exists
recentlyViewed = recentlyViewed.filter(p => p.id !== product.id);
// Add to beginning of array
recentlyViewed.unshift(product);
// Limit to maxProducts
if (recentlyViewed.length > this.maxProducts) {
recentlyViewed = recentlyViewed.slice(0, this.maxProducts);
}
// Save to localStorage
localStorage.setItem(this.storageKey, JSON.stringify(recentlyViewed));
}
getRecentlyViewed() {
try {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
} catch (error) {
console.error('Error reading recently viewed:', error);
return [];
}
}
async render() {
let products = this.getRecentlyViewed();
// Filter out current product
if (this.currentProduct) {
products = products.filter(p => p.id !== this.currentProduct.id);
}
if (products.length === 0) {
this.container.style.display = 'none';
return;
}
// Fetch fresh product data
const freshProducts = await this.fetchProducts(products.map(p => p.handle));
this.container.innerHTML = this.getTemplate(freshProducts);
this.container.style.display = 'block';
}
async fetchProducts(handles) {
const products = [];
for (const handle of handles) {
try {
const response = await fetch(`/products/${handle}.js`);
const product = await response.json();
products.push(product);
} catch (error) {
console.error(`Error fetching product ${handle}:`, error);
}
}
return products;
}
getTemplate(products) {
return `
<div class="recently-viewed">
<h2 class="recently-viewed-title">Recently Viewed</h2>
<div class="recently-viewed-grid">
${products.map(product => this.getProductCard(product)).join('')}
</div>
</div>
`;
}
getProductCard(product) {
return `
<div class="recently-viewed-item">
<a href="/products/${product.handle}" class="recently-viewed-link">
<div class="recently-viewed-image">
<img src="${product.featured_image}" alt="${product.title}">
</div>
<div class="recently-viewed-info">
<h3 class="recently-viewed-product-title">${product.title}</h3>
<div class="recently-viewed-price">
${this.formatPrice(product.price)}
</div>
</div>
</a>
</div>
`;
}
formatPrice(cents) {
return '$' + (cents / 100).toFixed(2);
}
clear() {
localStorage.removeItem(this.storageKey);
}
}
// Initialize on product pages
document.addEventListener('DOMContentLoaded', () => {
// Check if we're on a product page
const productData = document.querySelector('[data-product-json]');
if (productData) {
try {
const product = JSON.parse(productData.textContent);
new RecentlyViewed({
currentProduct: {
id: product.id,
handle: product.handle,
title: product.title,
image: product.featured_image,
price: product.price
}
});
} catch (error) {
console.error('Error parsing product data:', error);
}
} else {
// Initialize for displaying recently viewed on other pages
new RecentlyViewed({
container: '[data-recently-viewed]'
});
}
});
Liquid Template - Product Page
<!-- Add this hidden div with product data on product pages -->
<script type="application/json" data-product-json>
{
"id": {{ product.id }},
"handle": "{{ product.handle }}",
"title": {{ product.title | json }},
"featured_image": "{{ product.featured_image | img_url: 'large' }}",
"price": {{ product.price }}
}
</script>
Liquid Template - Display Section
<!-- Add this where you want to display recently viewed products -->
<section class="recently-viewed-section">
<div class="container">
<div data-recently-viewed></div>
</div>
</section>
CSS Styling
.recently-viewed-section {
padding: 60px 0;
background: #f9f9f9;
}
.recently-viewed {
display: none; /* Hidden until populated */
}
.recently-viewed-title {
font-size: 28px;
margin-bottom: 30px;
text-align: center;
}
.recently-viewed-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 30px;
}
.recently-viewed-item {
background: #fff;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
}
.recently-viewed-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.recently-viewed-link {
text-decoration: none;
color: inherit;
display: block;
}
.recently-viewed-image {
position: relative;
padding-bottom: 100%;
overflow: hidden;
background: #f5f5f5;
}
.recently-viewed-image img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.recently-viewed-info {
padding: 15px;
}
.recently-viewed-product-title {
font-size: 16px;
margin: 0 0 10px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.recently-viewed-price {
font-size: 18px;
font-weight: bold;
color: #000;
}
/* Responsive */
@media (max-width: 768px) {
.recently-viewed-grid {
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.recently-viewed-title {
font-size: 22px;
margin-bottom: 20px;
}
}
Advanced: With Slider
// Recently Viewed with Carousel
class RecentlyViewedSlider extends RecentlyViewed {
getTemplate(products) {
return `
<div class="recently-viewed">
<div class="recently-viewed-header">
<h2 class="recently-viewed-title">Recently Viewed</h2>
<div class="recently-viewed-controls">
<button class="slider-arrow slider-prev" data-prev>←</button>
<button class="slider-arrow slider-next" data-next>→</button>
</div>
</div>
<div class="recently-viewed-slider" data-slider>
<div class="recently-viewed-track" data-track>
${products.map(product => this.getProductCard(product)).join('')}
</div>
</div>
</div>
`;
}
async render() {
await super.render();
if (this.container.querySelector('[data-slider]')) {
this.initSlider();
}
}
initSlider() {
const track = this.container.querySelector('[data-track]');
const prevBtn = this.container.querySelector('[data-prev]');
const nextBtn = this.container.querySelector('[data-next]');
let position = 0;
const itemWidth = 280; // Width + gap
prevBtn.addEventListener('click', () => {
position = Math.min(position + itemWidth, 0);
track.style.transform = `translateX(${position}px)`;
});
nextBtn.addEventListener('click', () => {
const maxScroll = -(track.scrollWidth - track.parentElement.offsetWidth);
position = Math.max(position - itemWidth, maxScroll);
track.style.transform = `translateX(${position}px)`;
});
}
}
Clear Recently Viewed (Optional)
<!-- Add a clear button if needed -->
<button onclick="clearRecentlyViewed()">Clear History</button>
<script>
function clearRecentlyViewed() {
localStorage.removeItem('recently_viewed_products');
location.reload();
}
</script>
Features
- Automatic Tracking: Tracks products as customers view them
- Persistent: Uses localStorage to persist across sessions
- Smart Filtering: Excludes current product from display
- Fresh Data: Fetches live product info on render
- Configurable Limit: Set maximum number to track
- Responsive Grid: Mobile-friendly layout
- Slider Option: Advanced carousel version available
- Privacy-Friendly: Stored locally, not on server
Related Snippets
Shopify Wishlist with LocalStorage
Add wishlist/favorites functionality without requiring a Shopify app
JAVASCRIPTshopifyintermediate
javascriptPreview
// Wishlist Class
class Wishlist {
constructor(options = {}) {
this.storageKey = options.storageKey || 'shopify_wishlist';
...#shopify#wishlist#favorites+1
11/28/2025
View
Shopify AJAX Collection Filters
Add dynamic filtering to collection pages without page reloads
JAVASCRIPTshopifyadvanced
javascriptPreview
// Collection Filters Class
class CollectionFilters {
constructor(options = {}) {
this.container = document.querySelector(options.container || '[data-collection-container]');
...#shopify#filters#ajax+2
11/27/2025
View
Shopify Free Shipping Progress Bar
Show customers how much more they need to spend for free shipping
JAVASCRIPTshopifybeginner
javascriptPreview
// Free Shipping Bar Class
class FreeShippingBar {
constructor(options = {}) {
this.threshold = options.threshold || 5000; // Amount in cents
...#shopify#shipping#cart+2
11/25/2025
View