Copied SVG to clipboard
Something went wrong
Copied code to clipboard
Something went wrong
Saved to bookmarks!
Removed from bookmarks
Webflow Challenge: Win $5K

Default

User image

Default

Name

  • -€50
    Upgrade to Lifetime
The Vault/

Opening Hours (Timetable)

Opening Hours (Timetable)

Documentation

Webflow

Code

Setup: External Scripts

External Scripts in Webflow

Make sure to always put the External Scripts before the Javascript step of the resource.

In this video you learn where to put these in your Webflow project? Or how to include a paid GSAP Club plugin in your project?

HTML

Copy

Step 1: Copy structure to Webflow

Copy structure to Webflow

In the video below we described how you can copy + paste the structure of this resource to your Webflow project.

Copy to Webflow

Webflow structure is not required for this resource.

Step 1: Add HTML

HTML

Copy

<div data-opening-hours-timezone="Europe/Amsterdam" data-opening-hours-init="" class="opening-hours">
  <div class="opening-hours__top">
    <h2 class="opening-hours__title">Opening hours</h2>
    <div class="opening-hours__status">
      <div class="opening-hours__status-bg"></div>
      <div class="opening-hours__status-dot"></div>
      <p class="opening-hours__p is--closed">Closed</p>
      <p class="opening-hours__p is--open">Open</p>
    </div>
  </div>
  <div class="opening-hours__timetable">
    <div data-opening-hours-day="monday" data-opening-hours-open="10:00" data-opening-hours-close="18:00" class="opening-hours__row">
      <div class="opening-hours__day">
        <p class="opening-hours__p">Monday</p>
      </div>
      <div class="opening-hours__time">
        <p class="opening-hours__p">10:00–18:00</p>
      </div>
    </div>
    <div data-opening-hours-current-day="" data-opening-hours-day="tuesday" data-opening-hours-open="9:00" data-opening-hours-close="18:00" class="opening-hours__row">
      <div class="opening-hours__day">
        <p class="opening-hours__p">Tuesday</p>
      </div>
      <div class="opening-hours__time">
        <p class="opening-hours__p">09:00–18:00</p>
      </div>
    </div>
    <div data-opening-hours-close="12:00" data-opening-hours-day="wednesday" data-opening-hours-open="9:00" class="opening-hours__row">
      <div class="opening-hours__day">
        <p class="opening-hours__p">Wednesday</p>
      </div>
      <div class="opening-hours__time">
        <p class="opening-hours__p">09:00–12:00</p>
      </div>
    </div>
    <div data-opening-hours-close="18:00" data-opening-hours-day="thursday" data-opening-hours-open="9:00" class="opening-hours__row">
      <div class="opening-hours__day">
        <p class="opening-hours__p">Thursday</p>
      </div>
      <div class="opening-hours__time">
        <p class="opening-hours__p">09:00–18:00</p>
      </div>
    </div>
    <div data-opening-hours-close="22:00" data-opening-hours-day="friday" data-opening-hours-open="9:00" class="opening-hours__row">
      <div class="opening-hours__day">
        <p class="opening-hours__p">Friday</p>
      </div>
      <div class="opening-hours__time">
        <p class="opening-hours__p">09:00–22:00</p>
      </div>
    </div>
    <div data-opening-hours-close="7:00" data-opening-hours-day="saturday" data-opening-hours-open="18:00" class="opening-hours__row">
      <div class="opening-hours__day">
        <p class="opening-hours__p">Saturday</p>
      </div>
      <div class="opening-hours__time">
        <p class="opening-hours__p">18:00–07:00</p>
      </div>
    </div>
    <div data-opening-hours-day="sunday" class="opening-hours__row">
      <div class="opening-hours__day">
        <p class="opening-hours__p">Sunday</p>
      </div>
      <div class="opening-hours__time">
        <p class="opening-hours__p">Closed</p>
      </div>
    </div>
  </div>
</div>

HTML structure is not required for this resource.

Step 2: Add CSS

CSS

Copy
.opening-hours {
  grid-column-gap: 1em;
  grid-row-gap: 1em;
  color: #000;
  background-color: #fff;
  border-radius: 1.5em;
  flex-flow: column;
  width: 100%;
  max-width: 20em;
  padding: 1em .5em .5em;
  display: flex;
}

.opening-hours__top {
  justify-content: space-between;
  align-items: center;
  padding-left: .75em;
  padding-right: .5em;
  display: flex;
}

.opening-hours__title {
  margin-top: 0;
  margin-bottom: 0;
  font-size: 1.25em;
  font-weight: 600;
  line-height: 1.25;
}

