Workday Meter

Your workday as a loading bar. Every block fills in real time as the clock moves, with breaks and lunch marked along the way. It runs on your device's clock, nothing is sent anywhere.

Working --:--:--
0.0%7:00 AM to 5:30 PM
  • Work
  • Break
  • Lunch
Elapsed-
Remaining-
Next-

    Why Watch the Day Fill Up

    A workday is hard to feel from the inside. The hours blur together, and 2pm can seem like the middle of the day or the very end depending entirely on your mood. A visible meter swaps that vague feeling for an actual number, so you can tell how much of the day is behind you without doing the math in your head.

    A Status Bar for Something That Normally Has None

    Downloads, installs, and uploads all get progress bars, but the thing that takes up most of your waking hours doesn't get one. Watching blocks fill turns an abstract stretch of time into something finite, which makes a slow afternoon easier to pace and makes it obvious when the finish line is close.

    Why Breaks Get Their Own Blocks

    Breaks and lunch show up in a different color on purpose. They're part of the schedule, not gaps in it, and seeing them laid out ahead of time makes it easier to push through a stretch of work when the next stopping point is already on the map. It also keeps you honest about actually taking them.

    Making It Match Your Day

    The schedule is a plain list at the top of the script below: a label, a type, and start and end times in 24-hour format. Change those lines to match your own shift, add more breaks, or drop lunch entirely, and the grid, the bar, and the countdowns all rebuild themselves from that one list. It reads your device's local clock, so it works in any time zone. It also runs every day, so add a weekday check if you only want it showing on workdays.

    Copy the Code

    Plain HTML, CSS, and JavaScript, no dependencies, no build step, nothing phoned home. Comments walk through what each block is doing, so it's easy to adapt rather than just paste and forget.

    <!-- Workday Meter -- copy this whole block into any HTML page. -->
    <!-- No build step, no dependencies -- plain HTML, CSS, and JS. -->
    <!-- Everything runs locally from your device's clock. Nothing is sent anywhere. -->
    
    <div id="workday-meter">
      <div class="workday-status">
        <span class="workday-phase" id="wdPhase">Working</span>
        <span class="workday-clock" id="wdClock">--:--:--</span>
        <span class="workday-detail" id="wdDetail"></span>
      </div>
    
      <div class="workday-bar" id="wdBar" role="progressbar" aria-label="Workday progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
        <div class="workday-bar-fill" id="wdBarFill"></div>
        <div id="wdBarZones"></div>
      </div>
      <div class="workday-percent-row"><span id="wdPercent">0.0%</span><span>Day progress</span></div>
    
      <div class="workday-grid" id="wdGrid" aria-hidden="true"></div>
    
      <div class="workday-stats">
        <div class="workday-stat"><span class="workday-stat-label">Elapsed</span><span class="workday-stat-value" id="wdElapsed">-</span></div>
        <div class="workday-stat"><span class="workday-stat-label">Remaining</span><span class="workday-stat-value" id="wdRemaining">-</span></div>
        <div class="workday-stat"><span class="workday-stat-label">Next</span><span class="workday-stat-value" id="wdNext">-</span></div>
      </div>
    
      <ul class="workday-schedule" id="wdSchedule"></ul>
    </div>
    
    <style>
      /* Minimal styling -- restyle freely to match your own site. */
      #workday-meter { font-family: sans-serif; max-width: 720px; color: #dce8e4; }
      .workday-status { text-align: center; padding: 16px; margin-bottom: 14px; background: #121a1c; border-radius: 8px; }
      .workday-phase { display: block; font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: #4de8ff; }
      .workday-clock { display: block; font-size: 40px; font-weight: 700; color: #39ff88; }
      .workday-detail { color: #7f938d; font-size: 14px; }
    
      /* Progress bar: a full-width gradient revealed left to right with clip-path. */
      .workday-bar { position: relative; height: 24px; background: #121a1c; border-radius: 6px; overflow: hidden; }
      .workday-bar-fill { position: absolute; inset: 0; clip-path: inset(0 100% 0 0); transition: clip-path 1s linear;
        background: linear-gradient(90deg, #39ff88, #4de8ff 55%, #4a7dff); }
      /* Break and lunch zones: striped overlay on the bar. */
      .workday-zone { position: absolute; top: 0; bottom: 0;
        background: repeating-linear-gradient(45deg, rgba(10,13,14,0.7) 0 4px, rgba(10,13,14,0.15) 4px 8px); }
      .workday-percent-row { display: flex; justify-content: space-between; margin: 6px 0 18px; font-size: 12px; color: #7f938d; }
    
      /* Block grid: --fill (0 to 1) is set by the script as each block's time passes. */
      .workday-grid { display: grid; grid-template-columns: repeat(21, 1fr); gap: 4px; margin-bottom: 14px; }
      .workday-block { --fill: 0; --wd-color: #39ff88; --wd-dim: rgba(57,255,136,0.08);
        aspect-ratio: 1; border-radius: 3px; border: 1px solid #1d2a2c;
        background: linear-gradient(90deg, var(--wd-color) calc(var(--fill) * 100%), var(--wd-dim) calc(var(--fill) * 100%)); }
      .workday-block.wd-break { --wd-color: #4de8ff; --wd-dim: rgba(77,232,255,0.16); }
      .workday-block.wd-lunch { --wd-color: #4a7dff; --wd-dim: rgba(74,125,255,0.18); }
      .workday-block.is-current { animation: wdPulse 1.6s ease-in-out infinite; }
      @keyframes wdPulse { 50% { box-shadow: 0 0 10px rgba(57,255,136,0.7); } }
      @media (prefers-reduced-motion: reduce) { .workday-block.is-current { animation: none; } }
      @media (max-width: 600px) { .workday-grid { grid-template-columns: repeat(14, 1fr); } }
    
      .workday-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 14px; }
      .workday-stat { padding: 10px 12px; background: #121a1c; border-radius: 6px; }
      .workday-stat-label { display: block; font-size: 11px; text-transform: uppercase; color: #7f938d; }
      .workday-schedule { list-style: none; margin: 0; padding: 0; }
      .workday-item { display: flex; justify-content: space-between; padding: 8px 2px; border-bottom: 1px solid #1d2a2c; color: #7f938d; font-size: 14px; }
      .workday-item.is-done { opacity: 0.5; text-decoration: line-through; }
      .workday-item.is-now { color: #39ff88; }
    </style>
    
    <script>
    (function () {
      // ---- Edit this list to match your own day. Times are 24-hour "HH:MM"
      // in the visitor's local time. type is "work", "break", or "lunch"
      // (it only controls the block color). Segments must run back to back.
      var SCHEDULE = [
        { label: 'Work',  type: 'work',  start: '07:00', end: '09:15' },
        { label: 'Break', type: 'break', start: '09:15', end: '09:30' },
        { label: 'Work',  type: 'work',  start: '09:30', end: '11:25' },
        { label: 'Lunch', type: 'lunch', start: '11:25', end: '12:00' },
        { label: 'Work',  type: 'work',  start: '12:00', end: '14:15' },
        { label: 'Break', type: 'break', start: '14:15', end: '14:30' },
        { label: 'Work',  type: 'work',  start: '14:30', end: '17:30' }
      ];
      var BLOCK_MINUTES = 5; // each grid block = this many minutes
    
      var gridEl = document.getElementById('wdGrid');
      var barEl = document.getElementById('wdBar');
      var fillEl = document.getElementById('wdBarFill');
      var zonesEl = document.getElementById('wdBarZones');
      var phaseEl = document.getElementById('wdPhase');
      var clockEl = document.getElementById('wdClock');
      var detailEl = document.getElementById('wdDetail');
      var percentEl = document.getElementById('wdPercent');
      var elapsedEl = document.getElementById('wdElapsed');
      var remainingEl = document.getElementById('wdRemaining');
      var nextEl = document.getElementById('wdNext');
      var listEl = document.getElementById('wdSchedule');
    
      function toMin(t) {
        var p = t.split(':');
        return parseInt(p[0], 10) * 60 + parseInt(p[1], 10);
      }
    
      // Minutes since midnight -> "9:15 AM"
      function fmtTime(min) {
        var h = Math.floor(min / 60);
        var m = min % 60;
        var ap = h >= 12 ? 'PM' : 'AM';
        h = h % 12 || 12;
        return h + ':' + (m < 10 ? '0' : '') + m + ' ' + ap;
      }
    
      // Minutes (can be fractional) -> "2h 15m", "2h", or "12m". Countdowns
      // round up so they never read "0m" while seconds are still left; pass
      // down = true for elapsed time, which should round down instead.
      function fmtDur(min, down) {
        var t = Math.max(0, down ? Math.floor(min + 1e-9) : Math.ceil(min - 1e-9));
        var h = Math.floor(t / 60);
        var m = t % 60;
        if (h && m) return h + 'h ' + m + 'm';
        if (h) return h + 'h';
        return m + 'm';
      }
    
      function clamp(n, lo, hi) { return Math.min(hi, Math.max(lo, n)); }
    
      var segs = SCHEDULE.map(function (s) {
        return { label: s.label, type: s.type, start: toMin(s.start), end: toMin(s.end) };
      });
      var dayStart = segs[0].start;
      var dayEnd = segs[segs.length - 1].end;
      var total = dayEnd - dayStart;
    
      function segmentAt(min) {
        for (var i = 0; i < segs.length; i++) {
          if (min >= segs[i].start && min < segs[i].end) return i;
        }
        return -1;
      }
    
      // --- Build the block grid (one block per BLOCK_MINUTES of the day) ---
      var blockCount = Math.ceil(total / BLOCK_MINUTES);
      var blocks = [];
      var blockFill = [];
      for (var i = 0; i < blockCount; i++) {
        var b = document.createElement('span');
        var idx = segmentAt(dayStart + i * BLOCK_MINUTES);
        b.className = 'workday-block wd-' + segs[idx].type;
        gridEl.appendChild(b);
        blocks.push(b);
        blockFill.push(-1);
      }
    
      // --- Mark breaks/lunch as striped zones on the progress bar ---
      segs.forEach(function (s) {
        if (s.type === 'work') return;
        var z = document.createElement('span');
        z.className = 'workday-zone wd-' + s.type;
        z.style.left = ((s.start - dayStart) / total * 100) + '%';
        z.style.width = ((s.end - s.start) / total * 100) + '%';
        zonesEl.appendChild(z);
      });
    
      // --- Build the schedule list ---
      var items = segs.map(function (s) {
        var li = document.createElement('li');
        li.className = 'workday-item wd-' + s.type;
        li.innerHTML = '<span class="workday-item-name"></span><span class="workday-item-time"></span>';
        li.firstChild.textContent = s.label;
        li.lastChild.textContent = fmtTime(s.start) + ' - ' + fmtTime(s.end);
        listEl.appendChild(li);
        return li;
      });
      var itemState = items.map(function () { return ''; });
    
      function tick() {
        var d = new Date();
        var now = d.getHours() * 60 + d.getMinutes() + d.getSeconds() / 60;
        var pct = clamp((now - dayStart) / total, 0, 1);
    
        clockEl.textContent = d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', second: '2-digit' });
        fillEl.style.clipPath = 'inset(0 ' + (100 - pct * 100).toFixed(3) + '% 0 0)';
        percentEl.textContent = (pct * 100).toFixed(1) + '%';
        barEl.setAttribute('aria-valuenow', String(Math.round(pct * 100)));
    
        // Each block fills left to right as its 5 minutes pass.
        for (var i = 0; i < blocks.length; i++) {
          var f = Math.round(clamp((now - (dayStart + i * BLOCK_MINUTES)) / BLOCK_MINUTES, 0, 1) * 100) / 100;
          if (f !== blockFill[i]) {
            blockFill[i] = f;
            blocks[i].style.setProperty('--fill', f);
            blocks[i].classList.toggle('is-current', f > 0 && f < 1);
          }
        }
    
        var cur = segmentAt(now);
        var phase, detail;
        if (now < dayStart) {
          phase = 'Before work';
          detail = 'Starts at ' + fmtTime(dayStart) + ', in ' + fmtDur(dayStart - now);
          nextEl.textContent = segs[0].label + ' at ' + fmtTime(dayStart);
        } else if (cur === -1) {
          phase = 'Day complete';
          detail = 'Finished at ' + fmtTime(dayEnd) + '. See you tomorrow.';
          nextEl.textContent = 'Tomorrow at ' + fmtTime(dayStart);
        } else {
          var s = segs[cur];
          var nxt = segs[cur + 1];
          phase = s.type === 'work' ? 'Working' : s.label;
          if (nxt) {
            detail = s.type === 'work'
              ? fmtDur(s.end - now) + ' until ' + nxt.label.toLowerCase()
              : 'Back to work in ' + fmtDur(s.end - now);
            nextEl.textContent = nxt.label + ' at ' + fmtTime(nxt.start);
          } else {
            detail = fmtDur(s.end - now) + ' until the day ends';
            nextEl.textContent = 'End of day at ' + fmtTime(dayEnd);
          }
        }
        phaseEl.textContent = phase;
        detailEl.textContent = detail;
        elapsedEl.textContent = fmtDur(clamp(now - dayStart, 0, total), true);
        remainingEl.textContent = fmtDur(clamp(dayEnd - now, 0, total));
    
        // Mark each schedule row done / now / upcoming (only touches the DOM on change).
        for (var j = 0; j < segs.length; j++) {
          var state = now >= segs[j].end ? 'is-done' : (j === cur ? 'is-now' : 'is-upcoming');
          if (state !== itemState[j]) {
            itemState[j] = state;
            items[j].className = 'workday-item wd-' + segs[j].type + ' ' + state;
          }
        }
      }
    
      tick();
      setInterval(tick, 1000);
    })();
    
    </script>