<main>
  <analog-clock label="New York" timezone="-4"></analog-clock>
  <analog-clock label="Copenhagen" timezone="+1" indices steps date="day"></analog-clock>
  <analog-clock label="Tokyo" timezone="+9" indices marker="●" class="round-markers dark" steps></analog-clock>
  <analog-clock label="SAIKO" system="roman" date="day" class="roman platinum"></analog-clock>
  <analog-clock label="ROBEX" system="roman" numerals="4" class="roman gold"></analog-clock>
  <analog-clock label="မြန်မာ" system="mymr" timezone="+6.5" class="burmese" indices marker="•"></analog-clock>
  <analog-clock label="ประเทศไทย" system="thai" timezone="+7" class="thai" indices marker="·" marker-hour="•"></analog-clock>
  <analog-clock label="अरुणाचल" system="wcho" timezone="+5.5" class="indian"></analog-clock>
</main >
.burmese {
  --_yellow: #FFCC00;
  --_green: #43A047;
  --_red: #EF4438;
  
  --analog-clock-bg: radial-gradient(
    circle at 50% 50%,
    var(--_yellow) 45%,
    var(--_green) 46%,
    var(--_green) 49%,
    var(--_red) 50%,
    var(--_red) 55%
  );
  --analog-clock-c: var(--_yellow);
  --analog-clock-fw: 600;
  --analog-clock-hour: var(--_green);
  --analog-clock-minute: var(--_green);
  --analog-clock-second: #FFF;
  --analog-clock-cap: var(--_green);
  --analog-clock-indices-c: color-mix(in oklab, var(--_yellow), transparent 30%);
  --analog-clock-indices-p: 3cqi;
  --analog-clock-date-c: var(--_yellow);
  --analog-clock-label-c: var(--_red);
}
.dark { color-scheme: dark; }
.gold {
  --_gold: #E2CA7D;
  --_dark: color-mix(in oklab, var(--_gold) 60%, black);
    --_accent: color-mix(in oklab, var(--_gold) 80%, maroon);
  --analog-clock-bg: 
    radial-gradient(
      circle at 50% 50%,
      color-mix(in oklab, var(--_gold) 20%, white) 50%,
      var(--_gold) 0 51%,
      color-mix(in oklab, var(--_gold) 85%, black) 95%
    );
  --analog-clock-c: color-mix(in oklab, var(--_gold) 50%, black);
  --analog-clock-ff: "Didot", "Bodoni MT", "Noto Serif Display", serif;
  --analog-clock-fw: 500;
  --analog-clock-fs: 7cqi;
  --analog-clock-hour: var(--_dark);
  --analog-clock-minute: var(--_dark);
  --analog-clock-second: var(--_accent);
  --analog-clock-cap: color-mix(in oklab, var(--_dark), white 20%);
}
.indian {
  --_saffron: #FF9933;
  --_green: #138808;
  --_blue: #000080;
  
  --analog-clock-bg: radial-gradient(
    circle at 50% 50%,
    #FFF 45%,
    var(--_saffron) 46%,
    var(--_saffron) 48%,
    #FFF 49%,
    #FFF 51%,
    var(--_green) 52%,
    var(--_green) 54%,
    #FFF 55%
  );
  --analog-clock-c: var(--_blue);
  --analog-clock-fw: 600;
  --analog-clock-hour: var(--_saffron);
  --analog-clock-minute: var(--_green);
  --analog-clock-second: #B41E8E;
  --analog-clock-cap: var(--_blue);
  --analog-clock-indices-c: var(--_green);
  --analog-clock-date-c: var(--_blue);
}
.platinum {
  --_platinum: #E5E4E2;
  --_dark: color-mix(in oklab, var(--_platinum) 60%, black);
  --_accent: color-mix(in oklab, var(--_platinum) 80%, steelblue);
  --analog-clock-bg: 
    radial-gradient(
      circle at 50% 50%,
      color-mix(in oklab, var(--_platinum) 20%, white) 60%,
      color-mix(in oklab, var(--_platinum) 85%, black) 95%
    );
  --analog-clock-c: color-mix(in oklab, var(--_platinum) 50%, black);
  --analog-clock-ff: "Didot", "Bodoni MT", "Noto Serif Display", serif;
  --analog-clock-fw: 500;
  --analog-clock-fs: 7cqi;
  --analog-clock-hour: var(--_dark);
  --analog-clock-minute: var(--_dark);
  --analog-clock-second: var(--_accent);
  --analog-clock-cap: color-mix(in oklab, var(--_dark), white 20%);
}
.roman {
  --analog-clock-ff: "Didot", "Bodoni MT", "Noto Serif Display", serif;
}
.round-markers {
  --analog-clock-indices-fs: 3cqi;
  --analog-clock-indices-hour-fw: 400;
  --analog-clock-indices-p: 2ch;
  --analog-clock-numerals-m: 1ch;
}
.thai {
  --_red: #EF3340;
  --_white: #FFF;
  --_blue: #00247D;
  
  --analog-clock-bg: radial-gradient(
    circle at 50% 50%,
    var(--_white) 45%,
    var(--_red) 46%,
    var(--_red) 48%,
    var(--_white) 49%,
    var(--_white) 51%,
    var(--_blue) 52%,
    var(--_blue) 54%,
    var(--_white) 55%
  );
  --analog-clock-c: var(--_blue);
  --analog-clock-fw: 600;
  --analog-clock-hour: var(--_red);
  --analog-clock-minute: var(--_blue);
  --analog-clock-second: gold;
  --analog-clock-cap: var(--_blue);
  --analog-clock-indices-c: var(--_red);
  --analog-clock-indices-hour-c: var(--_blue);
  --analog-clock-indices-p: 3cqi;
  --analog-clock-date-c: var(--_blue);
}

