JAVASCRIPTshopifybeginner

Shopify Product Countdown Timer

Add urgency with countdown timers for sales and limited-time offers

#shopify#countdown#timer#urgency#sales
Share this snippet:

Code

javascript
1// Countdown Timer Class
2class 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
137document.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