Last active
September 12, 2025 04:37
-
-
Save pszemraj/e415fd66d2747e9db019c3410eddfceb to your computer and use it in GitHub Desktop.
simple html clock
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Clock AI</title> | |
| <style> | |
| :root { | |
| --bg: #0a0b0d; | |
| --panel: #141518; | |
| --text: #ffffff; | |
| --muted: #666; | |
| --accent: #0ea5e9; | |
| --scale: 1.8; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| height: 100vh; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'JetBrains Mono', 'SF Mono', 'Monaco', 'Inconsolata', monospace; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| user-select: none; | |
| overflow: hidden; | |
| } | |
| /* BIG TIME - THE WHOLE POINT */ | |
| .time { | |
| font-variant-numeric: tabular-nums; | |
| font-weight: 700; | |
| font-size: calc(min(22vw, 200px) * var(--scale)); | |
| letter-spacing: -0.02em; | |
| line-height: 1; | |
| display: flex; | |
| align-items: center; | |
| margin: 20px 0; | |
| } | |
| .time span { | |
| display: inline-block; | |
| } | |
| .colon { | |
| width: 0.15em; | |
| height: 0.15em; | |
| background: currentColor; | |
| border-radius: 50%; | |
| margin: 0 0.2em; | |
| opacity: 0.4; | |
| align-self: center; | |
| } | |
| .seconds { | |
| font-size: 0.35em; | |
| opacity: 0.5; | |
| margin-left: 0.1em; | |
| font-weight: 500; | |
| } | |
| .date { | |
| font-size: calc(14px * var(--scale)); | |
| text-transform: uppercase; | |
| letter-spacing: 0.3em; | |
| opacity: 0.5; | |
| margin-bottom: 10px; | |
| } | |
| .tz { | |
| font-size: calc(12px * var(--scale)); | |
| opacity: 0.3; | |
| letter-spacing: 0.2em; | |
| margin-top: 10px; | |
| } | |
| /* Controls - keep them simple */ | |
| .controls { | |
| position: fixed; | |
| top: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| opacity: 0.2; | |
| transition: opacity 0.2s; | |
| } | |
| body:hover .controls { | |
| opacity: 0.9; | |
| } | |
| .controls select, | |
| .controls button { | |
| background: var(--panel); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| color: var(--text); | |
| padding: 6px 12px; | |
| border-radius: 6px; | |
| font-size: 11px; | |
| cursor: pointer; | |
| font-family: inherit; | |
| } | |
| .controls button.toggle { | |
| width: 48px; | |
| height: 24px; | |
| padding: 2px; | |
| position: relative; | |
| } | |
| .controls button.toggle::after { | |
| content: ''; | |
| position: absolute; | |
| width: 18px; | |
| height: 18px; | |
| background: var(--text); | |
| border-radius: 50%; | |
| top: 2px; | |
| left: 2px; | |
| transition: transform 0.2s; | |
| } | |
| .controls button.toggle[data-on="true"]::after { | |
| transform: translateX(24px); | |
| } | |
| .controls button.toggle[data-on="true"] { | |
| background: rgba(14, 165, 233, 0.3); | |
| } | |
| /* Scale slider */ | |
| input[type="range"] { | |
| width: 200px; | |
| height: 24px; | |
| background: var(--panel); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| border-radius: 12px; | |
| outline: none; | |
| -webkit-appearance: none; | |
| cursor: pointer; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| background: var(--text); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| input[type="range"]::-webkit-slider-runnable-track { | |
| height: 100%; | |
| border-radius: 12px; | |
| } | |
| .scale-label { | |
| font-size: 10px; | |
| opacity: 0.5; | |
| margin-left: -10px; | |
| } | |
| /* Extra zones - optional */ | |
| .zones { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 40px; | |
| font-size: calc(11px * var(--scale)); | |
| opacity: 0.3; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .zone { | |
| display: flex; | |
| gap: 8px; | |
| align-items: baseline; | |
| } | |
| .zone-name { | |
| opacity: 0.5; | |
| text-transform: uppercase; | |
| letter-spacing: 0.1em; | |
| } | |
| .zone-time { | |
| font-weight: 600; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="controls"> | |
| <select id="tzSel"> | |
| <option value="auto">Auto</option> | |
| <option value="America/New_York">New York</option> | |
| <option value="America/Chicago">Chicago</option> | |
| <option value="America/Denver">Denver</option> | |
| <option value="America/Los_Angeles">Los Angeles</option> | |
| <option value="Europe/London">London</option> | |
| <option value="Europe/Paris">Paris</option> | |
| <option value="Europe/Berlin">Berlin</option> | |
| <option value="Asia/Tokyo">Tokyo</option> | |
| <option value="Asia/Shanghai">Shanghai</option> | |
| <option value="Australia/Sydney">Sydney</option> | |
| <option value="UTC">UTC</option> | |
| </select> | |
| <button id="secToggle" class="toggle" data-on="false"></button> | |
| <input id="scaleSlider" type="range" min="0.5" max="3.0" step="0.05" value="1.8"> | |
| <span class="scale-label" id="scaleLabel">1.8x</span> | |
| </div> | |
| <div class="date" id="date">LOADING...</div> | |
| <div class="time"> | |
| <span id="h">00</span> | |
| <span class="colon"></span> | |
| <span id="m">00</span> | |
| <span class="seconds" id="s" style="display:none"></span> | |
| </div> | |
| <div class="tz" id="tz">EDT</div> | |
| <div class="zones" id="zones" style="display:none"> | |
| <!-- Will be populated by JS --> | |
| </div> | |
| <script> | |
| // State from localStorage | |
| let showSec = JSON.parse(localStorage.getItem("clock.showSec") ?? "false"); | |
| let zonePref = localStorage.getItem("clock.tz") ?? "auto"; | |
| let scale = parseFloat(localStorage.getItem("clock.scale") ?? "1.8"); | |
| let showZones = JSON.parse(localStorage.getItem("clock.showZones") ?? "false"); | |
| // Elements | |
| const el = { | |
| h: document.getElementById("h"), | |
| m: document.getElementById("m"), | |
| s: document.getElementById("s"), | |
| date: document.getElementById("date"), | |
| tz: document.getElementById("tz"), | |
| tzSel: document.getElementById("tzSel"), | |
| secToggle: document.getElementById("secToggle"), | |
| scaleSlider: document.getElementById("scaleSlider"), | |
| scaleLabel: document.getElementById("scaleLabel"), | |
| zones: document.getElementById("zones") | |
| }; | |
| // Additional zones to show | |
| const extraZones = [ | |
| { name: 'NYC', tz: 'America/New_York' }, | |
| { name: 'UTC', tz: 'UTC' }, | |
| { name: 'LON', tz: 'Europe/London' }, | |
| { name: 'TOK', tz: 'Asia/Tokyo' } | |
| ]; | |
| // Initialize UI | |
| el.tzSel.value = zonePref; | |
| el.secToggle.dataset.on = String(showSec); | |
| el.s.style.display = showSec ? 'inline-block' : 'none'; | |
| el.scaleSlider.value = scale; | |
| el.scaleLabel.textContent = scale.toFixed(1) + 'x'; | |
| document.documentElement.style.setProperty('--scale', String(scale)); | |
| el.zones.style.display = showZones ? 'flex' : 'none'; | |
| // Seconds toggle | |
| el.secToggle.addEventListener("click", () => { | |
| showSec = !showSec; | |
| el.secToggle.dataset.on = String(showSec); | |
| el.s.style.display = showSec ? 'inline-block' : 'none'; | |
| localStorage.setItem("clock.showSec", JSON.stringify(showSec)); | |
| if (!showSec) el.s.textContent = ''; | |
| }); | |
| // Timezone change | |
| el.tzSel.addEventListener("change", e => { | |
| zonePref = e.target.value; | |
| localStorage.setItem("clock.tz", zonePref); | |
| }); | |
| // Scale slider | |
| el.scaleSlider.addEventListener("input", e => { | |
| scale = parseFloat(e.target.value); | |
| document.documentElement.style.setProperty('--scale', String(scale)); | |
| el.scaleLabel.textContent = scale.toFixed(1) + 'x'; | |
| }); | |
| el.scaleSlider.addEventListener("change", () => { | |
| localStorage.setItem("clock.scale", String(scale)); | |
| }); | |
| // Get current timezone | |
| function getCurrentTz() { | |
| return zonePref === "auto" | |
| ? Intl.DateTimeFormat().resolvedOptions().timeZone | |
| : zonePref; | |
| } | |
| // Update function - only update what changed | |
| let lastMin = -1; | |
| function update() { | |
| const now = new Date(); | |
| const tz = getCurrentTz(); | |
| // Time formatting | |
| const timeParts = new Intl.DateTimeFormat('en-US', { | |
| timeZone: tz, | |
| hour12: false, | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit' | |
| }).formatToParts(now); | |
| const time = Object.fromEntries(timeParts.map(p => [p.type, p.value])); | |
| // Always update time | |
| el.h.textContent = time.hour; | |
| el.m.textContent = time.minute; | |
| if (showSec) { | |
| el.s.textContent = ':' + time.second; | |
| } | |
| // Update other elements only once per minute | |
| if (parseInt(time.minute) !== lastMin) { | |
| lastMin = parseInt(time.minute); | |
| // Date | |
| const dateStr = new Intl.DateTimeFormat('en-US', { | |
| timeZone: tz, | |
| weekday: 'long', | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }).format(now); | |
| el.date.textContent = dateStr.toUpperCase(); | |
| // Timezone | |
| const tzName = new Intl.DateTimeFormat('en-US', { | |
| timeZone: tz, | |
| timeZoneName: 'short' | |
| }).format(now).split(' ').pop(); | |
| el.tz.textContent = tzName; | |
| // Update extra zones if visible | |
| if (showZones) { | |
| updateZones(now); | |
| } | |
| } | |
| } | |
| function updateZones(now) { | |
| el.zones.innerHTML = extraZones.map(zone => { | |
| const time = new Intl.DateTimeFormat('en-US', { | |
| timeZone: zone.tz, | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| hour12: false | |
| }).format(now); | |
| return `<div class="zone"> | |
| <span class="zone-name">${zone.name}</span> | |
| <span class="zone-time">${time}</span> | |
| </div>`; | |
| }).join(''); | |
| } | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', e => { | |
| switch(e.key.toLowerCase()) { | |
| case 's': | |
| el.secToggle.click(); | |
| break; | |
| case 'z': | |
| showZones = !showZones; | |
| el.zones.style.display = showZones ? 'flex' : 'none'; | |
| localStorage.setItem("clock.showZones", JSON.stringify(showZones)); | |
| if (showZones) updateZones(new Date()); | |
| break; | |
| case 'f': | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen(); | |
| } else { | |
| document.exitFullscreen(); | |
| } | |
| break; | |
| case '+': | |
| case '=': | |
| if (scale < 3.0) { | |
| scale = Math.min(3.0, scale + 0.1); | |
| el.scaleSlider.value = scale; | |
| el.scaleSlider.dispatchEvent(new Event('input')); | |
| el.scaleSlider.dispatchEvent(new Event('change')); | |
| } | |
| break; | |
| case '-': | |
| case '_': | |
| if (scale > 0.5) { | |
| scale = Math.max(0.5, scale - 0.1); | |
| el.scaleSlider.value = scale; | |
| el.scaleSlider.dispatchEvent(new Event('input')); | |
| el.scaleSlider.dispatchEvent(new Event('change')); | |
| } | |
| break; | |
| } | |
| }); | |
| // Run update | |
| update(); | |
| setInterval(update, showSec ? 100 : 500); | |
| // Prevent context menu | |
| document.addEventListener('contextmenu', e => e.preventDefault()); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment