22 CSS Countdown Timers18 / 22

Light JSMIT licensed

Sticky Announcement Bar Countdown

A site-wide sticky bar where the timer, not the message, is the dominant element: oversized tabular digits, one short line of offer copy, a compact CTA and a real dismiss control. It sticks to the top of the scroll container, collapses to a two-line layout on narrow screens and states its own end condition.

Published Updated

Live Demo
Try it

The code

<section class="cdt-18" aria-labelledby="cdt-18-title">
  <input class="cdt-18__tgl" type="checkbox" id="cdt-18-theme">
  <label class="cdt-18__tglbtn" for="cdt-18-theme">
    <svg class="cdt-18__moon" viewBox="0 0 20 20" width="15" height="15" aria-hidden="true"><path d="M15.7 12.8A6.5 6.5 0 0 1 7.2 4.3a6.6 6.6 0 1 0 8.5 8.5Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
    <svg class="cdt-18__sun" viewBox="0 0 20 20" width="15" height="15" aria-hidden="true"><circle cx="10" cy="10" r="3.7" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10 1.8v2.1M10 16.1v2.1M1.8 10h2.1M16.1 10h2.1M4.2 4.2l1.5 1.5M14.3 14.3l1.5 1.5M15.8 4.2l-1.5 1.5M5.7 14.3l-1.5 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
    <span class="cdt-18__darktext">Dark mode</span><span class="cdt-18__lighttext">Light mode</span>
  </label>
  <div class="cdt-18__scroll">
    <div class="cdt-18__bar" data-bar role="timer">
      <time class="cdt-18__digits" datetime="" data-clock aria-hidden="true">
        <b data-h>01</b><span>:</span><b data-m>59</b><span>:</span><b data-s>07</b>
      </time>
      <p class="cdt-18__copy" data-copy>Free next-day shipping on everything — ends when the clock does.</p>
      <button class="cdt-18__cta" type="button" data-cta>Shop the drop</button>
      <button class="cdt-18__x" type="button" data-close>
        <span class="cdt-18__sr">Dismiss the offer bar</span>
        <svg viewBox="0 0 14 14" width="13" height="13" aria-hidden="true"><path d="M2.5 2.5l9 9M11.5 2.5l-9 9" stroke="currentColor" stroke-width="1.8"/></svg>
      </button>
    </div>

    <div class="cdt-18__page">
      <header class="cdt-18__nav" aria-hidden="true"><strong>Fern Supply</strong><span>New</span><span>Workshop</span><span>Journal</span><span>Cart · 2</span></header>
      <h2 class="cdt-18__title" id="cdt-18-title">Scroll — the bar stays, the timer keeps counting</h2>
      <p class="cdt-18__lede">The bar is sticky inside this scroll container, which is where the pattern usually breaks: an <code>overflow: hidden</code> ancestor between the bar and the scroll root disables sticky entirely.</p>
      <div class="cdt-18__grid" aria-hidden="true">
        <div class="cdt-18__tile"></div><div class="cdt-18__tile"></div><div class="cdt-18__tile"></div>
        <div class="cdt-18__tile"></div><div class="cdt-18__tile"></div><div class="cdt-18__tile"></div>
      </div>
      <p class="cdt-18__foot">At zero the bar swaps to a closed state in place rather than disappearing and shifting the page under you.</p>
      <output class="cdt-18__srlive" aria-live="polite" data-live></output>
    </div>
  </div>
</section>
.cdt-18 {
  --page: #f5f5f2;
  --bar: #17181a;
  --barink: #f7f8f4;
  --ink: #17181a;
  --muted: #6b6d70;
  --rule: #e2e2dd;
  --accent: #c8f52e;
  --digit: 30px;
  --font-display: "Archivo", "Geist", ui-sans-serif, system-ui, sans-serif;
  width: 100%;
  min-height: 100vh;
  display: block;
  box-sizing: border-box;
  background: var(--page);
  color: var(--ink);
  font-family: var(--font-display);
}

@supports (color: oklch(50% 0.1 20)) {
  .cdt-18 {
    --page: oklch(96.5% 0.004 100);
    --bar: oklch(19% 0.006 260);
    --barink: oklch(97% 0.008 100);
    --ink: oklch(20% 0.006 260);
    --muted: oklch(52% 0.008 260);
    --rule: oklch(91% 0.006 100);
    --accent: oklch(90% 0.19 120);
  }
}

