<main>
  <digital-clock class="digital-clock"></digital-clock>
  <digital-clock label="New York" lang="en-US" date="short" timezone="-4" time="12hour" class="newyork"></digital-clock>
  <digital-clock label="Tokyo" lang="ja-JP" date="narrow" timezone="+9" class="tokyo"></digital-clock>
  <digital-clock label="Sydney" lang="en-AU" date="short" timezone="+10" time="12hour short" class="sydney"></digital-clock>
  <digital-clock label="東京" lang="ja-JP" date="short" number-system="hiragana" timezone="+9" class="hiragana"></digital-clock>
  <digital-clock label="الرياض" lang="ar-SA" date="full" number-system="arabic-indic" timezone="+3" class="riyadh"></digital-clock> 
  <digital-clock label="北京" lang="zh-CN" date="full" number-system="cjk-decimal" timezone="+8" class="beijing"></digital-clock>
  <digital-clock label="กรุงเทพฯ" lang="th" date="short" number-system="thai" timezone="+7" class="bangkok"></digital-clock>
  <digital-clock label="ROMA" number-system="upper-roman" timezone="+2" time="short" class="roma"></digital-clock>  
</main>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;700&family=Noto+Serif+SC&family=Victor+Mono:ital,wght@0,700;1,700&display=swap');
body {
  background: hsl(0, 10%, 10%);
  padding: 1em;
}
main {
  display: flex;
  flex-wrap: wrap;
  gap: 2ch;
  justify-content: center;
  margin-inline: auto;
  max-inline-size: 1000px;
}
digital-clock {
  border-radius: .25em;
  display: grid;
  font-family: monospace;
  font-size: 1rem;
  grid-template-columns: 1fr 1fr;
  padding: 1em 2em;
  &::part(date) { place-self: start end; }
  &::part(time) {
    grid-column: span 2;
    font-size: 250%;
    place-self: center;
  }
}
.bangkok {
  background: linear-gradient(135deg, #273c75, #e84118, #44bd32);
  border: 2px solid #fbc531;
  border-radius: 6px;
  box-shadow: 0 0 15px rgba(253, 203, 110, 0.6);
  color: #f5f6fa;
  &::part(time) { 
    font-size: 350%;
    text-shadow: 0 0 8px #fbc531, 0 0 12px rgba(255, 255, 255, 0.2);
    letter-spacing: 2px;
  }
  &::part(date),
  &::part(label)  {
    color: #ffeaa7;
    font-weight: 700;
  }
}
.beijing {
  background: linear-gradient(to right, #8a0000, #b80000);
  border-radius: 6px;
  color: #ffcc00;
  font-family: "Noto Sans SC", "Noto Sans", sans-serif;
  &::part(time) { 
    text-shadow: 0 0 6px rgba(255, 204, 0, 0.3);
    font-weight: 500;
  }
  &::part(date),
  &::part(label) {
    color: #ffe680;
  }
}
.digital-clock {
  background: #0f1824;
  border: 2ch solid #29313d;
  color: #CACACA;
  font-family: "Victor Mono", monospace;
}
.hiragana {
  background: #080808;
  color: #ff1a1a;
  font-family: ui-sans-serif, system-ui;
  width: 300px;
  &::part(time) { 
    text-shadow: 0 0 5px rgba(255, 26, 26, 0.7);
  }
}
.newyork {
  background-color: #242625;
  color: #3085E6;
  font-family: Bahnschrift, 'DIN Alternate', 'Franklin Gothic Medium', 'Nimbus Sans Narrow', sans-serif-condensed, sans-serif;
  &::part(time) {
    font-size: 350%;
    margin-block: .25ch;
  }
  &::part(ampm) { font-size: 50%; place-self: end; }
  &::part(ampm), &::part(hours), &::part(minutes), &::part(seconds) {
    text-box: cap alphabetic;
  }
}
.riyadh {
  background: #1b5e20;
  border-radius: 8px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  color: #f8f8f8;
  font-family: "Noto Sans Arabic", "Noto Sans", sans-serif;
  &::part(time) { 
    font-size: 350%; 
    letter-spacing: 1px;
  }
  &::part(hours), &::part(minutes), &::part(seconds) {
    text-box: cap alphabetic;
  }
  &::part(date),
  &::part(label) {
    color: #e6d2b5;
  }
  padding: 1.5em;
  position: relative;
  &::before {
    content: '';
    position: absolute;
    inset: 6px;
    border: 1px solid rgba(230, 210, 181, 0.3);
    border-radius: 4px;
  }
}
.roma {
  background: antiquewhite;
  font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif;
  &::part(label) {
    font-size: 150%;
    grid-column: span 2;
    letter-spacing: 1ch;
    place-self: center;
  }
}
.sydney {
  background: #fefefe;
  border: 1px solid #DDd;
  font-family: Optima, Candara, 'Noto Sans', source-sans-pro, sans-serif;
}
.tokyo {
  background: #DDD;
  color: #272E38;
  grid-template-columns: 1fr 1fr;
  padding: 1em 2em;
}
CSS.registerProperty({
  name: '--seconds',
  syntax: '<integer>',
  initialValue: '0',
  inherits: false
});

CSS.registerProperty({
  name: '--minutes',
  syntax: '<integer>',
  initialValue: '0',
  inherits: false
});

CSS.registerProperty({
  name: '--hours',
  syntax: '<integer>',
  initialValue: '0',
  inherits: false
});

const styles = new CSSStyleSheet();
styles.replaceSync(`
  :host {
    background-color: var(--digital-clock-bg, var(--CanvasGray));
    border-radius: var(--digital-clock-bdrs, var(--input-bdrs));
    box-sizing: border-box;
    color-scheme: light dark;
    display: flex;
    direction: ltr;
    gap: var(--digital-clock-gap, 1ch);
    inline-size: min-content;
    padding: var(--digital-clock-p, .75ch 1.5ch);
  }
  :host::part(ampm)::after {
    content: counter(hours, am-pm);
  }
  :host::part(date) {
    font-family: var(--digital-clock-date-ff, inherit);
    font-size: var(--digital-clock-date-fs, inherit);
    font-weight: var(--digital-clock-date-fw, inherit);
    text-wrap: nowrap;
  } 
  :host::part(label) {
    font-family: var(--digital-clock-label-ff, inherit);
    font-size: var(--digital-clock-label-fs, inherit);
    font-weight: var(--digital-clock-label-fw, inherit);
    text-wrap: nowrap;
  }
  :host::part(time) {
    all: unset;
    display: grid;
    font-size: var(--digital-clock-fs, inherit);
    font-variant-numeric: tabular-nums;
    font-weight: var(--digital-clock-fw, inherit);
    grid-auto-flow: column;
    inline-size: min-content;
    list-style: none;
    text-wrap: nowrap;
  }
  :host::part(ampm),
  :host::part(hours) {
    animation: hours 86400s steps(24, end) infinite;
    animation-delay: var(--delay-hours, 0s);
    counter-reset: hours var(--hours);
  }
  :host::part(hours)::after {
    content: counter(hours, var(--number-system, decimal-leading-zero)) ' ';
  }
  :host::part(minutes){
    animation: minutes 3600s steps(60, end) infinite;
    animation-delay: var(--delay-minutes, 0s);
    counter-reset: minutes var(--minutes);
  }
  :host::part(minutes)::before {
    content: ':';
  }
  :host::part(minutes)::after {
    content: counter(minutes, var(--number-system, decimal-leading-zero)) ' ';
  }
  :host::part(seconds) {
    animation: seconds 60s steps(60, end) infinite;
    animation-delay: var(--delay-seconds, 0s);
    counter-reset: seconds var(--seconds);
  }
  :host::part(seconds)::before {
    content: ':';
  }
  :host::part(seconds)::after {
    content: counter(seconds, var(--number-system, decimal-leading-zero)) ' ';
  }
  :host([time*="12hour"])::part(hours) {
    counter-reset: hours calc(mod(var(--hours) - 1, 12) + 1);
  }

  @keyframes hours {
    from { --hours: 0; }
    to { --hours: 24; } 
  }
  @keyframes minutes { 
    from { --minutes: 0; }
    to { --minutes: 60; } 
  }
  @keyframes seconds { 
    from { --seconds: 0;}
    to { --seconds: 60; }
  }
`);

let counterStyleInjected = false;

class DigitalClock extends HTMLElement {
  #root;
  #date;
  #label;

  constructor() {
    super();
    this.#root = this.attachShadow({ mode: 'open' });
    this.#root.adoptedStyleSheets = [styles];

    const hasDate = this.hasAttribute('date');
    const hasLabel = this.hasAttribute('label');
    const time = this.getAttribute('time');
    const is12Hour = time?.includes('12hour');

    this.#root.innerHTML = `
      ${hasLabel ? `<span part="label"></span>`:''}
      ${hasDate ? `<span part="date"></span>`:''}
      <ol part="time">
        <li part="hours"></li>
        <li part="minutes"></li>
        ${time?.includes('short') ? '':`<li part="seconds"></li>`}
        ${is12Hour ? `<li part="ampm"></li>` : ''}
      </ol>
    `;

    if (hasDate) {
      this.#date = this.#root.querySelector('[part=date]');
    }
    if (hasLabel) {
      this.#label = this.#root.querySelector('[part=label]');
      this.#label.textContent = this.getAttribute('label') || '';
    }
    this.#updateClock(hasDate);

    if (!counterStyleInjected) {
      /* Safari Hack: Safari has issues with counter-style in shadow DOM */
      this.#injectCounterStyle();
      counterStyleInjected = true;
    }
  }

  #formatDate(date, format) {
    const lang = this.getAttribute('lang') ||  document.documentElement.lang || navigator.language;
    let options;
    switch (format) {
      case 'narrow':
        options = { 
          day: 'numeric',
          month: 'numeric',
          year: '2-digit'
        };
        break;
      case 'short':
        options = { 
          day: 'numeric',
          month: 'short',
          year: '2-digit'
        };
        break;
      case 'full':
      default:
        options = { 
          weekday: 'long',
          day: 'numeric',
          month: 'long',
          year: 'numeric'
        };
    }
    return new Intl.DateTimeFormat(lang, options).format(date);
  }

  #injectCounterStyle() {
    const style = document.createElement('style');
    style.textContent = `
      @counter-style am-pm {
        system: cyclic;
        symbols: "am" "am" "am" "am" "am" "am" "am" "am" "am" "am" "am" "pm" "pm" "pm" "pm" "pm" "pm" "pm" "pm" "pm" "pm" "pm" "pm" "am";
      }
    `;
    document.head.appendChild(style);
  }

  #roundTzOffset(offset) {
    return Math.round((parseFloat(offset) || 0) * 4) / 4
  };

  #updateClock(hasDate) {
    const time = new Date();
    const tzOffset = this.#roundTzOffset(this.getAttribute('timezone') || '0');
    const utc = time.getTime() + (time.getTimezoneOffset() * 60000);
    const tzTime = new Date(utc + (3600000 * tzOffset));

    const hours = tzTime.getHours() * 3600;
    const minutes = tzTime.getMinutes() * 60;
    const seconds = tzTime.getSeconds();

    this.style.setProperty('--delay-hours', `-${hours + minutes + seconds}s`);
    this.style.setProperty('--delay-minutes', `-${minutes + seconds}s`);
    this.style.setProperty('--delay-seconds', `-${seconds}s`);
    this.style.setProperty('--number-system', this.getAttribute('number-system') || 'decimal-leading-zero');
    if (hasDate) this.#updateDate(tzTime);
  }

  #updateDate(tzTime) {
    const dateAttr = this.getAttribute('date');
    if (dateAttr) {
      this.#date.textContent = this.#formatDate(tzTime, dateAttr);
      this.#date.hidden = false;
    } else {
      this.#date.hidden = true;
    }
  }
}

customElements.define('digital-clock', DigitalClock);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.