body { margin: 1rem; }
main {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
const styles = new CSSStyleSheet();
styles.replaceSync(`
  :host {
    aspect-ratio: 1;
    background: var(--analog-clock-bg, light-dark(hsl(0, 0%, 95%), hsl(0, 0%, 15%)));
    border-radius: 50%;
    color: var(--analog-clock-c, light-dark(hsl(0, 0%, 15%), hsl(0, 0%, 85%)));
    color-scheme: light dark;
    container-type: inline-size;
    font-family: var(--analog-clock-ff, ui-sans-serif, system-ui, sans-serif);
    display: grid;
    grid-template-rows: repeat(3, 1fr);
    inline-size: 100%;
    overflow: clip;
    position: relative;
  }

  /* === Indices === */

  :host::part(indices) {
    aspect-ratio: 1;
    border-radius: 50%;
    box-sizing: border-box;
    color: var(--analog-clock-indices-c, light-dark(hsl(0, 0%, 85%), hsl(0, 0%, 35%)));
    font-size: var(--analog-clock-indices-fs, 6cqi);
    grid-area: 1 / 1 / 4 / 1;
    margin: 0;
    padding: var(--analog-clock-indices-p, 0);
    place-self: center;
    width: 100%;
  }
  :host::part(hour) {
    color: var(--analog-clock-indices-hour-c, light-dark(hsl(0, 0%, 15%), hsl(0, 0%, 85%)));
    font-weight: var(--analog-clock-indices-hour-fw, 800);
  }
  :host [part~=indices] li {
    display: inline-block;
    list-style: none;
    offset-distance: var(--_d);
    offset-path: content-box;
    width: fit-content;
  }

  /* === Numerals === */

  :host::part(numerals) {
    grid-area: 1 / 1 / 4 / 1;
    margin: var(--analog-clock-numerals-m, 0);
    padding: 0;
    position: relative;
  }
  :host [part~=numerals] li {
    --_r: calc((100% - 15cqi) / 2);
    --_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));
    --_y: calc(var(--_r) + (var(--_r) * sin(var(--_d))));
    aspect-ratio: 1;
    display: grid;
    font-size: var(--analog-clock-fs, 6cqi);
    font-weight: var(--analog-clock-fw, 700);
    left: var(--_x);
    place-content: center;
    position: absolute;
    top: var(--_y);
    width: 15cqi;
  }

  /* === Hands and Date === */

  :host::part(hands) {
    display: grid;
    grid-area: 2 / 1 / 3 / 1;
    grid-template-columns: repeat(3, 1fr);
  }
  :host::part(hands)::after {
    aspect-ratio: 1;
    background-color: var(--analog-clock-cap, currentColor);
    border-radius: 50%;
    content: "";
    grid-area: 1 / 2 / 1 / 3;
    height: var(--analog-clock-cap-sz, 8cqi);
    isolation: isolate;
    place-self: center;
  }
  :host [part~="hands"] b {
    border-radius: calc(var(--_w) * 2);
    display: block;
    height: var(--_h);
    left: calc((100% - var(--_w)) / 2);
    position: absolute;
    top: calc((100% / 2) - var(--_h));
    transform: rotate(0deg);
    transform-origin: bottom;
    width: var(--_w);
  }
  :host::part(hours) {
    --_h: 35%;
    --_w: 2cqi;
    animation: turn 43200s linear infinite;
    animation-delay: var(--_dh, 0ms);
    background-color: var(--analog-clock-hour, currentColor);
  }
  :host::part(minutes) {
    --_h: 45%;
    --_w: 2cqi;
    animation: turn 3600s steps(60, end) infinite;
    animation-delay: var(--_dm, 0ms);
    background-color: var(--analog-clock-minute, currentColor);
  }
  :host::part(seconds) {
    --_h: 45%;
    --_w: 1cqi;
    animation: turn 60s var(--_tf, linear) infinite;
    animation-delay: var(--_ds, 0ms);
    background-color: var(--analog-clock-second, #ff8c05);
  }

  /* === Label and Date === */
 
  :host::part(date) { 
    border: .25cqi solid currentColor;
    color: var(--analog-clock-date-c, #888);
    font-family: var(--analog-clock-date-ff, ui-monospace, monospace);
    font-size: var(--analog-clock-date-fs, 5cqi);
    grid-area: 1 / 3 / 1 / 4;
    padding: 0 .6ch;
    place-self: center start;
  }
  :host::part(label) {
    color: var(--analog-clock-label-c, currentColor);
    font-size: var(--analog-clock-label-fs, 5cqi);
    font-weight: var(--analog-clock-label-fw, 600);
    grid-area: 3 / 1 / 4 / 2;
    place-self: start center;
  }

  @keyframes turn {
    to { transform: rotate(1turn); }
  }
`);

class AnalogClock extends HTMLElement {
  #root;
  #date;
  #numberFormatter;
  #romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII'];

  #formatNumber(num) {
    const system = this.getAttribute('system') || 'latn';

    if (system === 'roman') return this.#romanNumerals[num - 1];
    if (system === 'romanlow') return this.#romanNumerals[num - 1].toLowerCase();
    
    if (!this.#numberFormatter) {
      this.#numberFormatter = new Intl.NumberFormat('en', { 
        numberingSystem: system
      });
    }
    return this.#numberFormatter.format(num);
  }

  #generateNumerals(count) {
    count = Math.min(12, Math.max(1, parseInt(count) || 12));
    const step = 360 / count;
    return Array.from({ length: count }, (_, i) => {
      const deg = ((i * step) + 270) % 360;
      const num = ((i * (12 / count))) % 12 || 12;
      return `<li style="--_d:${deg}deg">${this.#formatNumber(num)}</li>`;
    }).join('');
  }

  #generateIndices() {
    if (!this.hasAttribute('indices')) return '';
    const isHours = this.getAttribute('indices') === 'hours';
    const count = isHours ? 12 : 60;
    const step = 100 / count;
    const marker = this.getAttribute('marker') || '|';
    const markerHour = this.getAttribute('marker-hour') || marker;
    
    return Array.from({ length: count }, (_, i) => {
      const percentage = `${(i * step)}%`;
      const isHourMark = isHours || i % 5 === 0;
      const part = isHourMark ? 'part="index hour"' : 'part="index"';
      const currentMarker = isHourMark ? markerHour : marker;
      return `<li style="--_d:${percentage}" ${part}>${currentMarker}</li>`;
    }).join('');
  }

  #formatDate(tzTime) {
    const date = this.getAttribute('date');
    if (!date) {
      this.#date.hidden = true;
      return '';
    }

    this.#date.hidden = false;
    const parts = {
      day: tzTime.getDate().toString().padStart(2, '0'),
      month: (tzTime.getMonth() + 1).toString().padStart(2, '0'),
      year: tzTime.getFullYear().toString()
    };

    return date.split(' ')
      .map(part => parts[part])
      .filter(Boolean)
      .join(' ');
  }

  constructor() {
    super();
    this.#root = this.attachShadow({ mode: 'open' });
    this.#root.adoptedStyleSheets = [styles];
    this.#root.innerHTML = `
      <ul part="indices">${this.#generateIndices()}</ul>
      <ol part="numerals">${this.#generateNumerals(this.getAttribute('numerals'))}</ol>
      <nav part="hands">
        <b part="seconds"></b>
        <b part="minutes"></b>
        <b part="hours"></b>
        <time part="date"></time>
      </nav>
      <span part="label"></span>`;

    this.#date = this.#root.querySelector('[part="date"]');
    this.#root.querySelector('[part="label"]').textContent = this.getAttribute('label') || '';

    if (this.hasAttribute('steps')) {
      this.style.setProperty('--_tf', 'steps(60)');
    }

    this.updateClock();
  }

  updateClock() {
    const time = new Date();
    const tzOffset = parseInt(this.getAttribute('timezone') || '0');
    
    // Convert to UTC first, then add timezone offset
    const utc = time.getTime() + (time.getTimezoneOffset() * 60000);
    const tzTime = new Date(utc + (3600000 * tzOffset));

    const hour = -3600 * (tzTime.getHours() % 12);
    const mins = -60 * tzTime.getMinutes();
    const secs = -tzTime.getSeconds();

    // Update date display
    this.#date.textContent = this.#formatDate(tzTime);

    this.style.setProperty('--_dh', `${(hour+mins)}s`);
    this.style.setProperty('--_dm', `${mins}s`);
    this.style.setProperty('--_ds', `${secs}s`);
  }
}

customElements.define('analog-clock', AnalogClock);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.