Open demo

CSS code

HTML
<main class="ct ct-zone-theme"><div class="ct-panel" data-countdown data-deadline="2030-06-21T17:00:00Z" data-zone="Europe/Riga"><p class="ct-kicker">A shared moment / Different places</p><h1>One deadline, wherever you are.</h1><p>Set the instant in UTC. Change the display zone without changing when it happens.</p><div class="ct-units" role="timer" aria-live="off" aria-label="Time remaining"><div class="ct-unit"><b data-value="0">00</b><span>Days</span></div><div class="ct-unit"><b data-value="1">00</b><span>Hours</span></div><div class="ct-unit"><b data-value="2">00</b><span>Minutes</span></div><div class="ct-unit"><b data-value="3">00</b><span>Seconds</span></div></div><p class="ct-date" data-date></p><form class="ct-form" data-configure><label>Deadline (UTC)<input type="datetime-local" name="deadline" value="2030-06-21T17:00" required min="2020-01-01T00:00" max="2099-12-31T23:59"></label><label>Display time zone<select name="zone"><option>Europe/Riga</option><option>UTC</option><option>America/New_York</option><option>Asia/Kolkata</option><option>Asia/Tokyo</option></select></label><button type="submit">Apply</button></form><p class="ct-status" data-status role="status"></p><p class="ct-note">Illustrative dates and content. Set your own deadline before using this example.</p></div></main>
CSS
.ct,.ct *{box-sizing:border-box}.ct [hidden]{display:none!important}.ct{font:17px/1.6 system-ui,-apple-system,'Segoe UI',sans-serif;color:#24323d;width:100%;max-width:850px;margin:auto;--accent:#235749;--paper:#fff;--line:#c9d4d0}.ct h1,.ct h2,.ct p{margin:0}.ct h1{font-size:clamp(30px,5vw,48px);line-height:1.13;letter-spacing:-.035em;margin-bottom:18px}.ct h2{font-size:23px;line-height:1.3;margin-bottom:12px}.ct p+p{margin-top:12px}.ct a{color:inherit;text-underline-offset:4px}.ct-kicker{font-size:14px;text-transform:uppercase;letter-spacing:.1em;font-weight:700;margin-bottom:14px!important}.ct-note{font-size:14px;margin-top:18px!important;color:#53636d}.ct-status{min-height:28px;margin-top:16px!important;font-size:16px}.ct :is(button,input,select){font:inherit;min-height:46px}.ct button{border:1px solid var(--accent);background:var(--accent);color:white;padding:9px 17px;border-radius:3px;cursor:pointer}.ct button.secondary{background:white;color:var(--accent)}.ct button:disabled{opacity:.6;cursor:not-allowed}.ct input,.ct select{width:100%;min-width:0;border:1px solid #7a8d96;background:white;color:#24323d;padding:8px 12px;border-radius:2px}.ct label{display:grid;gap:7px;font-weight:600}.ct :focus-visible{outline:3px solid #c77310;outline-offset:4px}.ct-units{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:25px}.ct-unit{min-width:0;padding:18px 8px;background:var(--paper);border:1px solid var(--line);text-align:center;color:#24323d}.ct-unit b{display:block;font-size:clamp(23px,4vw,42px);font-weight:600;line-height:1.2;font-variant-numeric:tabular-nums}.ct-unit span{display:block;margin-top:5px;font-size:14px}.ct-date{font-size:15px;margin-top:17px!important}.ct-panel{padding:38px;background:var(--paper);border:1px solid var(--line)}.ct-progress{width:100%;height:14px;accent-color:var(--accent);margin-top:30px}.ct-percentage{font-size:15px;margin-top:10px!important}.ct-events{display:flex;flex-wrap:wrap;gap:10px;margin:25px 0}.ct-events button[aria-pressed=true]{background:var(--accent);color:white}.ct-events button[aria-pressed=false]{background:white;color:var(--accent)}.ct-form{display:grid;grid-template-columns:1.4fr 1fr auto;gap:16px;align-items:end;margin-top:25px}.ct-registration fieldset{display:grid;grid-template-columns:1fr auto;align-items:end;gap:14px;border:0;padding:0;margin:25px 0 0;min-width:0}.ct-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:22px}.ct-duration{font-size:clamp(38px,8vw,66px);font-weight:600;font-variant-numeric:tabular-nums;line-height:1.2;letter-spacing:-.035em;margin:20px 0!important}.ct-timer{padding:25px;border:1px solid var(--line);background:#fff}.ct-timer label{max-width:210px}.ct-timers{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.ct-timers .ct-duration{font-size:32px}.ct-timers .ct-actions{gap:7px}.ct-timers button{padding:7px 10px;font-size:15px}.ct-timers .ct-status{font-size:14px}.ct [data-phase=ended] .ct-units{opacity:.68}.ct-ended-copy{display:none}.ct [data-phase=ended] .ct-ended-copy{display:block}.ct [data-phase=ended] .ct-upcoming-copy{display:none}.ct-banner{display:grid;grid-template-columns:1fr auto;gap:22px;background:#203a45;color:#fff;padding:28px;border-left:6px solid #d6bb7a}.ct-banner .ct-units{margin-top:18px;gap:10px;max-width:540px}.ct-banner .ct-unit{background:transparent;border-color:#8dabb6;color:white;padding:10px}.ct-banner .ct-unit b{font-size:27px}.ct-banner button{background:transparent;color:white;border-color:#8dabb6;min-width:46px;align-self:start}.ct-banner .ct-date{color:#e0e9ec}.ct-show-banner{margin-top:20px}.ct-progress-theme{--accent:#73512a;--paper:#fffdf8;--line:#d7cab5}.ct-progress-theme h1{font-family:Georgia,serif;font-weight:400}.ct-agenda-theme{--accent:#354c79;--paper:#fff;--line:#cbd3e1}.ct-zone-theme{--accent:#1e625f;--paper:#f8fcfa}.ct-registration-theme{--accent:#742d3f;--paper:#fffaf8;--line:#ddc4bd}.ct-registration-theme h1{font-family:Georgia,serif;font-weight:400}.ct-focus-theme{--accent:#343c5b;--paper:#f4f5fa}.ct-kitchen-theme{--accent:#315e47;--paper:#fafaf4}.ct-state-theme{--accent:#7a4b2f;--paper:#fffaf3}.ct-state-theme h1{font-family:Georgia,serif;font-weight:400}@media(max-width:650px){.ct-panel{padding:23px}.ct-units{gap:7px}.ct-unit{padding:13px 4px}.ct-unit b{font-size:21px}.ct-unit span{font-size:12px}.ct-form{grid-template-columns:1fr}.ct-registration fieldset{grid-template-columns:1fr}.ct-timers{grid-template-columns:1fr}.ct-banner{padding:22px;gap:12px}.ct-banner .ct-unit{padding:9px 3px}.ct-banner .ct-unit b{font-size:20px}.ct-banner .ct-units{grid-template-columns:repeat(2,minmax(0,1fr))}.ct-actions button{min-width:85px}.ct h1{font-size:32px}}@media(prefers-reduced-motion:reduce){.ct *{animation:none!important;transition:none!important}}
JavaScript
(() => {
  const format = seconds => {const s=Math.max(0,Math.ceil(seconds));return [Math.floor(s/86400),Math.floor(s/3600)%24,Math.floor(s/60)%60,s%60];};
  const valid = value => typeof value==='string' && /(?:Z|[+-]\d\d:\d\d)$/.test(value) && Number.isFinite(Date.parse(value));
  document.querySelectorAll('[data-countdown]').forEach(root => {
    let ended=false; const status=root.querySelector('[data-status]');
    function draw(){const raw=root.dataset.deadline;if(!valid(raw)){root.dataset.phase='invalid';const f=root.querySelector('[data-registration] fieldset');if(f)f.disabled=true;root.querySelectorAll('[data-value]').forEach(e=>e.textContent='—');status.textContent='Set a valid ISO deadline with Z or an explicit UTC offset.';return;}
      const target=Date.parse(raw),left=Math.max(0,target-Date.now()),values=format(left/1000);root.querySelectorAll('[data-value]').forEach(el=>el.textContent=String(values[Number(el.dataset.value)]).padStart(2,'0'));
      const timer=root.querySelector('[role=timer]');if(timer)timer.setAttribute('aria-label',values[0]+' days, '+values[1]+' hours, '+values[2]+' minutes, '+values[3]+' seconds remaining');
      const date=root.querySelector('[data-date]');if(date){let zone=root.dataset.zone||'UTC';try{date.textContent=new Intl.DateTimeFormat('en-GB',{dateStyle:'long',timeStyle:'short',timeZone:zone}).format(new Date(target))+' · '+zone;}catch{root.dataset.zone='UTC';date.textContent=new Date(target).toISOString()+' · UTC';}}
      const progress=root.querySelector('progress');if(progress){const start=Date.parse(root.dataset.start||'');if(!Number.isFinite(start)||start>=target){progress.value=0;root.querySelector('[data-percent]').textContent='Check the start and end dates.';}else{const percent=Math.max(0,Math.min(100,(Date.now()-start)/(target-start)*100));progress.value=percent;root.querySelector('[data-percent]').textContent=Math.round(percent)+'% of the example window elapsed';}}
      const form=root.querySelector('[data-registration]');if(form)form.querySelector('fieldset').disabled=left===0;
      root.dataset.phase=left===0?'ended':'upcoming';if(left===0&&!ended){status.textContent=root.dataset.endMessage||'The example deadline has passed.';ended=true;}else if(left>0&&ended){status.textContent='Countdown updated.';ended=false;}
    }
    root.querySelectorAll('[data-event-target]').forEach(button=>button.addEventListener('click',()=>{root.dataset.deadline=button.dataset.eventTarget;root.querySelector('[data-event-title]').textContent=button.textContent.trim();root.querySelectorAll('[data-event-target]').forEach(e=>e.setAttribute('aria-pressed',String(e===button)));status.textContent='Selected '+button.textContent.trim()+'.';ended=false;draw();}));
    const configure=root.querySelector('[data-configure]');if(configure)configure.addEventListener('submit',event=>{event.preventDefault();if(!configure.reportValidity())return;const value=configure.elements.deadline.value;if(!/^\d{4}-\d\d-\d\dT\d\d:\d\d$/.test(value)){status.textContent='Enter a complete UTC date and time.';return;}const iso=value+':00Z';if(!valid(iso)){status.textContent='Enter a valid UTC date and time.';return;}root.dataset.deadline=iso;root.dataset.zone=configure.elements.zone.value;status.textContent='Deadline updated. Only the display time zone changes.';ended=false;draw();});
    const close=root.querySelector('[data-dismiss]'),banner=root.querySelector('[data-banner]'),show=root.querySelector('[data-show-banner]');if(close&&banner&&show){close.addEventListener('click',()=>{banner.hidden=true;show.hidden=false;show.focus();});show.addEventListener('click',()=>{banner.hidden=false;show.hidden=true;close.focus();});}
    const registration=root.querySelector('[data-registration]');if(registration)registration.addEventListener('submit',event=>{event.preventDefault();draw();if(root.dataset.phase==='ended'||!registration.reportValidity())return;status.textContent='Demo validation passed. No email was sent or registration stored.';});
    root.querySelectorAll('[data-preview-state]').forEach(button=>button.addEventListener('click',()=>{root.dataset.deadline=new Date(Date.now()+(button.dataset.previewState==='ended'?-1000:3600000)).toISOString();status.textContent='Previewing the '+button.dataset.previewState+' state with a sample deadline.';ended=false;draw();}));
    draw();setInterval(draw,1000);document.addEventListener('visibilitychange',()=>{if(!document.hidden)draw();});
  });
  document.querySelectorAll('[data-duration-timer]').forEach(root=>{
    const input=root.querySelector('[data-minutes]'),readout=root.querySelector('[data-duration]'),start=root.querySelector('[data-start]'),pause=root.querySelector('[data-pause]'),reset=root.querySelector('[data-reset]'),status=root.querySelector('[data-duration-status]');let remaining=Number(input.value)*60000,deadline=0,running=false,finished=false;
    function draw(){if(running)remaining=Math.max(0,deadline-Date.now());const sec=Math.ceil(remaining/1000);readout.textContent=[Math.floor(sec/3600),Math.floor(sec/60)%60,sec%60].map(n=>String(n).padStart(2,'0')).join(':');if(running&&remaining===0){running=false;finished=true;status.textContent='Timer finished.';}start.disabled=running||finished;pause.disabled=!running;input.disabled=running;start.textContent=remaining<Number(input.value)*60000&&!finished?'Resume':'Start';}
    start.addEventListener('click',()=>{if(!input.reportValidity())return;if(!running&&!finished){deadline=Date.now()+remaining;running=true;status.textContent='Timer running.';draw();}});pause.addEventListener('click',()=>{if(running){remaining=Math.max(0,deadline-Date.now());running=false;status.textContent='Timer paused.';draw();}});function clear(){running=false;finished=false;remaining=Number(input.value)*60000;status.textContent='Timer reset.';draw();}reset.addEventListener('click',clear);input.addEventListener('change',()=>{if(input.checkValidity())clear();});draw();setInterval(draw,250);document.addEventListener('visibilitychange',()=>{if(!document.hidden)draw();});
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<main class="ct ct-zone-theme mx-auto"><div class="ct-panel" data-countdown data-deadline="2030-06-21T17:00:00Z" data-zone="Europe/Riga"><p class="ct-kicker text-uppercase">A shared moment / Different places</p><h1>One deadline, wherever you are.</h1><p>Set the instant in UTC. Change the display zone without changing when it happens.</p><div class="ct-units" role="timer" aria-live="off" aria-label="Time remaining"><div class="ct-unit"><b data-value="0">00</b><span>Days</span></div><div class="ct-unit"><b data-value="1">00</b><span>Hours</span></div><div class="ct-unit"><b data-value="2">00</b><span>Minutes</span></div><div class="ct-unit"><b data-value="3">00</b><span>Seconds</span></div></div><p class="ct-date" data-date></p><form class="ct-form" data-configure><label>Deadline (UTC)<input type="datetime-local" name="deadline" value="2030-06-21T17:00" required min="2020-01-01T00:00" max="2099-12-31T23:59"></label><label>Display time zone<select name="zone"><option>Europe/Riga</option><option>UTC</option><option>America/New_York</option><option>Asia/Kolkata</option><option>Asia/Tokyo</option></select></label><button type="submit">Apply</button></form><p class="ct-status" data-status role="status"></p><p class="ct-note">Illustrative dates and content. Set your own deadline before using this example.</p></div></main>
CSS
.ct,.ct *{box-sizing:border-box}.ct [hidden]{display:none!important}.ct{font:17px/1.6 system-ui,-apple-system,'Segoe UI',sans-serif;color:#24323d;width:100%;max-width:850px;margin:auto;--accent:#235749;--paper:#fff;--line:#c9d4d0}.ct h1,.ct h2,.ct p{margin:0}.ct h1{font-size:clamp(30px,5vw,48px);line-height:1.13;letter-spacing:-.035em;margin-bottom:18px}.ct h2{font-size:23px;line-height:1.3;margin-bottom:12px}.ct p+p{margin-top:12px}.ct a{color:inherit;text-underline-offset:4px}.ct-kicker{font-size:14px;text-transform:uppercase;letter-spacing:.1em;font-weight:700;margin-bottom:14px!important}.ct-note{font-size:14px;margin-top:18px!important;color:#53636d}.ct-status{min-height:28px;margin-top:16px!important;font-size:16px}.ct :is(button,input,select){font:inherit;min-height:46px}.ct button{border:1px solid var(--accent);background:var(--accent);color:white;padding:9px 17px;border-radius:3px;cursor:pointer}.ct button.secondary{background:white;color:var(--accent)}.ct button:disabled{opacity:.6;cursor:not-allowed}.ct input,.ct select{width:100%;min-width:0;border:1px solid #7a8d96;background:white;color:#24323d;padding:8px 12px;border-radius:2px}.ct label{display:grid;gap:7px;font-weight:600}.ct :focus-visible{outline:3px solid #c77310;outline-offset:4px}.ct-units{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:25px}.ct-unit{min-width:0;padding:18px 8px;background:var(--paper);border:1px solid var(--line);text-align:center;color:#24323d}.ct-unit b{display:block;font-size:clamp(23px,4vw,42px);font-weight:600;line-height:1.2;font-variant-numeric:tabular-nums}.ct-unit span{display:block;margin-top:5px;font-size:14px}.ct-date{font-size:15px;margin-top:17px!important}.ct-panel{padding:38px;background:var(--paper);border:1px solid var(--line)}.ct-progress{width:100%;height:14px;accent-color:var(--accent);margin-top:30px}.ct-percentage{font-size:15px;margin-top:10px!important}.ct-events{display:flex;flex-wrap:wrap;gap:10px;margin:25px 0}.ct-events button[aria-pressed=true]{background:var(--accent);color:white}.ct-events button[aria-pressed=false]{background:white;color:var(--accent)}.ct-form{display:grid;grid-template-columns:1.4fr 1fr auto;gap:16px;align-items:end;margin-top:25px}.ct-registration fieldset{display:grid;grid-template-columns:1fr auto;align-items:end;gap:14px;border:0;padding:0;margin:25px 0 0;min-width:0}.ct-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:22px}.ct-duration{font-size:clamp(38px,8vw,66px);font-weight:600;font-variant-numeric:tabular-nums;line-height:1.2;letter-spacing:-.035em;margin:20px 0!important}.ct-timer{padding:25px;border:1px solid var(--line);background:#fff}.ct-timer label{max-width:210px}.ct-timers{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.ct-timers .ct-duration{font-size:32px}.ct-timers .ct-actions{gap:7px}.ct-timers button{padding:7px 10px;font-size:15px}.ct-timers .ct-status{font-size:14px}.ct [data-phase=ended] .ct-units{opacity:.68}.ct-ended-copy{display:none}.ct [data-phase=ended] .ct-ended-copy{display:block}.ct [data-phase=ended] .ct-upcoming-copy{display:none}.ct-banner{display:grid;grid-template-columns:1fr auto;gap:22px;background:#203a45;color:#fff;padding:28px;border-left:6px solid #d6bb7a}.ct-banner .ct-units{margin-top:18px;gap:10px;max-width:540px}.ct-banner .ct-unit{background:transparent;border-color:#8dabb6;color:white;padding:10px}.ct-banner .ct-unit b{font-size:27px}.ct-banner button{background:transparent;color:white;border-color:#8dabb6;min-width:46px;align-self:start}.ct-banner .ct-date{color:#e0e9ec}.ct-show-banner{margin-top:20px}.ct-progress-theme{--accent:#73512a;--paper:#fffdf8;--line:#d7cab5}.ct-progress-theme h1{font-family:Georgia,serif;font-weight:400}.ct-agenda-theme{--accent:#354c79;--paper:#fff;--line:#cbd3e1}.ct-zone-theme{--accent:#1e625f;--paper:#f8fcfa}.ct-registration-theme{--accent:#742d3f;--paper:#fffaf8;--line:#ddc4bd}.ct-registration-theme h1{font-family:Georgia,serif;font-weight:400}.ct-focus-theme{--accent:#343c5b;--paper:#f4f5fa}.ct-kitchen-theme{--accent:#315e47;--paper:#fafaf4}.ct-state-theme{--accent:#7a4b2f;--paper:#fffaf3}.ct-state-theme h1{font-family:Georgia,serif;font-weight:400}@media(max-width:650px){.ct-panel{padding:23px}.ct-units{gap:7px}.ct-unit{padding:13px 4px}.ct-unit b{font-size:21px}.ct-unit span{font-size:12px}.ct-form{grid-template-columns:1fr}.ct-registration fieldset{grid-template-columns:1fr}.ct-timers{grid-template-columns:1fr}.ct-banner{padding:22px;gap:12px}.ct-banner .ct-unit{padding:9px 3px}.ct-banner .ct-unit b{font-size:20px}.ct-banner .ct-units{grid-template-columns:repeat(2,minmax(0,1fr))}.ct-actions button{min-width:85px}.ct h1{font-size:32px}}@media(prefers-reduced-motion:reduce){.ct *{animation:none!important;transition:none!important}}
JavaScript
(() => {
  const format = seconds => {const s=Math.max(0,Math.ceil(seconds));return [Math.floor(s/86400),Math.floor(s/3600)%24,Math.floor(s/60)%60,s%60];};
  const valid = value => typeof value==='string' && /(?:Z|[+-]\d\d:\d\d)$/.test(value) && Number.isFinite(Date.parse(value));
  document.querySelectorAll('[data-countdown]').forEach(root => {
    let ended=false; const status=root.querySelector('[data-status]');
    function draw(){const raw=root.dataset.deadline;if(!valid(raw)){root.dataset.phase='invalid';const f=root.querySelector('[data-registration] fieldset');if(f)f.disabled=true;root.querySelectorAll('[data-value]').forEach(e=>e.textContent='—');status.textContent='Set a valid ISO deadline with Z or an explicit UTC offset.';return;}
      const target=Date.parse(raw),left=Math.max(0,target-Date.now()),values=format(left/1000);root.querySelectorAll('[data-value]').forEach(el=>el.textContent=String(values[Number(el.dataset.value)]).padStart(2,'0'));
      const timer=root.querySelector('[role=timer]');if(timer)timer.setAttribute('aria-label',values[0]+' days, '+values[1]+' hours, '+values[2]+' minutes, '+values[3]+' seconds remaining');
      const date=root.querySelector('[data-date]');if(date){let zone=root.dataset.zone||'UTC';try{date.textContent=new Intl.DateTimeFormat('en-GB',{dateStyle:'long',timeStyle:'short',timeZone:zone}).format(new Date(target))+' · '+zone;}catch{root.dataset.zone='UTC';date.textContent=new Date(target).toISOString()+' · UTC';}}
      const progress=root.querySelector('progress');if(progress){const start=Date.parse(root.dataset.start||'');if(!Number.isFinite(start)||start>=target){progress.value=0;root.querySelector('[data-percent]').textContent='Check the start and end dates.';}else{const percent=Math.max(0,Math.min(100,(Date.now()-start)/(target-start)*100));progress.value=percent;root.querySelector('[data-percent]').textContent=Math.round(percent)+'% of the example window elapsed';}}
      const form=root.querySelector('[data-registration]');if(form)form.querySelector('fieldset').disabled=left===0;
      root.dataset.phase=left===0?'ended':'upcoming';if(left===0&&!ended){status.textContent=root.dataset.endMessage||'The example deadline has passed.';ended=true;}else if(left>0&&ended){status.textContent='Countdown updated.';ended=false;}
    }
    root.querySelectorAll('[data-event-target]').forEach(button=>button.addEventListener('click',()=>{root.dataset.deadline=button.dataset.eventTarget;root.querySelector('[data-event-title]').textContent=button.textContent.trim();root.querySelectorAll('[data-event-target]').forEach(e=>e.setAttribute('aria-pressed',String(e===button)));status.textContent='Selected '+button.textContent.trim()+'.';ended=false;draw();}));
    const configure=root.querySelector('[data-configure]');if(configure)configure.addEventListener('submit',event=>{event.preventDefault();if(!configure.reportValidity())return;const value=configure.elements.deadline.value;if(!/^\d{4}-\d\d-\d\dT\d\d:\d\d$/.test(value)){status.textContent='Enter a complete UTC date and time.';return;}const iso=value+':00Z';if(!valid(iso)){status.textContent='Enter a valid UTC date and time.';return;}root.dataset.deadline=iso;root.dataset.zone=configure.elements.zone.value;status.textContent='Deadline updated. Only the display time zone changes.';ended=false;draw();});
    const close=root.querySelector('[data-dismiss]'),banner=root.querySelector('[data-banner]'),show=root.querySelector('[data-show-banner]');if(close&&banner&&show){close.addEventListener('click',()=>{banner.hidden=true;show.hidden=false;show.focus();});show.addEventListener('click',()=>{banner.hidden=false;show.hidden=true;close.focus();});}
    const registration=root.querySelector('[data-registration]');if(registration)registration.addEventListener('submit',event=>{event.preventDefault();draw();if(root.dataset.phase==='ended'||!registration.reportValidity())return;status.textContent='Demo validation passed. No email was sent or registration stored.';});
    root.querySelectorAll('[data-preview-state]').forEach(button=>button.addEventListener('click',()=>{root.dataset.deadline=new Date(Date.now()+(button.dataset.previewState==='ended'?-1000:3600000)).toISOString();status.textContent='Previewing the '+button.dataset.previewState+' state with a sample deadline.';ended=false;draw();}));
    draw();setInterval(draw,1000);document.addEventListener('visibilitychange',()=>{if(!document.hidden)draw();});
  });
  document.querySelectorAll('[data-duration-timer]').forEach(root=>{
    const input=root.querySelector('[data-minutes]'),readout=root.querySelector('[data-duration]'),start=root.querySelector('[data-start]'),pause=root.querySelector('[data-pause]'),reset=root.querySelector('[data-reset]'),status=root.querySelector('[data-duration-status]');let remaining=Number(input.value)*60000,deadline=0,running=false,finished=false;
    function draw(){if(running)remaining=Math.max(0,deadline-Date.now());const sec=Math.ceil(remaining/1000);readout.textContent=[Math.floor(sec/3600),Math.floor(sec/60)%60,sec%60].map(n=>String(n).padStart(2,'0')).join(':');if(running&&remaining===0){running=false;finished=true;status.textContent='Timer finished.';}start.disabled=running||finished;pause.disabled=!running;input.disabled=running;start.textContent=remaining<Number(input.value)*60000&&!finished?'Resume':'Start';}
    start.addEventListener('click',()=>{if(!input.reportValidity())return;if(!running&&!finished){deadline=Date.now()+remaining;running=true;status.textContent='Timer running.';draw();}});pause.addEventListener('click',()=>{if(running){remaining=Math.max(0,deadline-Date.now());running=false;status.textContent='Timer paused.';draw();}});function clear(){running=false;finished=false;remaining=Number(input.value)*60000;status.textContent='Timer reset.';draw();}reset.addEventListener('click',clear);input.addEventListener('change',()=>{if(input.checkValidity())clear();});draw();setInterval(draw,250);document.addEventListener('visibilitychange',()=>{if(!document.hidden)draw();});
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<main class="ct ct-zone-theme mx-auto w-full font-sans"><div class="ct-panel" data-countdown data-deadline="2030-06-21T17:00:00Z" data-zone="Europe/Riga"><p class="ct-kicker uppercase tracking-widest">A shared moment / Different places</p><h1>One deadline, wherever you are.</h1><p>Set the instant in UTC. Change the display zone without changing when it happens.</p><div class="ct-units" role="timer" aria-live="off" aria-label="Time remaining"><div class="ct-unit"><b data-value="0">00</b><span>Days</span></div><div class="ct-unit"><b data-value="1">00</b><span>Hours</span></div><div class="ct-unit"><b data-value="2">00</b><span>Minutes</span></div><div class="ct-unit"><b data-value="3">00</b><span>Seconds</span></div></div><p class="ct-date" data-date></p><form class="ct-form" data-configure><label>Deadline (UTC)<input type="datetime-local" name="deadline" value="2030-06-21T17:00" required min="2020-01-01T00:00" max="2099-12-31T23:59"></label><label>Display time zone<select name="zone"><option>Europe/Riga</option><option>UTC</option><option>America/New_York</option><option>Asia/Kolkata</option><option>Asia/Tokyo</option></select></label><button type="submit">Apply</button></form><p class="ct-status" data-status role="status"></p><p class="ct-note">Illustrative dates and content. Set your own deadline before using this example.</p></div></main>
CSS
.ct,.ct *{box-sizing:border-box}.ct [hidden]{display:none!important}.ct{font:17px/1.6 system-ui,-apple-system,'Segoe UI',sans-serif;color:#24323d;width:100%;max-width:850px;margin:auto;--accent:#235749;--paper:#fff;--line:#c9d4d0}.ct h1,.ct h2,.ct p{margin:0}.ct h1{font-size:clamp(30px,5vw,48px);line-height:1.13;letter-spacing:-.035em;margin-bottom:18px}.ct h2{font-size:23px;line-height:1.3;margin-bottom:12px}.ct p+p{margin-top:12px}.ct a{color:inherit;text-underline-offset:4px}.ct-kicker{font-size:14px;text-transform:uppercase;letter-spacing:.1em;font-weight:700;margin-bottom:14px!important}.ct-note{font-size:14px;margin-top:18px!important;color:#53636d}.ct-status{min-height:28px;margin-top:16px!important;font-size:16px}.ct :is(button,input,select){font:inherit;min-height:46px}.ct button{border:1px solid var(--accent);background:var(--accent);color:white;padding:9px 17px;border-radius:3px;cursor:pointer}.ct button.secondary{background:white;color:var(--accent)}.ct button:disabled{opacity:.6;cursor:not-allowed}.ct input,.ct select{width:100%;min-width:0;border:1px solid #7a8d96;background:white;color:#24323d;padding:8px 12px;border-radius:2px}.ct label{display:grid;gap:7px;font-weight:600}.ct :focus-visible{outline:3px solid #c77310;outline-offset:4px}.ct-units{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:25px}.ct-unit{min-width:0;padding:18px 8px;background:var(--paper);border:1px solid var(--line);text-align:center;color:#24323d}.ct-unit b{display:block;font-size:clamp(23px,4vw,42px);font-weight:600;line-height:1.2;font-variant-numeric:tabular-nums}.ct-unit span{display:block;margin-top:5px;font-size:14px}.ct-date{font-size:15px;margin-top:17px!important}.ct-panel{padding:38px;background:var(--paper);border:1px solid var(--line)}.ct-progress{width:100%;height:14px;accent-color:var(--accent);margin-top:30px}.ct-percentage{font-size:15px;margin-top:10px!important}.ct-events{display:flex;flex-wrap:wrap;gap:10px;margin:25px 0}.ct-events button[aria-pressed=true]{background:var(--accent);color:white}.ct-events button[aria-pressed=false]{background:white;color:var(--accent)}.ct-form{display:grid;grid-template-columns:1.4fr 1fr auto;gap:16px;align-items:end;margin-top:25px}.ct-registration fieldset{display:grid;grid-template-columns:1fr auto;align-items:end;gap:14px;border:0;padding:0;margin:25px 0 0;min-width:0}.ct-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:22px}.ct-duration{font-size:clamp(38px,8vw,66px);font-weight:600;font-variant-numeric:tabular-nums;line-height:1.2;letter-spacing:-.035em;margin:20px 0!important}.ct-timer{padding:25px;border:1px solid var(--line);background:#fff}.ct-timer label{max-width:210px}.ct-timers{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.ct-timers .ct-duration{font-size:32px}.ct-timers .ct-actions{gap:7px}.ct-timers button{padding:7px 10px;font-size:15px}.ct-timers .ct-status{font-size:14px}.ct [data-phase=ended] .ct-units{opacity:.68}.ct-ended-copy{display:none}.ct [data-phase=ended] .ct-ended-copy{display:block}.ct [data-phase=ended] .ct-upcoming-copy{display:none}.ct-banner{display:grid;grid-template-columns:1fr auto;gap:22px;background:#203a45;color:#fff;padding:28px;border-left:6px solid #d6bb7a}.ct-banner .ct-units{margin-top:18px;gap:10px;max-width:540px}.ct-banner .ct-unit{background:transparent;border-color:#8dabb6;color:white;padding:10px}.ct-banner .ct-unit b{font-size:27px}.ct-banner button{background:transparent;color:white;border-color:#8dabb6;min-width:46px;align-self:start}.ct-banner .ct-date{color:#e0e9ec}.ct-show-banner{margin-top:20px}.ct-progress-theme{--accent:#73512a;--paper:#fffdf8;--line:#d7cab5}.ct-progress-theme h1{font-family:Georgia,serif;font-weight:400}.ct-agenda-theme{--accent:#354c79;--paper:#fff;--line:#cbd3e1}.ct-zone-theme{--accent:#1e625f;--paper:#f8fcfa}.ct-registration-theme{--accent:#742d3f;--paper:#fffaf8;--line:#ddc4bd}.ct-registration-theme h1{font-family:Georgia,serif;font-weight:400}.ct-focus-theme{--accent:#343c5b;--paper:#f4f5fa}.ct-kitchen-theme{--accent:#315e47;--paper:#fafaf4}.ct-state-theme{--accent:#7a4b2f;--paper:#fffaf3}.ct-state-theme h1{font-family:Georgia,serif;font-weight:400}@media(max-width:650px){.ct-panel{padding:23px}.ct-units{gap:7px}.ct-unit{padding:13px 4px}.ct-unit b{font-size:21px}.ct-unit span{font-size:12px}.ct-form{grid-template-columns:1fr}.ct-registration fieldset{grid-template-columns:1fr}.ct-timers{grid-template-columns:1fr}.ct-banner{padding:22px;gap:12px}.ct-banner .ct-unit{padding:9px 3px}.ct-banner .ct-unit b{font-size:20px}.ct-banner .ct-units{grid-template-columns:repeat(2,minmax(0,1fr))}.ct-actions button{min-width:85px}.ct h1{font-size:32px}}@media(prefers-reduced-motion:reduce){.ct *{animation:none!important;transition:none!important}}
JavaScript
(() => {
  const format = seconds => {const s=Math.max(0,Math.ceil(seconds));return [Math.floor(s/86400),Math.floor(s/3600)%24,Math.floor(s/60)%60,s%60];};
  const valid = value => typeof value==='string' && /(?:Z|[+-]\d\d:\d\d)$/.test(value) && Number.isFinite(Date.parse(value));
  document.querySelectorAll('[data-countdown]').forEach(root => {
    let ended=false; const status=root.querySelector('[data-status]');
    function draw(){const raw=root.dataset.deadline;if(!valid(raw)){root.dataset.phase='invalid';const f=root.querySelector('[data-registration] fieldset');if(f)f.disabled=true;root.querySelectorAll('[data-value]').forEach(e=>e.textContent='—');status.textContent='Set a valid ISO deadline with Z or an explicit UTC offset.';return;}
      const target=Date.parse(raw),left=Math.max(0,target-Date.now()),values=format(left/1000);root.querySelectorAll('[data-value]').forEach(el=>el.textContent=String(values[Number(el.dataset.value)]).padStart(2,'0'));
      const timer=root.querySelector('[role=timer]');if(timer)timer.setAttribute('aria-label',values[0]+' days, '+values[1]+' hours, '+values[2]+' minutes, '+values[3]+' seconds remaining');
      const date=root.querySelector('[data-date]');if(date){let zone=root.dataset.zone||'UTC';try{date.textContent=new Intl.DateTimeFormat('en-GB',{dateStyle:'long',timeStyle:'short',timeZone:zone}).format(new Date(target))+' · '+zone;}catch{root.dataset.zone='UTC';date.textContent=new Date(target).toISOString()+' · UTC';}}
      const progress=root.querySelector('progress');if(progress){const start=Date.parse(root.dataset.start||'');if(!Number.isFinite(start)||start>=target){progress.value=0;root.querySelector('[data-percent]').textContent='Check the start and end dates.';}else{const percent=Math.max(0,Math.min(100,(Date.now()-start)/(target-start)*100));progress.value=percent;root.querySelector('[data-percent]').textContent=Math.round(percent)+'% of the example window elapsed';}}
      const form=root.querySelector('[data-registration]');if(form)form.querySelector('fieldset').disabled=left===0;
      root.dataset.phase=left===0?'ended':'upcoming';if(left===0&&!ended){status.textContent=root.dataset.endMessage||'The example deadline has passed.';ended=true;}else if(left>0&&ended){status.textContent='Countdown updated.';ended=false;}
    }
    root.querySelectorAll('[data-event-target]').forEach(button=>button.addEventListener('click',()=>{root.dataset.deadline=button.dataset.eventTarget;root.querySelector('[data-event-title]').textContent=button.textContent.trim();root.querySelectorAll('[data-event-target]').forEach(e=>e.setAttribute('aria-pressed',String(e===button)));status.textContent='Selected '+button.textContent.trim()+'.';ended=false;draw();}));
    const configure=root.querySelector('[data-configure]');if(configure)configure.addEventListener('submit',event=>{event.preventDefault();if(!configure.reportValidity())return;const value=configure.elements.deadline.value;if(!/^\d{4}-\d\d-\d\dT\d\d:\d\d$/.test(value)){status.textContent='Enter a complete UTC date and time.';return;}const iso=value+':00Z';if(!valid(iso)){status.textContent='Enter a valid UTC date and time.';return;}root.dataset.deadline=iso;root.dataset.zone=configure.elements.zone.value;status.textContent='Deadline updated. Only the display time zone changes.';ended=false;draw();});
    const close=root.querySelector('[data-dismiss]'),banner=root.querySelector('[data-banner]'),show=root.querySelector('[data-show-banner]');if(close&&banner&&show){close.addEventListener('click',()=>{banner.hidden=true;show.hidden=false;show.focus();});show.addEventListener('click',()=>{banner.hidden=false;show.hidden=true;close.focus();});}
    const registration=root.querySelector('[data-registration]');if(registration)registration.addEventListener('submit',event=>{event.preventDefault();draw();if(root.dataset.phase==='ended'||!registration.reportValidity())return;status.textContent='Demo validation passed. No email was sent or registration stored.';});
    root.querySelectorAll('[data-preview-state]').forEach(button=>button.addEventListener('click',()=>{root.dataset.deadline=new Date(Date.now()+(button.dataset.previewState==='ended'?-1000:3600000)).toISOString();status.textContent='Previewing the '+button.dataset.previewState+' state with a sample deadline.';ended=false;draw();}));
    draw();setInterval(draw,1000);document.addEventListener('visibilitychange',()=>{if(!document.hidden)draw();});
  });
  document.querySelectorAll('[data-duration-timer]').forEach(root=>{
    const input=root.querySelector('[data-minutes]'),readout=root.querySelector('[data-duration]'),start=root.querySelector('[data-start]'),pause=root.querySelector('[data-pause]'),reset=root.querySelector('[data-reset]'),status=root.querySelector('[data-duration-status]');let remaining=Number(input.value)*60000,deadline=0,running=false,finished=false;
    function draw(){if(running)remaining=Math.max(0,deadline-Date.now());const sec=Math.ceil(remaining/1000);readout.textContent=[Math.floor(sec/3600),Math.floor(sec/60)%60,sec%60].map(n=>String(n).padStart(2,'0')).join(':');if(running&&remaining===0){running=false;finished=true;status.textContent='Timer finished.';}start.disabled=running||finished;pause.disabled=!running;input.disabled=running;start.textContent=remaining<Number(input.value)*60000&&!finished?'Resume':'Start';}
    start.addEventListener('click',()=>{if(!input.reportValidity())return;if(!running&&!finished){deadline=Date.now()+remaining;running=true;status.textContent='Timer running.';draw();}});pause.addEventListener('click',()=>{if(running){remaining=Math.max(0,deadline-Date.now());running=false;status.textContent='Timer paused.';draw();}});function clear(){running=false;finished=false;remaining=Number(input.value)*60000;status.textContent='Timer reset.';draw();}reset.addEventListener('click',clear);input.addEventListener('change',()=>{if(input.checkValidity())clear();});draw();setInterval(draw,250);document.addEventListener('visibilitychange',()=>{if(!document.hidden)draw();});
  });
})();

About this set

A UTC date editor and IANA time-zone selector make the distinction between a deadline instant and its local display explicit. Intl.DateTimeFormat handles local formatting and daylight-saving offsets.

What’s included

  • Explicit deadline or duration configuration
  • Clock-derived remaining time; no interval drift accumulation
  • Responsive controls and keyboard focus
  • Expiry and reset behavior covered by tests

Accessibility

Ticking values use role="timer" with aria-live="off". Status messages announce user actions and completion, not every second. Controls have visible labels and focus outlines. No motion is required.

Customise it

Edit the data-deadline/data-start ISO values, or the default duration input. Change the scoped .ct theme variables. The Bootstrap and Tailwind variants share scoped component CSS and vanilla logic with framework utility markup.