Open demo

CSS code

HTML
<main class="ct ct-state-theme"><div class="ct-panel" data-countdown data-deadline="2030-06-21T17:00:00Z" data-end-message="The example deadline has passed. Follow the next-step link."><p class="ct-kicker">Exhibition / State preview</p><div class="ct-upcoming-copy"><h1>There’s still time to take a look.</h1><p>The upcoming state shows a clear deadline and remaining time.</p></div><div class="ct-ended-copy"><h1>This chapter has closed.</h1><p>The expired state replaces the invitation with a useful next step.</p><a href="#state-controls">Explore the state controls below</a></div><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><div class="ct-actions" id="state-controls" role="group" aria-label="Preview example states"><button type="button" data-preview-state="upcoming">Preview upcoming</button><button type="button" class="secondary" data-preview-state="ended">Preview expired</button></div><p class="ct-status" data-status role="status"></p><p class="ct-note">State preview buttons use a temporary sample deadline. They are demonstration controls, not a real offer or event.</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-state-theme mx-auto"><div class="ct-panel" data-countdown data-deadline="2030-06-21T17:00:00Z" data-end-message="The example deadline has passed. Follow the next-step link."><p class="ct-kicker text-uppercase">Exhibition / State preview</p><div class="ct-upcoming-copy"><h1>There’s still time to take a look.</h1><p>The upcoming state shows a clear deadline and remaining time.</p></div><div class="ct-ended-copy"><h1>This chapter has closed.</h1><p>The expired state replaces the invitation with a useful next step.</p><a href="#state-controls">Explore the state controls below</a></div><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><div class="ct-actions d-flex flex-wrap" id="state-controls" role="group" aria-label="Preview example states"><button type="button" data-preview-state="upcoming">Preview upcoming</button><button type="button" class="secondary" data-preview-state="ended">Preview expired</button></div><p class="ct-status" data-status role="status"></p><p class="ct-note">State preview buttons use a temporary sample deadline. They are demonstration controls, not a real offer or event.</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-state-theme mx-auto w-full font-sans"><div class="ct-panel" data-countdown data-deadline="2030-06-21T17:00:00Z" data-end-message="The example deadline has passed. Follow the next-step link."><p class="ct-kicker uppercase tracking-widest">Exhibition / State preview</p><div class="ct-upcoming-copy"><h1>There’s still time to take a look.</h1><p>The upcoming state shows a clear deadline and remaining time.</p></div><div class="ct-ended-copy"><h1>This chapter has closed.</h1><p>The expired state replaces the invitation with a useful next step.</p><a href="#state-controls">Explore the state controls below</a></div><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><div class="ct-actions flex flex-wrap" id="state-controls" role="group" aria-label="Preview example states"><button type="button" data-preview-state="upcoming">Preview upcoming</button><button type="button" class="secondary" data-preview-state="ended">Preview expired</button></div><p class="ct-status" data-status role="status"></p><p class="ct-note">State preview buttons use a temporary sample deadline. They are demonstration controls, not a real offer or event.</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

An event card with explicit upcoming and expired content. Preview controls exercise both states without changing the wall clock. Expiry is clamped at zero and gives the visitor a next step instead of a negative counter.

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.