Shopify Countdown Timer Generator
Add a sale deadline, daily shipping cutoff or per-visitor offer timer to Shopify as a section, with no app.
Your details
Your section file (sections/custom-countdown-timer.liquid)
{%- comment -%}
Countdown timer section: a fixed deadline, a daily cutoff or a per-visitor timer.
The deadline is worked out in the time zone you pick, so every shopper sees the same end time.
{%- endcomment -%}
{%- style -%}
#Countdown-{{ section.id }}{display:block;background:{{ section.settings.background }};color:{{ section.settings.text_color }};padding:1.5rem 1rem;text-align:center}
#Countdown-{{ section.id }}[hidden]{display:none}
#Countdown-{{ section.id }} .ea-cd__inner{max-width:720px;margin:0 auto;display:flex;flex-direction:column;align-items:center;gap:.75rem}
#Countdown-{{ section.id }} .ea-cd__heading{margin:0;font-size:1.25em;font-weight:600}
#Countdown-{{ section.id }} .ea-cd__sub{margin:0;opacity:.85}
#Countdown-{{ section.id }} .ea-cd__clock{display:flex;flex-wrap:wrap;gap:.5rem;justify-content:center}
#Countdown-{{ section.id }} .ea-cd__clock[hidden],#Countdown-{{ section.id }} .ea-cd__unit[hidden],#Countdown-{{ section.id }} .ea-cd__expired[hidden]{display:none}
#Countdown-{{ section.id }} .ea-cd__unit{display:flex;flex-direction:column;align-items:center;min-width:4.25rem;padding:.6rem .5rem;border-radius:8px;background:{{ section.settings.box_background }};color:{{ section.settings.box_text }}}
#Countdown-{{ section.id }} .ea-cd__num{font-size:1.75em;font-weight:700;font-variant-numeric:tabular-nums;line-height:1.1}
#Countdown-{{ section.id }} .ea-cd__label{font-size:.7em;text-transform:uppercase;letter-spacing:.06em;opacity:.8}
#Countdown-{{ section.id }} .ea-cd__expired{margin:0;font-weight:600}
#Countdown-{{ section.id }} .ea-cd__button{display:inline-block;padding:.7em 1.4em;border-radius:999px;background:{{ section.settings.text_color }};color:{{ section.settings.background }};text-decoration:none;font-weight:600}
@media (prefers-reduced-motion:reduce){#Countdown-{{ section.id }} .ea-cd__unit--seconds{display:none}}
{%- endstyle -%}
<ea-countdown
id="Countdown-{{ section.id }}"
class="ea-cd"
data-mode="{{ section.settings.mode }}"
data-date="{{ section.settings.end_date | escape }}"
data-time="{{ section.settings.end_time | escape }}"
data-cutoff="{{ section.settings.cutoff_time | escape }}"
data-skip-weekends="{{ section.settings.skip_weekends }}"
data-tz="{{ section.settings.timezone }}"
data-minutes="{{ section.settings.evergreen_minutes | default: 30 }}"
data-expired="{{ section.settings.expired }}"
hidden
>
<div class="ea-cd__inner">
{%- if section.settings.heading != blank -%}
<p class="ea-cd__heading" id="CountdownHeading-{{ section.id }}">{{ section.settings.heading | escape }}</p>
{%- endif -%}
{%- if section.settings.subheading != blank -%}
<p class="ea-cd__sub">{{ section.settings.subheading | escape }}</p>
{%- endif -%}
<div class="ea-cd__clock" role="timer" {% if section.settings.heading != blank %}aria-labelledby="CountdownHeading-{{ section.id }}"{% else %}aria-label="Countdown"{% endif %}>
<span class="ea-cd__unit" data-unit-days><span class="ea-cd__num" data-days>00</span><span class="ea-cd__label">{{ section.settings.label_days | escape }}</span></span>
<span class="ea-cd__unit"><span class="ea-cd__num" data-hours>00</span><span class="ea-cd__label">{{ section.settings.label_hours | escape }}</span></span>
<span class="ea-cd__unit"><span class="ea-cd__num" data-minutes>00</span><span class="ea-cd__label">{{ section.settings.label_minutes | escape }}</span></span>
<span class="ea-cd__unit ea-cd__unit--seconds"><span class="ea-cd__num" data-seconds>00</span><span class="ea-cd__label">{{ section.settings.label_seconds | escape }}</span></span>
</div>
<p class="ea-cd__expired" hidden>{{ section.settings.expired_text | escape }}</p>
{%- if section.settings.button_label != blank and section.settings.button_link != blank -%}
<a class="ea-cd__button" href="{{ section.settings.button_link | escape }}">{{ section.settings.button_label | escape }}</a>
{%- endif -%}
</div>
</ea-countdown>
<script>
if (!customElements.get('ea-countdown')) {
const partsIn = (date, tz) => {
const map = {};
new Intl.DateTimeFormat('en-US', {
timeZone: tz, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric',
hour: 'numeric', minute: 'numeric', second: 'numeric'
}).formatToParts(date).forEach((p) => { map[p.type] = p.value; });
return map;
};
// Wall-clock time in a time zone to a UTC timestamp (handles daylight saving).
const zonedTime = (y, mo, d, h, mi, tz) => {
const offset = (t) => {
const p = partsIn(new Date(t), tz);
return Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second) - Math.floor(t / 1000) * 1000;
};
const guess = Date.UTC(y, mo - 1, d, h, mi);
return guess - offset(guess - offset(guess));
};
customElements.define('ea-countdown', class extends HTMLElement {
connectedCallback() {
try {
new Intl.DateTimeFormat('en-US', { timeZone: this.dataset.tz });
this.tz = this.dataset.tz;
} catch (e) {
this.tz = 'UTC';
}
this.end = this.deadline();
this.hidden = false;
this.tick();
clearInterval(this.timer);
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
this.timer = setInterval(() => this.tick(), reduce ? 30000 : 1000);
}
disconnectedCallback() { clearInterval(this.timer); }
deadline() {
const d = this.dataset;
const hm = (value, fallback) => {
const [h, m] = String(value || fallback).split(':').map(Number);
return [h || 0, m || 0];
};
if (d.mode === 'evergreen') {
const key = 'ea-cd-start-' + this.id;
let start = Date.now();
try {
const saved = Number(localStorage.getItem(key));
if (saved) start = saved; else localStorage.setItem(key, String(start));
} catch (e) {}
return start + (Number(d.minutes) || 30) * 60000;
}
if (d.mode === 'daily') {
const [h, m] = hm(d.cutoff, '14:00');
const now = partsIn(new Date(), this.tz);
let day = Date.UTC(+now.year, +now.month - 1, +now.day);
for (let i = 0; i < 8; i++, day += 86400000) {
const date = new Date(day);
const weekend = date.getUTCDay() === 0 || date.getUTCDay() === 6;
if (d.skipWeekends === 'true' && weekend) continue;
const target = zonedTime(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), h, m, this.tz);
if (target > Date.now()) return target;
}
return null;
}
const [y, mo, dd] = String(d.date || '').split('-').map(Number);
if (!y || !mo || !dd) return null;
const [h, m] = hm(d.time, '23:59');
return zonedTime(y, mo, dd, h, m, this.tz);
}
set(unit, value) {
const el = this.querySelector('[data-' + unit + ']');
if (el) el.textContent = String(value).padStart(2, '0');
}
tick() {
if (this.dataset.mode === 'daily' && this.end && this.end <= Date.now()) this.end = this.deadline();
const left = this.end ? this.end - Date.now() : 0;
if (left <= 0) { this.finish(); return; }
const secs = Math.floor(left / 1000);
const days = Math.floor(secs / 86400);
this.querySelector('[data-unit-days]').hidden = days === 0;
this.set('days', days);
this.set('hours', Math.floor(secs / 3600) % 24);
this.set('minutes', Math.floor(secs / 60) % 60);
this.set('seconds', secs % 60);
}
finish() {
clearInterval(this.timer);
['days', 'hours', 'minutes', 'seconds'].forEach((u) => this.set(u, 0));
if (this.dataset.expired === 'message') {
this.querySelector('.ea-cd__clock').hidden = true;
this.querySelector('.ea-cd__expired').hidden = false;
} else if (!(window.Shopify && window.Shopify.designMode)) {
this.hidden = true;
}
}
});
}
</script>
{% schema %}
{
"name": "Countdown timer",
"tag": "section",
"class": "ea-countdown-section",
"settings": [
{
"type": "select",
"id": "mode",
"label": "Timer type",
"options": [
{
"value": "fixed",
"label": "Fixed deadline"
},
{
"value": "daily",
"label": "Daily cutoff"
},
{
"value": "evergreen",
"label": "Per visitor"
}
],
"default": "fixed"
},
{
"type": "text",
"id": "end_date",
"label": "End date",
"default": "2026-11-30",
"info": "Fixed deadline. Format YYYY-MM-DD."
},
{
"type": "text",
"id": "end_time",
"label": "End time",
"default": "23:59",
"info": "Fixed deadline. 24-hour HH:MM."
},
{
"type": "text",
"id": "cutoff_time",
"label": "Daily cutoff",
"default": "14:00",
"info": "Daily cutoff. 24-hour HH:MM."
},
{
"type": "checkbox",
"id": "skip_weekends",
"label": "No cutoff on weekends",
"default": true
},
{
"type": "select",
"id": "timezone",
"label": "Time zone",
"options": [
{
"value": "UTC",
"label": "UTC"
},
{
"value": "America/New_York",
"label": "America/New York"
},
{
"value": "America/Chicago",
"label": "America/Chicago"
},
{
"value": "America/Denver",
"label": "America/Denver"
},
{
"value": "America/Phoenix",
"label": "America/Phoenix"
},
{
"value": "America/Los_Angeles",
"label": "America/Los Angeles"
},
{
"value": "America/Anchorage",
"label": "America/Anchorage"
},
{
"value": "Pacific/Honolulu",
"label": "Pacific/Honolulu"
},
{
"value": "America/Toronto",
"label": "America/Toronto"
},
{
"value": "America/Vancouver",
"label": "America/Vancouver"
},
{
"value": "America/Mexico_City",
"label": "America/Mexico City"
},
{
"value": "America/Sao_Paulo",
"label": "America/Sao Paulo"
},
{
"value": "Europe/London",
"label": "Europe/London"
},
{
"value": "Europe/Dublin",
"label": "Europe/Dublin"
},
{
"value": "Europe/Lisbon",
"label": "Europe/Lisbon"
},
{
"value": "Europe/Paris",
"label": "Europe/Paris"
},
{
"value": "Europe/Berlin",
"label": "Europe/Berlin"
},
{
"value": "Europe/Madrid",
"label": "Europe/Madrid"
},
{
"value": "Europe/Rome",
"label": "Europe/Rome"
},
{
"value": "Europe/Amsterdam",
"label": "Europe/Amsterdam"
},
{
"value": "Europe/Stockholm",
"label": "Europe/Stockholm"
},
{
"value": "Europe/Warsaw",
"label": "Europe/Warsaw"
},
{
"value": "Europe/Athens",
"label": "Europe/Athens"
},
{
"value": "Europe/Helsinki",
"label": "Europe/Helsinki"
},
{
"value": "Africa/Johannesburg",
"label": "Africa/Johannesburg"
},
{
"value": "Africa/Lagos",
"label": "Africa/Lagos"
},
{
"value": "Asia/Dubai",
"label": "Asia/Dubai"
},
{
"value": "Asia/Kolkata",
"label": "Asia/Kolkata"
},
{
"value": "Asia/Singapore",
"label": "Asia/Singapore"
},
{
"value": "Asia/Tokyo",
"label": "Asia/Tokyo"
},
{
"value": "Australia/Perth",
"label": "Australia/Perth"
},
{
"value": "Australia/Brisbane",
"label": "Australia/Brisbane"
},
{
"value": "Australia/Sydney",
"label": "Australia/Sydney"
},
{
"value": "Australia/Melbourne",
"label": "Australia/Melbourne"
},
{
"value": "Pacific/Auckland",
"label": "Pacific/Auckland"
}
],
"default": "America/New_York"
},
{
"type": "number",
"id": "evergreen_minutes",
"label": "Minutes per visitor",
"info": "Per visitor timer only.",
"default": 30
},
{
"type": "select",
"id": "expired",
"label": "When the timer ends",
"options": [
{
"value": "hide",
"label": "Hide the section"
},
{
"value": "message",
"label": "Show a message"
}
],
"default": "hide"
},
{
"type": "text",
"id": "expired_text",
"label": "Message after the end",
"default": "This offer has ended."
},
{
"type": "header",
"content": "Text"
},
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Black Friday sale ends in"
},
{
"type": "text",
"id": "subheading",
"label": "Subheading",
"default": "Up to 40% off knitwear and accessories"
},
{
"type": "text",
"id": "button_label",
"label": "Button label",
"default": "Shop the sale"
},
{
"type": "text",
"id": "button_link",
"label": "Button link",
"default": "/collections/sale",
"info": "A path such as /collections/sale, or a full URL."
},
{
"type": "text",
"id": "label_days",
"label": "Days label",
"default": "Days"
},
{
"type": "text",
"id": "label_hours",
"label": "Hours label",
"default": "Hours"
},
{
"type": "text",
"id": "label_minutes",
"label": "Minutes label",
"default": "Minutes"
},
{
"type": "text",
"id": "label_seconds",
"label": "Seconds label",
"default": "Seconds"
},
{
"type": "header",
"content": "Colors"
},
{
"type": "color",
"id": "background",
"label": "Background",
"default": "#111111"
},
{
"type": "color",
"id": "text_color",
"label": "Text",
"default": "#ffffff"
},
{
"type": "color",
"id": "box_background",
"label": "Number boxes",
"default": "#2b2b2b"
},
{
"type": "color",
"id": "box_text",
"label": "Numbers",
"default": "#ffffff"
}
],
"presets": [
{
"name": "Countdown timer"
}
]
}
{% endschema %}
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.
Live preview
The three kinds of timer
| Timer | Counts down to | Good for |
|---|---|---|
| Fixed deadline | A date and time in the time zone you pick | Black Friday, launches, the end of a sale |
| Daily cutoff | Today's cutoff, or the next working day's | “Order in the next 2h 14m for same-day dispatch” |
| Per visitor | A number of minutes from the shopper's first view | A welcome code that really expires |
The deadline is worked out in the time zone you choose, including daylight saving changes, so a sale ending at 23:59 in New York ends at the same moment for a shopper in London.
Adding the timer
- 01
Add the section
Go to Online Store → Themes → … → Edit code, add a new section named
custom-countdown-timerand paste the code. - 02
Place it
In Customize, add Countdown timer to the home page, a product template or the Header group.
- 03
Edit the deadline later
The end date, time, time zone, text and colors are all settings, so you can reuse the section for the next sale without code.
Use urgency honestly
EU consumer law bans falsely stating that a product will only be available for a very limited time to rush a decision (Annex I, point 7 of the Unfair Commercial Practices Directive). A timer that resets when it runs out, or a “sale” that never ends, is exactly that.
- Fixed deadlines should match the real end of the offer. Remove the discount when the timer ends.
- Per-visitor timers should only back an offer that really expires for that visitor. This one remembers the start time in the browser and doesn't restart.
- Daily cutoffs don't know about public holidays. Hide the section on days you don't ship.
- Reduced motion. Shoppers who ask for reduced motion see hours and minutes only, updated every 30 seconds.
Questions people ask
Is this countdown timer really free?
Yes. It's a theme section you paste into your theme, so there's no app, no subscription and no external script.
What happens when the timer ends?
Choose Hide the section or Show a message such as “This offer has ended.” In the theme editor it stays visible so you can still edit it.
Does the timer end the sale automatically?
No. It only displays the countdown. Set your discount's end date in Discounts to the same moment.
Can shoppers reset a per-visitor timer?
Clearing browser storage or switching devices starts a new timer. That's fine for a soft welcome offer, but don't use it where the deadline must be enforced.
Related tools
Last reviewed 30 September 2026.
Sources: Shopify: Section schema · Shopify: Input settings · Shopify: Integrate sections with the theme editor · Directive 2005/29/EC (Unfair Commercial Practices), Annex I
