JAVASCRIPTshopifybeginner
Shopify Product Countdown Timer
Add urgency with countdown timers for sales and limited-time offers
Faisal Yaqoob
November 20, 2025
#shopify#countdown#timer#urgency#sales
Code
javascript
1 // Countdown Timer Class 2 class CountdownTimer { 3 constructor(element, endTime, options = {}) { 4 this.element = element; 5 this.endTime = new Date(endTime).getTime(); 6 this.options = { 7 labels: { 8 days: 'Days', 9 hours: 'Hours', 10 minutes: 'Minutes', 11 seconds: 'Seconds', 12 }, 13 compact: options.compact || false, 14 hideOnExpire: options.hideOnExpire || false, 15 onExpire: options.onExpire || null, 16 ...options 17 }; 18
19 this.interval = null; 20 this.init(); 21 } 22
23 init() { 24 if (isNaN(this.endTime)) { 25 console.error('Invalid end time provided'); 26 return; 27 } 28
29 this.render(); 30 this.start(); 31 } 32
33 render() { 34 const template = this.options.compact ? this.getCompactTemplate() : this.getFullTemplate(); 35 this.element.innerHTML = template; 36 } 37
38 getFullTemplate() { 39 return ` 40 <div class="countdown"> 41 <div class="countdown-section"> 42 <span class="countdown-number" data-days>00</span> 43 <span class="countdown-label">${this.options.labels.days}</span> 44 </div> 45 <div class="countdown-separator">:</div> 46 <div class="countdown-section"> 47 <span class="countdown-number" data-hours>00</span> 48 <span class="countdown-label">${this.options.labels.hours}</span> 49 </div> 50 <div class="countdown-separator">:</div> 51 <div class="countdown-section"> 52 <span class="countdown-number" data-minutes>00</span> 53 <span class="countdown-label">${this.options.labels.minutes}</span> 54 </div> 55 <div class="countdown-separator">:</div> 56 <div class="countdown-section"> 57 <span class="countdown-number" data-seconds>00</span> 58 <span class="countdown-label">${this.options.labels.seconds}</span> 59 </div> 60 </div> 61 `; 62 } 63
64 getCompactTemplate() { 65 return ` 66 <div class="countdown countdown--compact"> 67 <span data-days>00</span>: 68 <span data-hours>00</span>: 69 <span data-minutes>00</span>: 70 <span data-seconds>00</span> 71 </div> 72 `; 73 } 74
75 start() { 76 this.update(); 77 this.interval = setInterval(() => this.update(), 1000); 78 } 79
80 update() { 81 const now = new Date().getTime(); 82 const distance = this.endTime - now; 83
84 if (distance < 0) { 85 this.expire(); 86 return; 87 } 88
89 const days = Math.floor(distance / (1000 * 60 * 60 * 24)); 90 const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 91 const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); 92 const seconds = Math.floor((distance % (1000 * 60)) / 1000); 93
94 this.element.querySelector('[data-days]').textContent = this.pad(days); 95 this.element.querySelector('[data-hours]').textContent = this.pad(hours); 96 this.element.querySelector('[data-minutes]').textContent = this.pad(minutes); 97 this.element.querySelector('[data-seconds]').textContent = this.pad(seconds); 98 } 99
100 expire() { 101 this.stop(); 102
103 if (this.options.hideOnExpire) { 104 this.element.style.display = 'none'; 105 } else { 106 this.element.innerHTML = '<p class="countdown-expired">Sale Ended</p>'; 107 } 108
109 if (typeof this.options.onExpire === 'function') { 110 this.options.onExpire(); 111 } 112
113 // Trigger custom event 114 document.dispatchEvent(new CustomEvent('countdown:expired', { 115 detail: { element: this.element } 116 })); 117 } 118
119 pad(num) { 120 return num.toString().padStart(2, '0'); 121 } 122
123 stop() { 124 if (this.interval) { 125 clearInterval(this.interval); 126 this.interval = null; 127 } 128 } 129
130 destroy() { 131 this.stop(); 132 this.element.innerHTML = ''; 133 } 134 } 135
136 // Auto-initialize all countdown timers 137 document.addEventListener('DOMContentLoaded', () => { 138 document.querySelectorAll('[data-countdown]').forEach(element => { 139 const endTime = element.dataset.countdown; 140 const compact = element.dataset.countdownCompact === 'true'; 141 const hideOnExpire = element.dataset.countdownHide === 'true'; 142
143 new CountdownTimer(element, endTime, { 144 compact, 145 hideOnExpire, 146 onExpire: () => { 147 // Custom action on expire 148 console.log('Countdown expired!'); 149 } 150 }); 151 }); 152 });
Shopify Product Countdown Timer
Create countdown timers for flash sales, limited-time offers, or product launches to create urgency and boost conversions.
// Countdown Timer Class
class CountdownTimer {
constructor(element, endTime, options = {}) {
this.element = element;
this.endTime = new Date(endTime).getTime();
this.options = {
labels: {
days: 'Days',
hours: 'Hours',
minutes: 'Minutes',
seconds: 'Seconds',
},
compact: options.compact || false,
hideOnExpire: options.hideOnExpire || false,
onExpire: options.onExpire || null,
...options
};
this.interval = null;
this.init();
}
init() {
if (isNaN(this.endTime)) {
console.error('Invalid end time provided');
return;
}
this.render();
this.start();
}
render() {
const template = this.options.compact ? this.getCompactTemplate() : this.getFullTemplate();
this.element.innerHTML = template;
}
getFullTemplate() {
return `
<div class="countdown">
<div class="countdown-section">
<span class="countdown-number" data-days>00</span>
<span class="countdown-label">${this.options.labels.days}</span>
</div>
<div class="countdown-separator">:</div>
<div class="countdown-section">
<span class="countdown-number" data-hours>00</span>
<span class="countdown-label">${this.options.labels.hours}</span>
</div>
<div class="countdown-separator">:</div>
<div class="countdown-section">
<span class="countdown-number" data-minutes>00</span>
<span class="countdown-label">${this.options.labels.minutes}</span>
</div>
<div class="countdown-separator">:</div>
<div class="countdown-section">
<span class="countdown-number" data-seconds>00</span>
<span class="countdown-label">${this.options.labels.seconds}</span>
</div>
</div>
`;
}
getCompactTemplate() {
return `
<div class="countdown countdown--compact">
<span data-days>00</span>:
<span data-hours>00</span>:
<span data-minutes>00</span>:
<span data-seconds>00</span>
</div>
`;
}
start() {
this.update();
this.interval = setInterval(() => this.update(), 1000);
}
update() {
const now = new Date().getTime();
const distance = this.endTime - now;
if (distance < 0) {
this.expire();
return;
}
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
this.element.querySelector('[data-days]').textContent = this.pad(days);
this.element.querySelector('[data-hours]').textContent = this.pad(hours);
this.element.querySelector('[data-minutes]').textContent = this.pad(minutes);
this.element.querySelector('[data-seconds]').textContent = this.pad(seconds);
}
expire() {
this.stop();
if (this.options.hideOnExpire) {
this.element.style.display = 'none';
} else {
this.element.innerHTML = '<p class="countdown-expired">Sale Ended</p>';
}
if (typeof this.options.onExpire === 'function') {
this.options.onExpire();
}
// Trigger custom event
document.dispatchEvent(new CustomEvent('countdown:expired', {
detail: { element: this.element }
}));
}
pad(num) {
return num.toString().padStart(2, '0');
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
destroy() {
this.stop();
this.element.innerHTML = '';
}
}
// Auto-initialize all countdown timers
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-countdown]').forEach(element => {
const endTime = element.dataset.countdown;
const compact = element.dataset.countdownCompact === 'true';
const hideOnExpire = element.dataset.countdownHide === 'true';
new CountdownTimer(element, endTime, {
compact,
hideOnExpire,
onExpire: () => {
// Custom action on expire
console.log('Countdown expired!');
}
});
});
});
Liquid Template Usage
<!-- Flash Sale Timer -->
<div class="sale-banner">
<h3>Flash Sale Ends In:</h3>
<div
data-countdown="{{ '2025-12-31T23:59:59' | date: '%Y-%m-%dT%H:%M:%S' }}"
data-countdown-hide="false">
</div>
</div>
<!-- Product Page Timer -->
{% if product.metafields.custom.sale_end_date %}
<div class="product-countdown">
<p>Limited Time Offer!</p>
<div
data-countdown="{{ product.metafields.custom.sale_end_date }}"
data-countdown-compact="false">
</div>
</div>
{% endif %}
<!-- Compact Timer -->
<div class="timer-compact">
Sale ends in:
<div
data-countdown="{{ '2025-12-25T00:00:00' | date: '%Y-%m-%dT%H:%M:%S' }}"
data-countdown-compact="true">
</div>
</div>
<!-- Daily Deal (resets at midnight) -->
{% assign tomorrow = 'now' | date: '%s' | plus: 86400 | date: '%Y-%m-%dT00:00:00' %}
<div class="daily-deal">
<h4>Today's Deal Ends In:</h4>
<div data-countdown="{{ tomorrow }}"></div>
</div>
CSS Styling
/* Full Countdown */
.countdown {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
color: #fff;
}
.countdown-section {
display: flex;
flex-direction: column;
align-items: center;
min-width: 60px;
}
.countdown-number {
font-size: 32px;
font-weight: bold;
line-height: 1;
margin-bottom: 5px;
}
.countdown-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
opacity: 0.9;
}
.countdown-separator {
font-size: 24px;
font-weight: bold;
opacity: 0.7;
}
/* Compact Version */
.countdown--compact {
display: inline-flex;
gap: 5px;
padding: 10px 15px;
background: #000;
color: #fff;
border-radius: 4px;
font-size: 18px;
font-weight: bold;
font-family: monospace;
}
/* Expired State */
.countdown-expired {
text-align: center;
padding: 20px;
background: #e74c3c;
color: #fff;
border-radius: 8px;
font-weight: bold;
}
/* Sale Banner */
.sale-banner {
text-align: center;
padding: 30px;
background: #f8f9fa;
margin-bottom: 30px;
}
.sale-banner h3 {
margin-bottom: 15px;
font-size: 24px;
}
/* Product Countdown */
.product-countdown {
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
text-align: center;
}
.product-countdown p {
margin-bottom: 15px;
font-weight: bold;
color: #856404;
}
/* Responsive */
@media (max-width: 576px) {
.countdown-number {
font-size: 24px;
}
.countdown-section {
min-width: 50px;
}
.countdown {
gap: 10px;
padding: 15px;
}
}
Advanced: Evergreen Timer
// Evergreen timer that resets for each visitor
class EvergreenTimer extends CountdownTimer {
constructor(element, duration, options = {}) {
const storageKey = 'timer_' + element.dataset.timerId;
const savedEndTime = localStorage.getItem(storageKey);
let endTime;
if (savedEndTime) {
endTime = savedEndTime;
} else {
// Set end time to current time + duration (in minutes)
endTime = new Date(Date.now() + duration * 60000).toISOString();
localStorage.setItem(storageKey, endTime);
}
super(element, endTime, options);
}
expire() {
super.expire();
// Optionally reset the timer after expiration
const storageKey = 'timer_' + this.element.dataset.timerId;
localStorage.removeItem(storageKey);
}
}
// Usage: 24-hour evergreen timer
document.querySelectorAll('[data-evergreen-timer]').forEach(element => {
const duration = parseInt(element.dataset.evergreenTimer); // in minutes
new EvergreenTimer(element, duration, {
compact: false,
onExpire: () => {
// Redirect or show special offer
window.location.href = '/collections/sale';
}
});
});
Stock Countdown
// Combine with stock counter for urgency
class StockCountdown {
constructor(element, stock, options = {}) {
this.element = element;
this.stock = stock;
this.options = options;
this.render();
}
render() {
const urgencyClass = this.stock < 5 ? 'stock--critical' : this.stock < 20 ? 'stock--low' : '';
this.element.innerHTML = `
<div class="stock-counter ${urgencyClass}">
<span class="stock-icon">🔥</span>
<span class="stock-text">Only ${this.stock} left in stock!</span>
</div>
`;
}
}
Features
- Flexible: Support for both full and compact layouts
- Customizable: Custom labels and styling
- Auto-Initialize: Automatic setup from data attributes
- Evergreen Option: Personal timers for each visitor
- Event System: Triggers events on expiration
- Responsive: Mobile-friendly design
- Multiple Timers: Run multiple timers on same page
- Persistent: Remembers timer state
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 Recently Viewed Products
Track and display products customers have recently viewed
JAVASCRIPTshopifybeginner
javascriptPreview
// Recently Viewed Products Class
class RecentlyViewed {
constructor(options = {}) {
this.storageKey = options.storageKey || 'recently_viewed_products';
...#shopify#recently-viewed#localstorage+1
11/26/2025
View