.cdt-18 *,
.cdt-18 *::before,
.cdt-18 *::after {
  box-sizing: border-box;
}

.cdt-18__scroll {
  block-size: 100vh;
  overflow-y: auto;
}

.cdt-18__bar {
  position: sticky;
  inset-block-start: 0;
  z-index: 5;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 6px 14px;
  padding: 8px clamp(10px, 2.6vw, 18px);
  background: var(--bar);
  color: var(--barink);
}

.cdt-18__bar[hidden] {
  display: none;
}

.cdt-18__digits {
  display: flex;
  align-items: baseline;
  gap: 1px;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.cdt-18__digits b {
  font: 700 clamp(21px, 5.6vw, var(--digit))/1 var(--font-display);
  letter-spacing: -0.03em;
}

.cdt-18__digits span {
  font: 700 clamp(16px, 4vw, 24px)/1 var(--font-display);
  opacity: 0.45;
}

.cdt-18__copy {
  min-inline-size: 0;
  margin: 0;
  font-size: 12.5px;
  line-height: 1.35;
  color: rgba(247, 248, 244, 0.78);
  text-wrap: pretty;
}

.cdt-18__cta {
  min-block-size: 36px;
  padding: 9px 14px;
  border: 0;
  border-radius: 4px;
  background: var(--accent);
  color: var(--bar);
  font: 700 12.5px/1 var(--font-display);
  white-space: nowrap;
  cursor: pointer;
}

.cdt-18__cta:hover {
  filter: brightness(1.07);
}

.cdt-18__cta:focus-visible {
  outline: 2px solid var(--barink);
  outline-offset: 2px;
}

.cdt-18__x {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--barink);
  cursor: pointer;
}

.cdt-18__x:hover {
  background: rgba(255, 255, 255, 0.1);
}

.cdt-18__x:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.cdt-18__sr,
.cdt-18__srlive {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

@media (max-width: 560px) {
  .cdt-18__bar {
    grid-template-columns: minmax(0, 1fr) auto auto;
  }

  .cdt-18__digits {
    grid-column: 1 / -1;
  }

  .cdt-18__copy {
    grid-column: 1;
  }
}

.cdt-18__page {
  padding: clamp(16px, 4vw, 40px);
}

.cdt-18__nav {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 16px;
  padding-block-end: 16px;
  border-block-end: 1px solid var(--rule);
  font-size: 12.5px;
  color: var(--muted);
}

.cdt-18__nav strong {
  font: 700 15px/1 var(--font-display);
  color: var(--ink);
}

.cdt-18__title {
  margin: 24px 0 8px;
  max-inline-size: 22ch;
  font: 700 clamp(24px, 5vw, 38px)/1.1 var(--font-display);
  letter-spacing: -0.03em;
  text-wrap: balance;
}

.cdt-18__lede {
  margin: 0 0 24px;
  max-inline-size: 56ch;
  font-size: 14px;
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}

.cdt-18__lede code {
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--rule);
  font: 500 12px/1.4 ui-monospace, Menlo, monospace;
  color: var(--ink);
}

.cdt-18__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px;
}

