Open demo

CSS code

HTML
<div><span class="lbl">Local time by zone</span>
<ul class="zones" data-world>
  <li class="z" data-tz="Europe/London"><h3>London</h3><b data-show="time">--:--</b><span data-show="day">—</span></li>
  <li class="z" data-tz="America/New_York"><h3>New York</h3><b data-show="time">--:--</b><span data-show="day">—</span></li>
  <li class="z" data-tz="America/Los_Angeles"><h3>San Francisco</h3><b data-show="time">--:--</b><span data-show="day">—</span></li>
  <li class="z" data-tz="Europe/Riga"><h3>Riga</h3><b data-show="time">--:--</b><span data-show="day">—</span></li>
  <li class="z" data-tz="Asia/Tokyo"><h3>Tokyo</h3><b data-show="time">--:--</b><span data-show="day">—</span></li>
  <li class="z" data-tz="Australia/Sydney"><h3>Sydney</h3><b data-show="time">--:--</b><span data-show="day">—</span></li>
</ul></div>
CSS
/* World clocks: one card per [data-tz] (an IANA zone). The script writes the local time, the day and
   whether it is working hours there, and sets data-awake, which turns the left edge green. */
.zones{list-style:none;display:grid;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));gap:15px}
.z{background:#fff;border-radius:13px;padding:17px 19px;box-shadow:0 2px 10px rgba(15,23,42,.09);border-left:4px solid #cbd5e1}
.z[data-awake]{border-left-color:#16a34a}
.z h3{font-size:.86rem;font-weight:600;color:#475569;margin-bottom:9px}
.z b{display:block;font-size:1.8rem;font-weight:600;line-height:1;font-variant-numeric:tabular-nums}
.z span{display:block;font-size:.74rem;color:#64748b;margin-top:7px}
JavaScript
// World clocks. Each [data-tz] card inside [data-world] shows the time in its IANA zone, the day,
// and "Working hours" (Mon-Fri, 09:00-18:00 local) or "After hours". Text only changes once a minute.
(function () {
  var cards = document.querySelectorAll('[data-world] [data-tz]');
  if (!cards.length) return;
  var START = 9, END = 18;
  function set(el, text) { if (el && el.textContent !== text) el.textContent = text; }
  function tick() {
    var now = new Date();
    cards.forEach(function (card) {
      var tz = card.getAttribute('data-tz');
      var hour = +now.toLocaleString('en-GB', { timeZone: tz, hour: '2-digit', hourCycle: 'h23' });
      var weekday = now.toLocaleDateString('en-GB', { timeZone: tz, weekday: 'short' });
      var awake = hour >= START && hour < END && weekday !== 'Sat' && weekday !== 'Sun';
      set(card.querySelector('[data-show="time"]'), now.toLocaleTimeString('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit' }));
      set(card.querySelector('[data-show="day"]'), now.toLocaleDateString('en-GB', { timeZone: tz, weekday: 'short', day: 'numeric', month: 'short' }) +
        ' · ' + (awake ? 'Working hours' : 'After hours'));
      card.toggleAttribute('data-awake', awake);
    });
    setTimeout(tick, 1000 - Date.now() % 1000);
  }
  tick();
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Local time by zone</span>
<ul class="zones row row-cols-1 row-cols-sm-2 row-cols-md-4 list-unstyled mb-0" data-world>
  <li class="col"><div class="card h-100 border-start" data-tz="Europe/London"><div class="card-body"><h3 class="fw-semibold">London</h3><b class="d-block fw-semibold" data-show="time">--:--</b><span class="d-block" data-show="day">—</span></div></div></li>
  <li class="col"><div class="card h-100 border-start" data-tz="America/New_York"><div class="card-body"><h3 class="fw-semibold">New York</h3><b class="d-block fw-semibold" data-show="time">--:--</b><span class="d-block" data-show="day">—</span></div></div></li>
  <li class="col"><div class="card h-100 border-start" data-tz="America/Los_Angeles"><div class="card-body"><h3 class="fw-semibold">San Francisco</h3><b class="d-block fw-semibold" data-show="time">--:--</b><span class="d-block" data-show="day">—</span></div></div></li>
  <li class="col"><div class="card h-100 border-start" data-tz="Europe/Riga"><div class="card-body"><h3 class="fw-semibold">Riga</h3><b class="d-block fw-semibold" data-show="time">--:--</b><span class="d-block" data-show="day">—</span></div></div></li>
  <li class="col"><div class="card h-100 border-start" data-tz="Asia/Tokyo"><div class="card-body"><h3 class="fw-semibold">Tokyo</h3><b class="d-block fw-semibold" data-show="time">--:--</b><span class="d-block" data-show="day">—</span></div></div></li>
  <li class="col"><div class="card h-100 border-start" data-tz="Australia/Sydney"><div class="card-body"><h3 class="fw-semibold">Sydney</h3><b class="d-block fw-semibold" data-show="time">--:--</b><span class="d-block" data-show="day">—</span></div></div></li>
</ul></div>
CSS
/* World clocks on the Bootstrap grid (.row-cols-*) with .card tiles. The left edge is .border-start (4px via --bs-border-width),
   coloured through --bs-border-color, which turns green when the script sets data-awake. */
.zones{--bs-gutter-x:15px;--bs-gutter-y:15px}
.zones .card{--bs-card-border-width:0;--bs-card-border-radius:13px;--bs-card-spacer-y:17px;--bs-card-spacer-x:19px;
  --bs-card-color:#1e293b;--bs-border-width:4px;--bs-border-color:#cbd5e1;box-shadow:0 2px 10px rgba(15,23,42,.09)}
.zones .card[data-awake]{--bs-border-color:#16a34a}
.zones h3{font-size:.86rem;color:#475569;margin-bottom:9px}
.zones b{font-size:1.8rem;line-height:1;font-variant-numeric:tabular-nums}
.zones .card span{font-size:.74rem;color:#64748b;margin-top:7px}
JavaScript
// World clocks. Each [data-tz] card inside [data-world] shows the time in its IANA zone, the day,
// and "Working hours" (Mon-Fri, 09:00-18:00 local) or "After hours". Text only changes once a minute.
(function () {
  var cards = document.querySelectorAll('[data-world] [data-tz]');
  if (!cards.length) return;
  var START = 9, END = 18;
  function set(el, text) { if (el && el.textContent !== text) el.textContent = text; }
  function tick() {
    var now = new Date();
    cards.forEach(function (card) {
      var tz = card.getAttribute('data-tz');
      var hour = +now.toLocaleString('en-GB', { timeZone: tz, hour: '2-digit', hourCycle: 'h23' });
      var weekday = now.toLocaleDateString('en-GB', { timeZone: tz, weekday: 'short' });
      var awake = hour >= START && hour < END && weekday !== 'Sat' && weekday !== 'Sun';
      set(card.querySelector('[data-show="time"]'), now.toLocaleTimeString('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit' }));
      set(card.querySelector('[data-show="day"]'), now.toLocaleDateString('en-GB', { timeZone: tz, weekday: 'short', day: 'numeric', month: 'short' }) +
        ' · ' + (awake ? 'Working hours' : 'After hours'));
      card.toggleAttribute('data-awake', awake);
    });
    setTimeout(tick, 1000 - Date.now() % 1000);
  }
  tick();
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#64748b] uppercase">Local time by zone</span>
<ul class="grid grid-cols-[repeat(auto-fill,minmax(185px,1fr))] gap-[15px]" data-world>
  <li class="rounded-[13px] border-l-4 border-[#cbd5e1] bg-white px-[19px] py-[17px] shadow-[0_2px_10px_rgba(15,23,42,.09)] data-awake:border-[#16a34a]" data-tz="Europe/London"><h3 class="mb-[9px] text-[.86rem] font-semibold text-[#475569]">London</h3><b class="block text-[1.8rem] leading-none font-semibold tabular-nums" data-show="time">--:--</b><span class="mt-[7px] block text-[.74rem] text-[#64748b]" data-show="day">—</span></li>
  <li class="rounded-[13px] border-l-4 border-[#cbd5e1] bg-white px-[19px] py-[17px] shadow-[0_2px_10px_rgba(15,23,42,.09)] data-awake:border-[#16a34a]" data-tz="America/New_York"><h3 class="mb-[9px] text-[.86rem] font-semibold text-[#475569]">New York</h3><b class="block text-[1.8rem] leading-none font-semibold tabular-nums" data-show="time">--:--</b><span class="mt-[7px] block text-[.74rem] text-[#64748b]" data-show="day">—</span></li>
  <li class="rounded-[13px] border-l-4 border-[#cbd5e1] bg-white px-[19px] py-[17px] shadow-[0_2px_10px_rgba(15,23,42,.09)] data-awake:border-[#16a34a]" data-tz="America/Los_Angeles"><h3 class="mb-[9px] text-[.86rem] font-semibold text-[#475569]">San Francisco</h3><b class="block text-[1.8rem] leading-none font-semibold tabular-nums" data-show="time">--:--</b><span class="mt-[7px] block text-[.74rem] text-[#64748b]" data-show="day">—</span></li>
  <li class="rounded-[13px] border-l-4 border-[#cbd5e1] bg-white px-[19px] py-[17px] shadow-[0_2px_10px_rgba(15,23,42,.09)] data-awake:border-[#16a34a]" data-tz="Europe/Riga"><h3 class="mb-[9px] text-[.86rem] font-semibold text-[#475569]">Riga</h3><b class="block text-[1.8rem] leading-none font-semibold tabular-nums" data-show="time">--:--</b><span class="mt-[7px] block text-[.74rem] text-[#64748b]" data-show="day">—</span></li>
  <li class="rounded-[13px] border-l-4 border-[#cbd5e1] bg-white px-[19px] py-[17px] shadow-[0_2px_10px_rgba(15,23,42,.09)] data-awake:border-[#16a34a]" data-tz="Asia/Tokyo"><h3 class="mb-[9px] text-[.86rem] font-semibold text-[#475569]">Tokyo</h3><b class="block text-[1.8rem] leading-none font-semibold tabular-nums" data-show="time">--:--</b><span class="mt-[7px] block text-[.74rem] text-[#64748b]" data-show="day">—</span></li>
  <li class="rounded-[13px] border-l-4 border-[#cbd5e1] bg-white px-[19px] py-[17px] shadow-[0_2px_10px_rgba(15,23,42,.09)] data-awake:border-[#16a34a]" data-tz="Australia/Sydney"><h3 class="mb-[9px] text-[.86rem] font-semibold text-[#475569]">Sydney</h3><b class="block text-[1.8rem] leading-none font-semibold tabular-nums" data-show="time">--:--</b><span class="mt-[7px] block text-[.74rem] text-[#64748b]" data-show="day">—</span></li>
</ul></div>
JavaScript
// World clocks. Each [data-tz] card inside [data-world] shows the time in its IANA zone, the day,
// and "Working hours" (Mon-Fri, 09:00-18:00 local) or "After hours". Text only changes once a minute.
(function () {
  var cards = document.querySelectorAll('[data-world] [data-tz]');
  if (!cards.length) return;
  var START = 9, END = 18;
  function set(el, text) { if (el && el.textContent !== text) el.textContent = text; }
  function tick() {
    var now = new Date();
    cards.forEach(function (card) {
      var tz = card.getAttribute('data-tz');
      var hour = +now.toLocaleString('en-GB', { timeZone: tz, hour: '2-digit', hourCycle: 'h23' });
      var weekday = now.toLocaleDateString('en-GB', { timeZone: tz, weekday: 'short' });
      var awake = hour >= START && hour < END && weekday !== 'Sat' && weekday !== 'Sun';
      set(card.querySelector('[data-show="time"]'), now.toLocaleTimeString('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit' }));
      set(card.querySelector('[data-show="day"]'), now.toLocaleDateString('en-GB', { timeZone: tz, weekday: 'short', day: 'numeric', month: 'short' }) +
        ' · ' + (awake ? 'Working hours' : 'After hours'));
      card.toggleAttribute('data-awake', awake);
    });
    setTimeout(tick, 1000 - Date.now() % 1000);
  }
  tick();
})();

About this set

A grid of city clocks for a distributed team: London, New York, San Francisco, Riga, Tokyo and Sydney, each on its own card showing the local time, the weekday and date, and whether it is working hours there. Cities inside working hours, Monday to Friday from 09:00 to 18:00 local time, get a green left edge and the words Working hours; the rest show a grey edge and After hours, so the state does not rely on colour alone. Use it on a team page, a support contact page or an internal dashboard to show who is likely to be online. Each card names an IANA time zone in a data-tz attribute, and the script reads the time with toLocaleTimeString and the timeZone option, so daylight saving changes are handled by the browser with no time zone library. To add a city, copy a card and change its name and zone. The grid fills as many 185 pixel columns as fit. The Bootstrap version uses the grid with .row-cols-* and .card tiles, and the Tailwind version uses an auto-fill grid utility.

What’s included

  • Six city cards driven by IANA time zones
  • Working hours shown as text and as a green left edge
  • Daylight saving handled by the browser's Intl support, no library
  • Add a city by copying one card and changing data-tz
  • Responsive grid from one column on phones to four on desktop

Accessibility

Cities are a list, each card has the city as a heading, and the time, date and working-hours state are plain text that changes at most once a minute, so screen readers read current values without interruptions. The working-hours state is written in words as well as colour, and the grey date text reaches 4.5:1 contrast.

Customise it

Edit the data-tz values and city names, and START and END in the script for different hours. In plain CSS change the colours on .z and .z[data-awake]; in Bootstrap the --bs-border-color and --bs-card-* variables on .zones .card; in Tailwind the border- and data-awake:border- utilities.