Limited-Time Discount Banner — Free Live Countdown Promo Banner (HTML/CSS/JS)

Limited-Time Discount Banner · Pricing · Plain HTML, CSS & JS · Live preview

CategoryPricing

About this UI Snippet

Limited-Time Discount Banner — A Countdown That Actually Counts Down From a Real Deadline

Screenshot of the Limited-Time Discount Banner snippet rendered live

A "23:59:59" countdown that never changes on page reload is a giveaway that the urgency is fake — this snippet builds a discount banner whose countdown is computed live from a real target end time, so the numbers on screen are always the true time remaining, refreshed every second.

A real target timestamp, not a fake string

The discount's expiry is a genuine Date object, endTime, computed once when the script runs. The demo sets it to Date.now() plus a fixed offset so the countdown always has something to show when you paste the snippet — in production you'd replace that with a fixed ISO timestamp like new Date('2026-09-01T00:00:00Z'), and the exact same tick() function keeps working, because it only ever computes *the difference* between now and that stored deadline.

Recomputed from scratch every second

Rather than decrementing a counter variable (which drifts if a tab is backgrounded and timers get throttled), tick() recalculates msLeft = endTime.getTime() - Date.now() fresh on every interval — then derives days, hours, minutes, and seconds from that fresh millisecond difference with integer division and modulo. Reload the page five minutes later and the countdown picks up exactly five minutes shorter, because it's always deriving from the real clock, never accumulating drift.

Correct unit math

totalSeconds divided by 86,400 (seconds in a day) gives whole days; the remainder modulo 3,600 gives the leftover hours; that remainder modulo 60 gives minutes; and the final modulo 60 gives seconds — the standard largest-unit-first breakdown that keeps every unit within its natural range (hours never shows 25, minutes never shows 61).

A real stop condition

When msLeft reaches zero or goes negative, tick() clears the interval with clearInterval, zeroes every displayed unit, and disables the CTA button with an "Offer expired" label — so the banner doesn't keep silently ticking into negative numbers or leave a live claim button active past the real deadline.

Real struck-through pricing

$29/mo is shown with text-decoration: line-through next to $17.40/mo — genuinely 40% off ($29 × 0.6 = $17.40), computed by hand and verified rather than an arbitrary discounted-looking number.

Where it fits

Pair it with a trial countdown for expiring trials, a coupon card for a code-based version, or a promo code input so the discount can actually be redeemed at checkout.

Customizing it

Swap in your real end date, discount percentage, and copy. Persist the end time in localStorage keyed per visitor if you want a personalized (rather than shared) countdown, or add a callback that hides the banner entirely once it expires instead of showing a disabled state.

What's included

Features

Real target timestamp
A genuine Date object drives the countdown, not a static string.
Drift-free recomputation
Every tick derives from the real clock, never decrements in place.
Correct unit breakdown
Days/hours/minutes/seconds via integer division and modulo.
Real expiry handling
Interval clears and the CTA disables once time actually runs out.
Verified discount math
$29 × 0.6 = $17.40 — a real, checkable 40% off.
Zero-padded digits
padStart keeps every unit at two digits for a stable layout.
Struck-through original price
A clear before/after price comparison, not just a percentage.
Framework-agnostic core
tick() is pure and ports directly to any component's effects.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSThe countdown starts immediately from a real end timestamp.
  2. 2
    Watch the seconds tickEvery unit recomputes from Date.now() vs. endTime each second.
  3. 3
    Reload the pageThe countdown continues from the correct real remaining time.
  4. 4
    Verify the discount math$29/mo × 0.6 = $17.40/mo — a genuine 40% off.
  5. 5
    Let it hit zeroThe CTA disables and reads "Offer expired" once time runs out.
  6. 6
    Wire up a real deadlineReplace endTime with a fixed ISO Date for your actual promotion.

Real-world uses

Common Use Cases

Flash sales
Drive urgency on a time-boxed promotion with a struck-through price and a countdown computed live from a real expiry Date object.
Trial-to-paid conversion
Pair with a trial countdown near the upgrade moment, deriving every tick from the real clock so it never drifts.
Coupon campaigns
Show alongside a coupon card or promo code input, with an interval that clears and a disabled call-to-action once time truly runs out.
Seasonal pricing pages
Anchor a holiday or end-of-quarter offer, using integer division and modulo to break time into days, hours, minutes and seconds.
Win-back and launch week offers
Give a lapsed customer a real deadline on a landing page, or discount early adopters in launch week with a countdown that survives page reloads.
Related: Cost Per User Breakdown
See the Cost Per User Breakdown for a related pricing pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

