Local & Weather

Free Business Hours Widget

Your opening hours, with a live "open now" indicator.

A tidy hours table that tells visitors at a glance whether you are open right now. Weekly hours are passed in as JSON with a timezone, and the widget computes open/closed status live. Perfect for contact pages, footers, and Google-proof "are they open?" answers.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/business-hours.js" data-hl-config='{"mon":"8:00-18:00","tue":"8:00-18:00","wed":"8:00-18:00","thu":"8:00-18:00","fri":"8:00-18:00","sat":"9:00-14:00","sun":"closed"}' async></script>
<!-- Free Business Hours widget by haynes lab — https://hayneslab.dev/tools/widgets/business-hours -->
Inline (self-contained)html
<!-- Free Business Hours widget by haynes lab — https://hayneslab.dev/tools/widgets/business-hours -->
<script data-hl-config='{"mon":"8:00-18:00","tue":"8:00-18:00","wed":"8:00-18:00","thu":"8:00-18:00","fri":"8:00-18:00","sat":"9:00-14:00","sun":"closed"}'>
/*!
 * haynes lab — Business Hours widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/business-hours
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-business-hours';
  var DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
  var DAY_LABELS = { mon: 'Monday', tue: 'Tuesday', wed: 'Wednesday', thu: 'Thursday', fri: 'Friday', sat: 'Saturday', sun: 'Sunday' };
  var WEEKDAY_TO_KEY = { Mon: 'mon', Tue: 'tue', Wed: 'wed', Thu: 'thu', Fri: 'fri', Sat: 'sat', Sun: 'sun' };

  function injectStyles() {
    if (document.getElementById(PREFIX + '-styles')) return;
    var style = document.createElement('style');
    style.id = PREFIX + '-styles';
    style.textContent =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; margin: 0; padding: 0; }' +
      '.' + PREFIX + ' { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #1f2937; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 16px 18px 12px; max-width: 340px; }' +
      '.' + PREFIX + '__header { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }' +
      '.' + PREFIX + '__title { font-size: 15px; font-weight: 600; color: #111827; }' +
      '.' + PREFIX + '__badge { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; padding: 3px 10px; border-radius: 999px; white-space: nowrap; }' +
      '.' + PREFIX + '__badge::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }' +
      '.' + PREFIX + '__badge--open { color: var(--hlw-accent, #22c55e); background: #f0fdf4; background: color-mix(in srgb, var(--hlw-accent, #22c55e) 12%, transparent); }' +
      '.' + PREFIX + '__badge--closed { color: #6b7280; background: #f3f4f6; }' +
      '.' + PREFIX + '__table { width: 100%; border-collapse: collapse; }' +
      '.' + PREFIX + '__row td { padding: 4px 0; border-bottom: 1px solid #f3f4f6; }' +
      '.' + PREFIX + '__row:last-child td { border-bottom: none; }' +
      '.' + PREFIX + '__day { color: #374151; }' +
      '.' + PREFIX + '__hours { text-align: right; color: #6b7280; }' +
      '.' + PREFIX + '__row--today .' + PREFIX + '__day { font-weight: 700; color: var(--hlw-accent, #22c55e); }' +
      '.' + PREFIX + '__row--today .' + PREFIX + '__hours { font-weight: 600; color: #111827; }' +
      '.' + PREFIX + '__credit { display: flex; align-items: center; justify-content: flex-end; gap: 4px; margin-top: 8px; font-size: 11px; color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }';
    document.head.appendChild(style);
  }

  function parseRange(value) {
    if (typeof value !== 'string') return null;
    var v = value.trim().toLowerCase();
    if (v === 'closed' || v === '') return null;
    var m = v.match(/^(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})$/);
    if (!m) return null;
    var start = (+m[1]) * 60 + (+m[2]);
    var end = (+m[3]) * 60 + (+m[4]);
    if (start >= 1440 || end > 1440) return null;
    return { start: start, end: end };
  }

  function nowInZone(tz) {
    try {
      var parts = new Intl.DateTimeFormat('en-US', {
        timeZone: tz, weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
      }).formatToParts(new Date());
      var bag = {};
      parts.forEach(function (p) { bag[p.type] = p.value; });
      var hour = (+bag.hour) % 24;
      return { day: WEEKDAY_TO_KEY[bag.weekday], minutes: hour * 60 + (+bag.minute) };
    } catch (e) {
      console.warn('[hlw business-hours] Unknown timezone "' + tz + '" — falling back to visitor local time.');
      var d = new Date();
      var keys = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
      return { day: keys[d.getDay()], minutes: d.getHours() * 60 + d.getMinutes() };
    }
  }

  function isOpen(hours, tz) {
    var now = nowInZone(tz);
    var range = parseRange(hours[now.day]);
    if (!range) return false;
    if (range.end <= range.start) return now.minutes >= range.start || now.minutes < range.end;
    return now.minutes >= range.start && now.minutes < range.end;
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function render(script) {
    var raw = script.getAttribute('data-hl-config');
    if (!raw) {
      console.warn('[hlw business-hours] Missing required data-hl-config (JSON weekly hours). Widget not rendered. See https://hayneslab.dev/tools/widgets/business-hours');
      return;
    }
    var hours;
    try {
      hours = JSON.parse(raw);
      if (!hours || typeof hours !== 'object') throw new Error('not an object');
    } catch (e) {
      console.warn('[hlw business-hours] data-hl-config is not valid JSON — widget not rendered.', e);
      return;
    }

    var tz = script.getAttribute('data-hl-timezone') || 'America/New_York';
    var title = script.hasAttribute('data-hl-title') ? script.getAttribute('data-hl-title') : 'Business hours';
    var showStatus = (script.getAttribute('data-hl-show-status') || 'true') !== 'false';
    var accent = script.getAttribute('data-hl-accent') || '#22c55e';

    injectStyles();

    var root = el('div', PREFIX);
    root.style.setProperty('--hlw-accent', accent);

    var badge = null;
    if (title !== '' || showStatus) {
      var header = el('div', PREFIX + '__header');
      if (title !== '') header.appendChild(el('h3', PREFIX + '__title', title));
      if (showStatus) badge = el('span', PREFIX + '__badge', '');
      if (badge) header.appendChild(badge);
      root.appendChild(header);
    }

    var todayKey = nowInZone(tz).day;
    var table = el('table', PREFIX + '__table');
    var tbody = document.createElement('tbody');
    DAYS.forEach(function (day) {
      var tr = el('tr', PREFIX + '__row' + (day === todayKey ? ' ' + PREFIX + '__row--today' : ''));
      tr.appendChild(el('td', PREFIX + '__day', DAY_LABELS[day]));
      var value = typeof hours[day] === 'string' && hours[day].trim() !== '' ? hours[day].trim() : 'closed';
      var range = parseRange(value);
      tr.appendChild(el('td', PREFIX + '__hours', range ? value.replace('-', '–') : 'Closed'));
      tbody.appendChild(tr);
    });
    table.appendChild(tbody);
    root.appendChild(table);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/business-hours';
    credit.target = '_blank';
    credit.rel = 'noopener';
    root.appendChild(credit);

    function updateStatus() {
      if (!badge) return;
      var open = isOpen(hours, tz);
      badge.textContent = open ? 'Open now' : 'Closed';
      badge.className = PREFIX + '__badge ' + (open ? PREFIX + '__badge--open' : PREFIX + '__badge--closed');
    }
    updateStatus();
    if (badge) setInterval(updateStatus, 60000);

    if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
      script.insertAdjacentElement('afterend', root);
    } else {
      document.body.appendChild(root);
    }
  }

  function init() {
    var script = document.currentScript ||
      document.querySelector('script[src*="widgets/business-hours.js"]');
    if (!script) {
      console.warn('[hlw business-hours] Could not locate its own <script> tag — widget not rendered.');
      return;
    }
    if (document.body) {
      render(script);
    } else {
      document.addEventListener('DOMContentLoaded', function () { render(script); });
    }
  }

  init();
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-config(required)JSON object keyed by lowercase day: {"mon": "9:00-17:00", "sat": "10:00-14:00", "sun": "closed"}. Times are 24-hour, in the timezone from data-hl-timezone.
data-hl-timezone"America/New_York"IANA timezone name the hours are defined in, e.g. "America/Chicago". The open/closed status is computed in this zone.
data-hl-title"Business hours"Heading displayed above the hours table. Set to "" to render no heading.
data-hl-show-status"true"When "true", a live "Open now" / "Closed" badge is shown next to the title.
data-hl-accent"#22c55e"Any CSS color for the "Open now" badge and current-day highlight.

Frequently asked questions

How do I install the business hours widget?

Paste the embed snippet where you want the hours table to appear and set data-hl-config to a JSON object mapping each day to a range like "9:00-17:00" or "closed". Set data-hl-timezone to your IANA timezone so the open/closed status is accurate.

How do I handle holidays or split shifts?

Update the JSON in data-hl-config — it is plain data, so seasonal hours are a quick edit. The widget renders one range per day; for split shifts, list your primary hours and note exceptions elsewhere on the page.

Does it work on WordPress, Shopify, or Squarespace?

Yes. It is dependency-free vanilla JS with scoped styles, so it runs anywhere you can paste a script tag or Custom HTML block. The open/closed badge computes on each page load from the visitor's clock against your timezone.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch