How to add a countdown timer to Webflow

Two approaches, and the simpler one is right most of the time. An Embed element takes the HTML the API already returns and needs no key, no proxy, and no maintenance. Custom code gives you a countdown styled entirely in the Webflow Designer, at the cost of a small serverless function to keep your API key off the page. Both are below.

Option one: the Embed element

A published timer returns ready-made iframe HTML. Copy it from GET /timers/{id}/outputs, or from the timer in your developer dashboard, and paste it into a Webflow Embed element.

Paste into an Embed element

<!-- Paste into a Webflow Embed element.
     This is the website_embed_html the API returns for a
     published timer. Nothing else is needed. -->
<div style="width:100%;max-width:300px">
  <div style="position:relative;width:100%;aspect-ratio:6 / 5">
    <iframe src="https://countdownshare.com/api/embed/c/550e8400-..."
            title="Spring sale ends"
            width="300" height="250"
            style="position:absolute;inset:0;width:100%;height:100%;border:0"
            scrolling="no" loading="lazy"></iframe>
  </div>
</div>

That is the whole integration. The iframe renders our page, which reads the server clock — so it is immune to a visitor with a wrong system clock, it survives a refresh, and it reflects a paused or extended timer without you republishing anything.

The wrapper div with aspect-ratio is what makes it responsive. Drop it and the iframe stays a fixed 300×250 box. Keep both wrappers and it scales to whatever width the Webflow column gives it.

The trade-off is styling: the countdown's appearance comes from the timer, not from your Designer settings. If it needs to match your type system exactly, use option two.

Option two, part one: the proxy

Webflow has no server, so it cannot hold a secret. A small serverless function does — one file on Vercel, Netlify, or Cloudflare Workers.

api/countdown.js

// A serverless function on Vercel, Netlify, or Cloudflare.
// Webflow cannot hold a secret, so this is where the key lives.
export default async function handler(request, response) {
  // Restrict to your own site — this endpoint is public.
  const origin = request.headers.origin;
  const allowed = ["https://yoursite.com", "https://yoursite.webflow.io"];

  if (allowed.includes(origin)) {
    response.setHeader("Access-Control-Allow-Origin", origin);
  }

  const timerId = request.query.id;
  if (!/^[0-9a-f-]{36}$/i.test(timerId ?? "")) {
    return response.status(400).json({ error: "bad id" });
  }

  const upstream = await fetch(
    `https://countdownshare.com/api/v1/timers/${timerId}/status`,
    { headers: { Authorization: `Bearer ${process.env.COUNTDOWNSHARE_API_KEY}` } },
  );

  if (!upstream.ok) return response.status(502).json({ error: "unavailable" });

  const { data } = await upstream.json();

  // Short cache: enough to absorb a traffic spike, short enough
  // that the countdown is never visibly wrong.
  response.setHeader("Cache-Control", "public, max-age=5, s-maxage=5");
  response.json({
    remaining_seconds: data.remaining.total_seconds,
    server_time: data.server_time,
    ended: data.ended,
  });
}

Three things this does deliberately

It restricts CORS to your own origins, because the endpoint is public and there is no reason for anyone else's site to use it. It validates the ID before making a request, so a malformed parameter is rejected cheaply. And it returns three fields rather than the whole payload — the page only needs what it will display.

The five-second cache is a deliberate compromise. It absorbs a traffic spike without the countdown ever being visibly wrong, since five seconds of staleness is invisible on a clock ticking in whole seconds.

Option two, part two: the countdown script

In the Designer, add a Text Block and give it a custom attribute pointing at the timer.

Designer setup

<!-- In the Webflow Designer, add a Text Block and give it
     a custom attribute:

       Name:  data-countdown
       Value: 550e8400-e29b-41d4-a716-446655440000

     Optionally add data-ended-text too. Style it however you
     like — the script only writes textContent, so all your
     Webflow typography and colour settings survive. -->

Then paste the script into Page Settings, before the closing body tag.

Before </body> tag

<!-- Webflow: Page Settings -> Before </body> tag -->
<script>
(function () {
  var el = document.querySelector("[data-countdown]");
  if (!el) return;

  var timerId = el.getAttribute("data-countdown");
  var skew = 0;
  var endsAt = 0;

  function paint() {
    var left = Math.max(0, Math.round((endsAt - (Date.now() - skew)) / 1000));

    if (left === 0) {
      el.textContent = el.getAttribute("data-ended-text") || "This offer has closed";
      return;
    }

    var d = Math.floor(left / 86400);
    var h = Math.floor((left % 86400) / 3600);
    var m = Math.floor((left % 3600) / 60);
    var s = left % 60;

    el.textContent =
      (d ? d + "d " : "") +
      String(h).padStart(2, "0") + ":" +
      String(m).padStart(2, "0") + ":" +
      String(s).padStart(2, "0");
  }

  function sync() {
    fetch("https://your-proxy.vercel.app/api/countdown?id=" + timerId)
      .then(function (r) { return r.json(); })
      .then(function (data) {
        // Anchor to the SERVER's clock, not the visitor's.
        skew = Date.now() - new Date(data.server_time).getTime();
        endsAt = Date.now() - skew + data.remaining_seconds * 1000;
        paint();
      })
      .catch(function () { /* leave the last good value on screen */ });
  }

  sync();
  setInterval(paint, 1000);
  // A backgrounded tab is throttled; re-read when it returns.
  document.addEventListener("visibilitychange", function () {
    if (!document.hidden) sync();
  });
})();
</script>

Why it anchors to server_time

The obvious version subtracts a hardcoded deadline from Date.now(), which is the visitor's clock. Measuring the offset against server_time once and subtracting it on every paint means a device that is an hour fast still shows the correct remaining time — which matters as soon as the countdown gates a discount.

The visibilitychange listener handles the other common failure: browsers throttle timers in hidden tabs to roughly once a minute, so a countdown left in a background tab is visibly stale when someone returns to it.

Creating timers automatically

Both options above assume the timer already exists. If Webflow forms or CMS items should create timers — a per-signup deadline, say — that also needs to happen server-side. A Webflow form webhook can call the same serverless function, which creates the timer with an idempotency key derived from the submission.

For anything more involved than that, Make and Zapier or n8n can sit between Webflow and the API without you maintaining a function at all.

Common questions

Can I call the API directly from Webflow custom code?

No. Anything in page JavaScript is visible in the network tab, so the key would be public immediately. Either use the Embed element, which needs no key at all, or route through a small serverless proxy as shown here.

Which approach should I use?

The Embed element unless you have a specific reason not to. It needs no proxy, no key, no maintenance, and it already handles clock drift. Reach for custom code only when the countdown has to match your Webflow typography exactly or sit inline in a sentence.

Does the embed work on the free .webflow.io domain?

Yes. It is a plain iframe, so it works anywhere HTML does — the staging domain, a custom domain, or a page exported from Webflow.

Will the custom-code version break Webflow interactions?

No. It only writes textContent to an element you designate, so Webflow interactions, styles, and animations on that element are untouched. Avoid replacing innerHTML, which would destroy any child elements Webflow expects to find.

Next steps

Start with the Embed element

Create a timer, publish it, copy the embed HTML. If it needs to match your Designer styles exactly, the proxy takes about ten minutes.