.cdt-18__tile {
  min-block-size: 168px;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: linear-gradient(160deg, #ffffff, #eceae3);
}

.cdt-18__foot {
  margin: 24px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
}
/* --- pure-CSS theme toggle ------------------------------------------- */

.cdt-18 {
  position: relative;
}

.cdt-18__tgl {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.cdt-18__tglbtn {
  position: absolute;
  inset-block-end: 14px;
  inset-inline-end: 14px;
  z-index: 20;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-block-size: 44px;
  min-inline-size: 44px;
  padding: 12px 14px;
  border: 1px solid currentColor;
  border-radius: 10px;
  background: transparent;
  color: inherit;
  font: 600 11px/1 system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0.62;
}

.cdt-18__tglbtn:hover {
  opacity: 1;
}

.cdt-18__tgl:focus-visible + .cdt-18__tglbtn {
  outline: 2px solid currentColor;
  outline-offset: 2px;
  opacity: 1;
}

.cdt-18__tglbtn svg {
  flex: none;
}

.cdt-18__sun,
.cdt-18__lighttext {
  display: none;
}

.cdt-18:has(.cdt-18__tgl:checked) .cdt-18__moon,
.cdt-18:has(.cdt-18__tgl:checked) .cdt-18__darktext {
  display: none;
}

.cdt-18:has(.cdt-18__tgl:checked) .cdt-18__sun {
  display: block;
}

.cdt-18:has(.cdt-18__tgl:checked) .cdt-18__lighttext {
  display: inline;
}

@media (max-width: 460px) {
  .cdt-18__tglbtn {
    padding: 12px;
  }

  .cdt-18__darktext,
    .cdt-18__lighttext,
    .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__darktext,
    .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__lighttext {
    display: none;
  }
}
/* toggle in the system-light case */

@media (prefers-color-scheme: light) {
  .cdt-18:has(.cdt-18__tgl:checked) {
    --page: #101112;
    --bar: #c8f52e;
    --barink: #14160c;
    --ink: #f2f3ee;
    --muted: #93959a;
    --rule: #26282a;
    --accent: #14160c;
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__copy {
    color: rgba(20, 22, 12, 0.78);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__cta {
    background: #14160c;
    color: #c8f52e;
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__x:hover {
    background: rgba(0, 0, 0, 0.12);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__tile {
    background: linear-gradient(160deg, #1a1c1e, #141517);
  }
}

@media (prefers-color-scheme: dark) {
  .cdt-18 {
    --page: #101112;
    --bar: #c8f52e;
    --barink: #14160c;
    --ink: #f2f3ee;
    --muted: #93959a;
    --rule: #26282a;
    --accent: #14160c;
  }

  .cdt-18__copy {
    color: rgba(20, 22, 12, 0.78);
  }

  .cdt-18__cta {
    background: #14160c;
    color: #c8f52e;
  }

  .cdt-18__x:hover {
    background: rgba(0, 0, 0, 0.12);
  }

  .cdt-18__tile {
    background: linear-gradient(160deg, #1a1c1e, #141517);
  }
  /* mirror flip-back: checked means the opposite of the system preference */

  .cdt-18:has(.cdt-18__tgl:checked) {
    --page: oklch(96.5% 0.004 100);
    --bar: oklch(19% 0.006 260);
    --barink: oklch(97% 0.008 100);
    --ink: oklch(20% 0.006 260);
    --muted: oklch(52% 0.008 260);
    --rule: oklch(91% 0.006 100);
    --accent: oklch(90% 0.19 120);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__copy {
    color: rgba(247, 248, 244, 0.78);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__cta {
    background: var(--accent);
    color: var(--bar);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__x:hover {
    background: rgba(255, 255, 255, 0.1);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__tile {
    background: linear-gradient(160deg, #ffffff, #eceae3);
  }
}

[data-theme="dark"] .cdt-18 {
  --page: #101112;
  --bar: #c8f52e;
  --barink: #14160c;
  --ink: #f2f3ee;
  --muted: #93959a;
  --rule: #26282a;
  --accent: #14160c;
}

[data-theme="dark"] .cdt-18 .cdt-18__cta {
  background: #14160c;
  color: #c8f52e;
}
const root = document.querySelector('.cdt-18');
if (root) {
  const q = (s) => root.querySelector(s);
  const bar = q('[data-bar]'), clock = q('[data-clock]'), live = q('[data-live]'), pad = (n) => String(n).padStart(2, '0');
  const target = new Date(Date.now() + 3600e3 + 59 * 60e3 + 7e3);
  let id = 0, seen = null;
  clock.dateTime = target.toISOString();
  const tick = () => {
    const ms = target - Date.now();
    if (ms <= 0) {
      clock.innerHTML = '<b>Closed</b>';
      q('[data-copy]').textContent = 'Free shipping has ended — standard rates apply.';
      q('[data-cta]').textContent = 'Browse the range';
      live.textContent = 'The free shipping offer has closed.';
      return;
    }
    const s = Math.floor(ms / 1000);
    q('[data-h]').textContent = pad(Math.floor(s / 3600));
    q('[data-m]').textContent = pad(Math.floor(s / 60) % 60);
    q('[data-s]').textContent = pad(s % 60);
    const b = ms < 60e3 ? 'm1' : ms < 600e3 ? 'm10' : 'far';
    if (b !== seen) { seen = b; live.textContent = { far: '', m10: 'Ten minutes left for free next-day shipping.', m1: 'One minute left for free next-day shipping.' }[b]; }
    id = setTimeout(tick, 1000 - (Date.now() % 1000));
  };
  q('[data-close]').addEventListener('click', () => { clearTimeout(id); bar.hidden = true; });
  tick();
  root.cleanup = () => clearTimeout(id);
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Countdown Timer from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Sticky Announcement Bar Countdown
Source: https://codefronts.com/snippets/css-countdown-timers/website-sticky-announcement-bars-site-wide/

A site-wide sticky bar where the timer, not the message, is the dominant element: oversized tabular digits, one short line of offer copy, a compact CTA and a real dismiss control. It sticks to the top of the scroll container, collapses to a two-line layout on narrow screens and states its own end condition.
## HTML
```html
<section class="cdt-18" aria-labelledby="cdt-18-title">
  <input class="cdt-18__tgl" type="checkbox" id="cdt-18-theme">
  <label class="cdt-18__tglbtn" for="cdt-18-theme">
    <svg class="cdt-18__moon" viewBox="0 0 20 20" width="15" height="15" aria-hidden="true"><path d="M15.7 12.8A6.5 6.5 0 0 1 7.2 4.3a6.6 6.6 0 1 0 8.5 8.5Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
    <svg class="cdt-18__sun" viewBox="0 0 20 20" width="15" height="15" aria-hidden="true"><circle cx="10" cy="10" r="3.7" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10 1.8v2.1M10 16.1v2.1M1.8 10h2.1M16.1 10h2.1M4.2 4.2l1.5 1.5M14.3 14.3l1.5 1.5M15.8 4.2l-1.5 1.5M5.7 14.3l-1.5 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
    <span class="cdt-18__darktext">Dark mode</span><span class="cdt-18__lighttext">Light mode</span>
  </label>
  <div class="cdt-18__scroll">
    <div class="cdt-18__bar" data-bar role="timer">
      <time class="cdt-18__digits" datetime="" data-clock aria-hidden="true">
        <b data-h>01</b><span>:</span><b data-m>59</b><span>:</span><b data-s>07</b>
      </time>
      <p class="cdt-18__copy" data-copy>Free next-day shipping on everything — ends when the clock does.</p>
      <button class="cdt-18__cta" type="button" data-cta>Shop the drop</button>
      <button class="cdt-18__x" type="button" data-close>
        <span class="cdt-18__sr">Dismiss the offer bar</span>
        <svg viewBox="0 0 14 14" width="13" height="13" aria-hidden="true"><path d="M2.5 2.5l9 9M11.5 2.5l-9 9" stroke="currentColor" stroke-width="1.8"/></svg>
      </button>
    </div>

    <div class="cdt-18__page">
      <header class="cdt-18__nav" aria-hidden="true"><strong>Fern Supply</strong><span>New</span><span>Workshop</span><span>Journal</span><span>Cart · 2</span></header>
      <h2 class="cdt-18__title" id="cdt-18-title">Scroll — the bar stays, the timer keeps counting</h2>
      <p class="cdt-18__lede">The bar is sticky inside this scroll container, which is where the pattern usually breaks: an <code>overflow: hidden</code> ancestor between the bar and the scroll root disables sticky entirely.</p>
      <div class="cdt-18__grid" aria-hidden="true">
        <div class="cdt-18__tile"></div><div class="cdt-18__tile"></div><div class="cdt-18__tile"></div>
        <div class="cdt-18__tile"></div><div class="cdt-18__tile"></div><div class="cdt-18__tile"></div>
      </div>
      <p class="cdt-18__foot">At zero the bar swaps to a closed state in place rather than disappearing and shifting the page under you.</p>
      <output class="cdt-18__srlive" aria-live="polite" data-live></output>
    </div>
  </div>
</section>
```
## CSS
```css
.cdt-18 {
  --page: #f5f5f2;
  --bar: #17181a;
  --barink: #f7f8f4;
  --ink: #17181a;
  --muted: #6b6d70;
  --rule: #e2e2dd;
  --accent: #c8f52e;
  --digit: 30px;
  --font-display: "Archivo", "Geist", ui-sans-serif, system-ui, sans-serif;
  width: 100%;
  min-height: 100vh;
  display: block;
  box-sizing: border-box;
  background: var(--page);
  color: var(--ink);
  font-family: var(--font-display);
}

@supports (color: oklch(50% 0.1 20)) {
  .cdt-18 {
    --page: oklch(96.5% 0.004 100);
    --bar: oklch(19% 0.006 260);
    --barink: oklch(97% 0.008 100);
    --ink: oklch(20% 0.006 260);
    --muted: oklch(52% 0.008 260);
    --rule: oklch(91% 0.006 100);
    --accent: oklch(90% 0.19 120);
  }
}

.cdt-18 *,
.cdt-18 *::before,
.cdt-18 *::after {
  box-sizing: border-box;
}

.cdt-18__scroll {
  block-size: 100vh;
  overflow-y: auto;
}

.cdt-18__bar {
  position: sticky;
  inset-block-start: 0;
  z-index: 5;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 6px 14px;
  padding: 8px clamp(10px, 2.6vw, 18px);
  background: var(--bar);
  color: var(--barink);
}

.cdt-18__bar[hidden] {
  display: none;
}

.cdt-18__digits {
  display: flex;
  align-items: baseline;
  gap: 1px;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.cdt-18__digits b {
  font: 700 clamp(21px, 5.6vw, var(--digit))/1 var(--font-display);
  letter-spacing: -0.03em;
}

.cdt-18__digits span {
  font: 700 clamp(16px, 4vw, 24px)/1 var(--font-display);
  opacity: 0.45;
}

.cdt-18__copy {
  min-inline-size: 0;
  margin: 0;
  font-size: 12.5px;
  line-height: 1.35;
  color: rgba(247, 248, 244, 0.78);
  text-wrap: pretty;
}

.cdt-18__cta {
  min-block-size: 36px;
  padding: 9px 14px;
  border: 0;
  border-radius: 4px;
  background: var(--accent);
  color: var(--bar);
  font: 700 12.5px/1 var(--font-display);
  white-space: nowrap;
  cursor: pointer;
}

.cdt-18__cta:hover {
  filter: brightness(1.07);
}

.cdt-18__cta:focus-visible {
  outline: 2px solid var(--barink);
  outline-offset: 2px;
}

.cdt-18__x {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--barink);
  cursor: pointer;
}

.cdt-18__x:hover {
  background: rgba(255, 255, 255, 0.1);
}

.cdt-18__x:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.cdt-18__sr,
.cdt-18__srlive {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

@media (max-width: 560px) {
  .cdt-18__bar {
    grid-template-columns: minmax(0, 1fr) auto auto;
  }

  .cdt-18__digits {
    grid-column: 1 / -1;
  }

  .cdt-18__copy {
    grid-column: 1;
  }
}

.cdt-18__page {
  padding: clamp(16px, 4vw, 40px);
}

.cdt-18__nav {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 16px;
  padding-block-end: 16px;
  border-block-end: 1px solid var(--rule);
  font-size: 12.5px;
  color: var(--muted);
}

.cdt-18__nav strong {
  font: 700 15px/1 var(--font-display);
  color: var(--ink);
}

.cdt-18__title {
  margin: 24px 0 8px;
  max-inline-size: 22ch;
  font: 700 clamp(24px, 5vw, 38px)/1.1 var(--font-display);
  letter-spacing: -0.03em;
  text-wrap: balance;
}

.cdt-18__lede {
  margin: 0 0 24px;
  max-inline-size: 56ch;
  font-size: 14px;
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}

.cdt-18__lede code {
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--rule);
  font: 500 12px/1.4 ui-monospace, Menlo, monospace;
  color: var(--ink);
}

.cdt-18__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px;
}

.cdt-18__tile {
  min-block-size: 168px;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: linear-gradient(160deg, #ffffff, #eceae3);
}

.cdt-18__foot {
  margin: 24px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
}
/* --- pure-CSS theme toggle ------------------------------------------- */

.cdt-18 {
  position: relative;
}

.cdt-18__tgl {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.cdt-18__tglbtn {
  position: absolute;
  inset-block-end: 14px;
  inset-inline-end: 14px;
  z-index: 20;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-block-size: 44px;
  min-inline-size: 44px;
  padding: 12px 14px;
  border: 1px solid currentColor;
  border-radius: 10px;
  background: transparent;
  color: inherit;
  font: 600 11px/1 system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0.62;
}

.cdt-18__tglbtn:hover {
  opacity: 1;
}

.cdt-18__tgl:focus-visible + .cdt-18__tglbtn {
  outline: 2px solid currentColor;
  outline-offset: 2px;
  opacity: 1;
}

.cdt-18__tglbtn svg {
  flex: none;
}

.cdt-18__sun,
.cdt-18__lighttext {
  display: none;
}

.cdt-18:has(.cdt-18__tgl:checked) .cdt-18__moon,
.cdt-18:has(.cdt-18__tgl:checked) .cdt-18__darktext {
  display: none;
}

.cdt-18:has(.cdt-18__tgl:checked) .cdt-18__sun {
  display: block;
}

.cdt-18:has(.cdt-18__tgl:checked) .cdt-18__lighttext {
  display: inline;
}

@media (max-width: 460px) {
  .cdt-18__tglbtn {
    padding: 12px;
  }

  .cdt-18__darktext,
    .cdt-18__lighttext,
    .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__darktext,
    .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__lighttext {
    display: none;
  }
}
/* toggle in the system-light case */

@media (prefers-color-scheme: light) {
  .cdt-18:has(.cdt-18__tgl:checked) {
    --page: #101112;
    --bar: #c8f52e;
    --barink: #14160c;
    --ink: #f2f3ee;
    --muted: #93959a;
    --rule: #26282a;
    --accent: #14160c;
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__copy {
    color: rgba(20, 22, 12, 0.78);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__cta {
    background: #14160c;
    color: #c8f52e;
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__x:hover {
    background: rgba(0, 0, 0, 0.12);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__tile {
    background: linear-gradient(160deg, #1a1c1e, #141517);
  }
}

@media (prefers-color-scheme: dark) {
  .cdt-18 {
    --page: #101112;
    --bar: #c8f52e;
    --barink: #14160c;
    --ink: #f2f3ee;
    --muted: #93959a;
    --rule: #26282a;
    --accent: #14160c;
  }

  .cdt-18__copy {
    color: rgba(20, 22, 12, 0.78);
  }

  .cdt-18__cta {
    background: #14160c;
    color: #c8f52e;
  }

  .cdt-18__x:hover {
    background: rgba(0, 0, 0, 0.12);
  }

  .cdt-18__tile {
    background: linear-gradient(160deg, #1a1c1e, #141517);
  }
  /* mirror flip-back: checked means the opposite of the system preference */

  .cdt-18:has(.cdt-18__tgl:checked) {
    --page: oklch(96.5% 0.004 100);
    --bar: oklch(19% 0.006 260);
    --barink: oklch(97% 0.008 100);
    --ink: oklch(20% 0.006 260);
    --muted: oklch(52% 0.008 260);
    --rule: oklch(91% 0.006 100);
    --accent: oklch(90% 0.19 120);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__copy {
    color: rgba(247, 248, 244, 0.78);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__cta {
    background: var(--accent);
    color: var(--bar);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__x:hover {
    background: rgba(255, 255, 255, 0.1);
  }

  .cdt-18:has(.cdt-18__tgl:checked) .cdt-18__tile {
    background: linear-gradient(160deg, #ffffff, #eceae3);
  }
}

[data-theme="dark"] .cdt-18 {
  --page: #101112;
  --bar: #c8f52e;
  --barink: #14160c;
  --ink: #f2f3ee;
  --muted: #93959a;
  --rule: #26282a;
  --accent: #14160c;
}

[data-theme="dark"] .cdt-18 .cdt-18__cta {
  background: #14160c;
  color: #c8f52e;
}
```

## JavaScript
```js
const root = document.querySelector('.cdt-18');
if (root) {
  const q = (s) => root.querySelector(s);
  const bar = q('[data-bar]'), clock = q('[data-clock]'), live = q('[data-live]'), pad = (n) => String(n).padStart(2, '0');
  const target = new Date(Date.now() + 3600e3 + 59 * 60e3 + 7e3);
  let id = 0, seen = null;
  clock.dateTime = target.toISOString();
  const tick = () => {
    const ms = target - Date.now();
    if (ms <= 0) {
      clock.innerHTML = '<b>Closed</b>';
      q('[data-copy]').textContent = 'Free shipping has ended — standard rates apply.';
      q('[data-cta]').textContent = 'Browse the range';
      live.textContent = 'The free shipping offer has closed.';
      return;
    }
    const s = Math.floor(ms / 1000);
    q('[data-h]').textContent = pad(Math.floor(s / 3600));
    q('[data-m]').textContent = pad(Math.floor(s / 60) % 60);
    q('[data-s]').textContent = pad(s % 60);
    const b = ms < 60e3 ? 'm1' : ms < 600e3 ? 'm10' : 'far';
    if (b !== seen) { seen = b; live.textContent = { far: '', m10: 'Ten minutes left for free next-day shipping.', m1: 'One minute left for free next-day shipping.' }[b]; }
    id = setTimeout(tick, 1000 - (Date.now() % 1000));
  };
  q('[data-close]').addEventListener('click', () => { clearTimeout(id); bar.hidden = true; });
  tick();
  root.cleanup = () => clearTimeout(id);
}
```

How this works

The timer is the component. A promo bar leads with a sentence and hides a small clock at the end; this bar inverts that hierarchy deliberately — the digits are the largest thing in the strip, the copy is a single clause, and the CTA is compact. That is what makes it a countdown bar rather than an announcement bar with a timer bolted on, and it is also what keeps it legible at a glance while scrolling.

Sticky needs a container to stick to. position: sticky resolves against the nearest scrolling ancestor, so a bar that works in a demo and fails in production usually has an overflow: hidden wrapper between it and the scroll root. The demo ships its own scroll container so the behaviour is visible, and the bar sits at inset-block-start: 0 with a z-index above the page content it slides over.

Dismiss has to be a real control. A bare × glyph in a <span> is unreachable by keyboard and unnamed to a screen reader. This one is a <button type="button"> at 44×44px with a visually hidden label, and dismissal removes the bar and clears the timer handle in the same step — leaving a ticking timeout behind for a bar that no longer exists is a small leak that shows up as a console error after unmount.

The bar has to end gracefully. At zero the digits are replaced by an Offer closed label and the CTA becomes a link to the regular range, rather than the bar vanishing mid-scroll and shifting the page under the reader. Every tick recomputes from Date.now(), so a bar left open in a background tab is correct on return instead of frozen at whatever second the throttling started.

Make it yours

  • Change the offer window by editing the milliseconds added to Date.now() at the top of the script.
  • Retint the bar via --accent and --bar; the digits, CTA and rule follow.
  • Move the bar to the bottom by swapping inset-block-start for inset-block-end.
  • Drop the dismiss control if the offer is short enough not to need one.
  • Set --digit to make the digits louder or quieter relative to the copy.
  • Swap the CTA for a code chip by replacing the button content with your promo code.
  • Drop the theme toggle by deleting the checkbox, its <label> and every :has(.cdt-18__tgl:checked) rule.

Gotchas — read before shipping

  • An overflow: hidden ancestor silently disables position: sticky — the most common reason a bar works in isolation and not in the page.
  • A dismiss glyph in a non-button element is unreachable by keyboard and unnamed to assistive tech.
  • Dismissing without clearing the timer leaves a handle ticking against a removed node.
  • Letting the bar disappear at zero shifts the page under the reader mid-scroll; swap to a closed state in place.
  • Making the message dominant and the timer tiny turns this into a promo bar, which is a different component with a different owner.
  • A theme toggle written in only one colour-scheme media block inverts correctly for one system preference and does nothing for the other — the flip-back rule has to exist in both.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

The pure-CSS theme toggle uses :has(:checked), which puts the Firefox floor at 121 (Chrome 105, Safari 15.4); where :has() is missing the toggle button simply does nothing and the demo follows prefers-color-scheme alone. oklch() sets the floor for the charcoal bar and lime accent, with sRGB hex declared first. position: sticky, the grid reflow and the drift-corrected tick are all Baseline, so the bar behaves identically on older engines with a slightly flatter accent.

Techniques used in this demo

Search CodeFronts

Loading…