Shopify Countdown Timer Generator

Add a sale deadline, daily shipping cutoff or per-visitor offer timer to Shopify as a section, with no app.

Describe the result you need. AI will set up the tool and ask for any missing details.

Configure with AI

Enter to submit · Shift + Enter for a new line

Sent to AI. Leave out passwords and private customer data.

Your details

Timer

The deadline is the same moment for every shopper, wherever they are.

Text

Four labels, comma separated. Translate them here for other languages.

Colors

Your section file (sections/custom-countdown-timer.liquid)

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

TimerCounts down toGood for
Fixed deadlineA date and time in the time zone you pickBlack Friday, launches, the end of a sale
Daily cutoffToday's cutoff, or the next working day's“Order in the next 2h 14m for same-day dispatch”
Per visitorA number of minutes from the shopper's first viewA 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

  1. 01

    Add the section

    Go to Online Store → Themes → … → Edit code, add a new section named custom-countdown-timer and paste the code.

  2. 02

    Place it

    In Customize, add Countdown timer to the home page, a product template or the Header group.

  3. 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.

Want this builtinto your store?

I'm Emmanuel, a Shopify and WooCommerce developer. If this tool pointed at something you'd rather have fixed for you (a faster store, cleaner product data, a migration, a custom feature), send me a note.

Emmanuel Asika

Emmanuel Asika

Shopify & WooCommerce developer, Dublin

I build and fix online stores: new builds, migrations, speed, SEO and the custom bits apps can't do.

hello@emmanuelasika.com