
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
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
<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
.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
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
[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
Last updated
October 20, 2025
Type
The Vault
Category
Utilities & Scripts
Need help?
Join Slack