.opening-hours__status {
  grid-column-gap: .375em;
  grid-row-gap: .375em;
  color: #c00;
  border-radius: 3em;
  justify-content: space-between;
  align-items: center;
  padding: .5em .75em .5em .625em;
  display: flex;
  position: relative;
}

[data-opening-hours-store-status="open"] .opening-hours__status {
  color: #008214;
}

.opening-hours__status-bg {
  opacity: .08;
  background-color: currentColor;
  border-radius: 50em;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

.opening-hours__status-dot {
  background-color: currentColor;
  border-radius: 50%;
  width: .75em;
  height: .75em;
  position: relative;
}

.opening-hours__timetable {
  grid-column-gap: .125em;
  grid-row-gap: .125em;
  flex-flow: column;
  display: flex;
}

.opening-hours__row {
  color: #333;
  border: 1px solid #0000;
  border-radius: 50em;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: .5em .75em;
  display: flex;
}

[data-opening-hours-current-day] {
  background-color: rgba(0, 0, 0, 0.05);
  border: 1px solid rgba(0, 0, 0, 0.05);
}

.opening-hours__p {
  margin-top: 0;
  margin-bottom: 0;
  font-size: 1em;
  font-weight: 400;
  line-height: 1;
  position: relative;
}

.opening-hours__p.is--open {
  display: none;
}

[data-opening-hours-current-day] .opening-hours__p {
  font-weight: 600;
  color: #000;
}

[data-opening-hours-store-status="open"] .opening-hours__status .opening-hours__p.is--closed {
  display: none;
}

[data-opening-hours-store-status="open"] .opening-hours__status .opening-hours__p.is--open {
  display: block;
}

Step 2: Add custom Javascript

Custom Javascript in Webflow

In this video, Ilja gives you some guidance about using JavaScript in Webflow:

Step 2: Add Javascript

Step 3: Add Javascript

Javascript

Copy
function initOpeningHours() {
  const defaultTimezone = "Europe/Amsterdam";
  const timeTables = document.querySelectorAll('[data-opening-hours-init]');
  if (!timeTables.length) return;

  timeTables.forEach(root => {
    const tz = root.getAttribute('data-opening-hours-timezone') || defaultTimezone;

    const timeToMinutes = str => {
      const m = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(str || '');
      return m ? (parseInt(m[1], 10) * 60 + parseInt(m[2], 10)) : null;
    };

    const getNowParts = () => {
      let useTz = tz;
      try { new Intl.DateTimeFormat('en-GB', { timeZone: tz }); }
      catch { useTz = defaultTimezone; }
      const fmt = new Intl.DateTimeFormat('en-GB', {
        timeZone: useTz,
        weekday: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      });
      const parts = fmt.formatToParts(new Date());
      const map = Object.fromEntries(parts.map(p => [p.type, p.value]));
      const weekdayIdx = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'].indexOf(map.weekday);
      return { weekdayIdx, hour: parseInt(map.hour,10), minute: parseInt(map.minute,10) };
    };

    const dayIndex = { monday:0, tuesday:1, wednesday:2, thursday:3, friday:4, saturday:5, sunday:6 };
    const rows = Array.from(root.querySelectorAll('[data-opening-hours-day]'));
    if (!rows.length) return;

    // check duplicates
    const dayCount = {};
    rows.forEach(r => {
      const d = (r.getAttribute('data-opening-hours-day') || '').trim().toLowerCase();
      if (d) dayCount[d] = (dayCount[d] || 0) + 1;
    });
    Object.keys(dayCount).forEach(d => {
      if (dayCount[d] > 1) console.error(`[OpeningHours] Duplicate day "${d}" found in`, root);
    });

    const ordered = new Array(7);
    rows.forEach(r => {
      const d = (r.getAttribute('data-opening-hours-day') || '').trim().toLowerCase();
      if (d in dayIndex) ordered[dayIndex[d]] = r;
    });
    if (ordered.some(r => !r)) return;

    const schedule = ordered.map(row => {
      const o = (row.getAttribute('data-opening-hours-open')  || '').trim();
      const c = (row.getAttribute('data-opening-hours-close') || '').trim();
      const openMin  = timeToMinutes(o);
      const closeMin = timeToMinutes(c);
      if (openMin == null || closeMin == null) return { open:false, openMin:0, closeMin:0, overnight:false };
      const overnight = openMin > closeMin;
      return { open:true, openMin, closeMin, overnight };
    });

    const evaluate = () => {
      const now = getNowParts();
      const curIdx = now.weekdayIdx;
      const nowMin = now.hour * 60 + now.minute;

      // mark current day
      ordered.forEach(r => r.removeAttribute('data-opening-hours-current-day'));
      ordered[curIdx].setAttribute('data-opening-hours-current-day', '');

      const today = schedule[curIdx];
      const yesterday = schedule[(curIdx + 6) % 7];

      let isOpen = false;
      if (today.open) {
        if (!today.overnight) {
          isOpen = nowMin >= today.openMin && nowMin < today.closeMin;
        } else {
          isOpen = (nowMin >= today.openMin) || (nowMin < today.closeMin);
        }
      }
      if (!isOpen && yesterday.open && yesterday.overnight && nowMin < yesterday.closeMin) {
        isOpen = true;
      }

      ordered.forEach((row, idx) => {
        row.setAttribute('data-opening-hours-status', (idx === curIdx && isOpen) ? 'open' : 'closed');
      });

      root.setAttribute('data-opening-hours-store-status', isOpen ? 'open' : 'closed');
    };

    evaluate();
    clearInterval(root._openingHoursTimer);
    root._openingHoursTimer = setInterval(evaluate, 60 * 1000);

    const visHandler = () => { if (!document.hidden) evaluate(); };
    if (root._openingHoursVisHandler) {
      document.removeEventListener('visibilitychange', root._openingHoursVisHandler);
    }
    root._openingHoursVisHandler = visHandler;
    document.addEventListener('visibilitychange', visHandler);
  });
}

// Initialize Opening Hours (Timetable)
document.addEventListener('DOMContentLoaded', () => {
  initOpeningHours();
});

Step 3: Add custom CSS

Step 2: Add custom CSS

Custom CSS in Webflow

Curious about where to put custom CSS in Webflow? Ilja explains it in the below video:

CSS

Copy
[data-opening-hours-store-status="open"] .opening-hours__status {
  color: #008214;
}

[data-opening-hours-store-status="open"] .opening-hours__status .opening-hours__p.is--closed {
  display: none;
}

[data-opening-hours-store-status="open"] .opening-hours__status .opening-hours__p.is--open {
  display: block;
}

[data-opening-hours-current-day] {
  background-color: rgba(0, 0, 0, 0.05);
  border: 1px solid rgba(0, 0, 0, 0.05);
}

[data-opening-hours-current-day] .opening-hours__p {
  font-weight: 600;
  color: #000;
}

Implementation

Container

Wrap one weekly timetable in a root that initializes the script with [data-opening-hours-init].

<div data-opening-hours-init data-opening-hours-timezone="Europe/Amsterdam">
  <div data-opening-hours-day="monday" data-opening-hours-open="09:00" data-opening-hours-close="18:00"></div>
  <!-- Other 6 days of the week -->
</div>

Timezone

Set a per table timezone using [data-opening-hours-timezone="Europe/Amsterdam"], falling back to the internal default set in the Javascript.

You can find a full list with timezone (TZ Identifier) options on Wikipedia.

Examples:

  • Africa/Lagos
  • America/New_York
  • Asia/Tel_Aviv
  • Australia/West

Store status

The script exposes the overall state on the container using [data-opening-hours-store-status="open/closed"], so you can drive a single global status badge without querying rows.

Day

Identify each row by weekday using [data-opening-hours-day="monday"], with the HTML order ignored since indexing is based on the attribute.

Values: monday, tuesday, wednesday, thursday, friday, saturday, sunday

Open

Provide the start time in 24 hour format using [data-opening-hours-open="HH:MM"], where values like 18:00 are valid.

Close

Provide the end time in 24 hour format using [data-opening-hours-close="HH:MM"], where an end earlier than the start marks the range as overnight.

<div data-opening-hours-day="monday" data-opening-hours-open="09:00" data-opening-hours-close="18:00"></div>

Status per day

The script marks the current day row as open or closed using [data-opening-hours-status="open/closed"], which you can use for row styling.

Current day

The script flags today’s row using the boolean attribute [data-opening-hours-current-day], allowing simple selectors to highlight the active weekday.

Closed days

Leave a weekday without [data-opening-hours-open] and [data-opening-hours-close] to mark it as closed, letting the script automatically treat it as inactive in the timetable.

<div data-opening-hours-day="sunday">Closed</div>

Overnight logic

If the start exceeds the end the window is treated as overnight by comparing the current time against both today and yesterday when needed using [data-opening-hours-open] and [data-opening-hours-close].

<div data-opening-hours-day="saturday" data-opening-hours-open="21:00" data-opening-hours-close="7:00"></div>

Resource Details

Javascript
Time
Script
Basic
Timetable
Days

Original source

Dennis Snellenberg

Creator Credits

We always strive to credit creators as accurately as possible. While similar concepts might appear online, we aim to provide proper and respectful attribution.