It's genuinely live. endTime is a real Date object, and every second tick() recomputes msLeft as endTime.getTime() minus the current Date.now(), then re-derives days, hours, minutes, and seconds from that fresh difference. Reloading the page or leaving it open for a while both show the correct real remaining time, not a replayed animation.

A counter that decrements by one every "second" drifts if the browser throttles a background tab's timers or if a tick is delayed, so the display can end up wrong. Recomputing endTime minus Date.now() from scratch every tick means the displayed time is always exactly correct relative to the real target, regardless of any timer drift.

The original price is $29/mo and the discount is 40%, so the discounted price is $29 × (1 − 0.40) = $29 × 0.6 = $17.40/mo — exactly what the banner displays. Both the percentage and the resulting price are consistent and independently checkable.

tick() detects msLeft <= 0, sets every displayed unit to "00", changes the CTA button's text to "Offer expired" and disables it, and calls clearInterval so the timer stops running entirely rather than continuing to tick into negative numbers.

Store endTime as a ref or a piece of state set once on mount, and run the same recompute-from-scratch tick logic inside a setInterval created in a mount effect, clearing it on unmount. Render the four unit values from state updated each tick — the date-math functions themselves need no DOM access and copy over unchanged.

Source Code

<div class="dcb-banner">
  <div class="dcb-left">
    <span class="dcb-tag">Limited-time offer</span>
    <h3>40% off Pro, this week only</h3>
    <div class="dcb-prices">
      <span class="dcb-old">$29/mo</span>
      <span class="dcb-new">$17.40/mo</span>
    </div>
  </div>
  <div class="dcb-right">
    <div class="dcb-countdown" id="dcbCountdown">
      <div class="dcb-unit"><b id="dcbDays">00</b><small>days</small></div>
      <div class="dcb-unit"><b id="dcbHours">00</b><small>hrs</small></div>
      <div class="dcb-unit"><b id="dcbMins">00</b><small>min</small></div>
      <div class="dcb-unit"><b id="dcbSecs">00</b><small>sec</small></div>
    </div>
    <button type="button" class="dcb-cta" id="dcbCta">Claim discount</button>
  </div>
</div>

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to re-derive the countdown math by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why tick() recomputes msLeft as endTime.getTime() minus Date.now() on every single interval firing rather than decrementing a stored seconds-remaining counter, and how that recompute-from-scratch approach avoids the timer drift that affects countdowns built by simply subtracting one each tick — especially in a backgrounded browser tab where intervals get throttled. The same assistant can help you verify the unit math (days via division by 86400, then successive modulo operations for hours, minutes, and seconds) and extend the widget: ask how to persist a personalized end time per visitor in localStorage, how to hide the banner entirely (instead of showing a disabled CTA) once the offer expires, or how to add a warning color state when under an hour remains. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "limited-time discount banner" with a live countdown timer in plain HTML, CSS, and JavaScript with no framework or library.

Requirements:
- Show a struck-through original price next to a discounted price, where the discounted price is genuinely computed from the original price and a stated discount percentage (e.g. original × (1 − discount)) — verify the arithmetic is correct, not just visually plausible.
- Set a real target end Date object for when the discount expires (in the demo this can be "now plus a fixed offset" so it always has time left when loaded; note in a comment that production code should use a fixed ISO date instead).
- Build the countdown display (days, hours, minutes, seconds) by recomputing the full remaining time from scratch every second — take the real millisecond difference between the target Date and the current time via Date.now(), and derive each display unit from that fresh difference using integer division and modulo — do NOT decrement a stored counter variable by one each tick, since that approach drifts if timers are throttled or delayed.
- Derive days from the total remaining seconds divided by 86400, hours from the remainder of that division modulo 3600 then divided by 3600, minutes from the next remainder modulo 60, and seconds from the final modulo 60 — so each unit stays within its natural range.
- When the remaining time reaches zero or goes negative, stop the interval entirely, zero out every displayed unit, and change the call-to-action button to a disabled "expired" state — the countdown must not continue ticking into negative numbers.
- Zero-pad every displayed number to two digits so the layout doesn't shift as digits change.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.