While Webflow offers powerful design and interaction capabilities, it currently lacks built-in controls for scheduling when elements appear or managing their visibility timing. This limitation often creates challenges when you need to show elements on specific schedules or control how often they reappear for returning visitors.
This guide will walk you through implementing a powerful, attribute-based solution that gives you flexible control over when elements appear and reappear on your site.
Here are some common scenarios where visibility timing control proves invaluable:
Our solution introduces a powerful custom attribute (brix-element-timing-visibility="pattern") that gives you precise control over element visibility timing. This attribute-based approach offers several advantages:
Let's break down the implementation process into clear, manageable steps.
1. Access your Webflow project settings
2. Navigate to the Custom Code section
3. Locate the "Before </body> tag" field
4. Insert the following code:
<script>
/*!
* BRIX Templates Element Timing Visibility for Webflow
* ----------------------------------------------------------------------------
* Shows/hides elements based on their custom attribute:
* brix-element-timing-visibility="PATTERN"
*
* Patterns:
* 1) Frequency-based (stored in localStorage):
* "once", "0" => show once, never reappear
* "3d" => show once every 3 days
* "12h" => show once every 12 hours
* "2w" => show once every 2 weeks
* "1m" => show once every 1 month (30 days)
* 2) Date-based (no localStorage):
* "before:YYYY-MM-DDThh:mm:ssZ" => show only before a certain date/time
* "after:YYYY-MM-DDThh:mm:ssZ" => show only after a certain date/time
* "between:START,END" => show only between two date/times
*
* The script logs a summary in the console, listing how many elements were found,
* how they're categorized, and any relevant IDs or patterns.
*
* For each element that qualifies to be shown, we fade it in by switching
* from display:none to display:block and animating opacity over 0.5s.
*
* Usage steps:
* 1. Give your element an initial display:none in Webflow.
* 2. Add the attribute: brix-element-timing-visibility="PATTERN"
* 3. Publish your site.
* 4. The script automatically decides whether to show the element or not.
*
* Version: 1.0.0
* Author: BRIX Templates
*/
(function() {
'use strict';
//--------------------------------------------------------------------------
// 1) Fade animation duration (milliseconds)
//--------------------------------------------------------------------------
const FADE_DURATION = 500;
//--------------------------------------------------------------------------
// 1.1) Safe localStorage helpers
//--------------------------------------------------------------------------
function safeGetItem(key) {
try {
return localStorage.getItem(key);
} catch (err) {
console.warn('localStorage getItem failed:', err);
return null;
}
}
function safeSetItem(key, value) {
try {
localStorage.setItem(key, value);
} catch (err) {
console.warn('localStorage setItem failed:', err);
}
}
function safeRemoveItem(key) {
try {
localStorage.removeItem(key);
} catch (err) {
console.warn('localStorage removeItem failed:', err);
}
}
//--------------------------------------------------------------------------
// 2) Wait for DOM ready
//--------------------------------------------------------------------------
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initBrixTiming);
} else {
initBrixTiming();
}
//--------------------------------------------------------------------------
// 3) Main entry point
//--------------------------------------------------------------------------
function initBrixTiming() {
const allElements = document.querySelectorAll('[brix-element-timing-visibility]');
const total = allElements.length;
// Categorize each element
const frequencyElems = [];
const dateElems = [];
const onceElems = [];
let freqCount = 0;
let dateCount = 0;
let onceCount = 0;
allElements.forEach((elem, index) => {
const rawPattern = (elem.getAttribute('brix-element-timing-visibility') || '').trim();
// Determine the identifier for console logging:
// ID > joined class list > fallback element-N
let identifier = '';
if (elem.id) {
identifier = elem.id;
} else if (elem.classList && elem.classList.length > 0) {
identifier = Array.from(elem.classList).join('_');
} else {
identifier = `element-${index + 1}`;
}
if (isDatePattern(rawPattern)) {
// It's date-based
dateCount++;
dateElems.push({ elem, rawPattern, identifier, index });
} else if (rawPattern === 'once' || rawPattern === '0') {
// It's a single-time display
onceCount++;
onceElems.push({ elem, rawPattern, identifier, index });
} else {
// It's a frequency-based pattern
freqCount++;
frequencyElems.push({ elem, rawPattern, identifier, index });
}
});
// Build console summary log:
console.log('------------------------');
console.log(`BRIX Templates Element Timing Visibility for Webflow is activated, and has found ${total} element${total !== 1 ? 's' : ''} to manage:`);
// Frequency-based summary
if (freqCount > 0) {
console.log(`- ${freqCount} element${freqCount > 1 ? 's' : ''} scheduled by frequency:`);
frequencyElems.forEach(({ rawPattern, identifier }) => {
const friendly = getFriendlyFrequency(rawPattern);
console.log(` - ${friendly} (${identifier})`);
});
}
// Date-based summary
if (dateCount > 0) {
console.log(`- ${dateCount} element${dateCount > 1 ? 's' : ''} scheduled by date:`);
dateElems.forEach(({ rawPattern, identifier }) => {
const friendly = getFriendlyDateString(rawPattern);
console.log(` - ${friendly} (${identifier})`);
});
}
// Once-based summary
if (onceCount > 0) {
console.log(`- ${onceCount} element${onceCount > 1 ? 's' : ''} set to show only once:`);
onceElems.forEach(({ identifier }) => {
console.log(` - once (${identifier})`);
});
}
console.log('------------------------');
// Now process them all for actual show/hide logic
dateElems.forEach(({ elem, rawPattern }) => handleDateBased(elem, rawPattern));
onceElems.forEach(({ elem, rawPattern, index }) => handleFrequencyBased(elem, rawPattern, index));
frequencyElems.forEach(({ elem, rawPattern, index }) => handleFrequencyBased(elem, rawPattern, index));
}
//--------------------------------------------------------------------------
// 4) Distinguish date vs frequency patterns
//--------------------------------------------------------------------------
function isDatePattern(str) {
return (
str.startsWith('before:') ||
str.startsWith('after:') ||
str.startsWith('between:')
);
}
//--------------------------------------------------------------------------
// 5) Handle date-based pattern:
// e.g. "before:2024-12-31T23:59:59-0500"
// "after:2024-01-01T00:00:00Z"
// "between:2024-01-01T00:00:00Z,2024-12-31T23:59:59Z"
//--------------------------------------------------------------------------
function handleDateBased(elem, pattern) {
const now = new Date();
// BEFORE:
if (pattern.startsWith('before:')) {
const dateStr = pattern.replace('before:', '').trim();
const cutoff = new Date(dateStr);
if (isNaN(cutoff)) return; // invalid => don't show
if (now < cutoff) fadeInElement(elem);
return;
}
// AFTER:
if (pattern.startsWith('after:')) {
const dateStr = pattern.replace('after:', '').trim();
const cutoff = new Date(dateStr);
if (isNaN(cutoff)) return;
if (now > cutoff) fadeInElement(elem);
return;
}
// BETWEEN:
if (pattern.startsWith('between:')) {
const range = pattern.replace('between:', '').trim();
const [startStr, endStr] = range.split(',');
if (!startStr || !endStr) return;
const startDate = new Date(startStr.trim());
const endDate = new Date(endStr.trim());
if (isNaN(startDate) || isNaN(endDate)) return;
// New check: If start > end, warn and never show
if (startDate > endDate) {
console.warn(`"between:" pattern has startDate > endDate (${startStr} > ${endStr}). Element will not show.`);
return;
}
if (now >= startDate && now <= endDate) {
fadeInElement(elem);
}
return;
}
}
//--------------------------------------------------------------------------
// 6) Handle frequency-based pattern:
// e.g. "3d", "12h", "2w", "1m", "once"/"0"
//--------------------------------------------------------------------------
function handleFrequencyBased(elem, pattern, index) {
const key = generateKey(elem, pattern, index);
const now = Date.now();
// Try to read existing data from localStorage (safely)
let storedData = null;
const rawStored = safeGetItem(key);
if (rawStored) {
try {
storedData = JSON.parse(rawStored);
} catch (err) {
// If JSON parse fails, remove the item so we don't block
safeRemoveItem(key);
}
}
// If there's stored data but the pattern is different, reset it
if (storedData && storedData.pattern !== pattern) {
storedData = null;
safeRemoveItem(key);
}
// Decide if we can show
let canShow = false;
if (!storedData) {
// No data => never shown before
canShow = true;
} else {
const storedNextShow = storedData.nextShow;
if (typeof storedNextShow === 'number' && now >= storedNextShow) {
// Next show time has passed
canShow = true;
}
}
if (canShow) {
fadeInElement(elem);
// Compute new nextShow time
const nextShow = computeNextShow(pattern, now);
// If the pattern is "once"/"0" or invalid, we store a huge number
// Otherwise store the actual nextShow time
const finalNextShow = Number.isFinite(nextShow) ? nextShow : Number.MAX_SAFE_INTEGER;
// Save back to localStorage (safely)
const newData = {
pattern: pattern,
nextShow: finalNextShow
};
safeSetItem(key, JSON.stringify(newData));
}
}
//--------------------------------------------------------------------------
// 7) Generate localStorage key for a frequency-based element
//--------------------------------------------------------------------------
function generateKey(elem, pattern, index) {
// 1) If there's an ID, use: "brix-timing-{id}-{pattern}"
if (elem.id) {
return `brix-timing-${elem.id}-${pattern}`;
}
// 2) If there's at least one class, join them all + pattern
if (elem.classList && elem.classList.length > 0) {
const classString = Array.from(elem.classList).join('_');
return `brix-timing-${classString}-${pattern}`;
}
// 3) Fallback: "brix-timing-element-{index+1}-{pattern}"
console.warn(`No ID or classes found for element #${index+1}. Using index-based fallback key. If the DOM order changes, localStorage tracking might change.`);
return `brix-timing-element-${index + 1}-${pattern}`;
}
//--------------------------------------------------------------------------
// 8) Compute the next show time for a frequency pattern
//--------------------------------------------------------------------------
function computeNextShow(pattern, nowMillis) {
// "once" / "0" => indefinite
if (pattern === 'once' || pattern === '0') {
return Infinity;
}
// Attempt to parse "3d", "12h", "2w", "1m", etc.
const numeric = parseInt(pattern, 10);
if (isNaN(numeric) || numeric <= 0) {
console.warn(`Invalid frequency pattern "${pattern}". Defaulting to never show again.`);
return Infinity;
}
const lastChar = pattern.slice(-1).toLowerCase();
let multiplier = 0;
switch (lastChar) {
case 'h':
multiplier = 60 * 60 * 1000; // hours
break;
case 'd':
multiplier = 24 * 60 * 60 * 1000; // days
break;
case 'w':
multiplier = 7 * 24 * 60 * 60 * 1000; // weeks
break;
case 'm':
multiplier = 30 * 24 * 60 * 60 * 1000; // approx months
break;
default:
// fallback => treat as days
multiplier = 24 * 60 * 60 * 1000;
break;
}
return nowMillis + numeric * multiplier;
}
//--------------------------------------------------------------------------
// 9) Fade element in
//--------------------------------------------------------------------------
function fadeInElement(el) {
el.style.opacity = '0';
el.style.display = 'block';
el.style.transition = `opacity ${FADE_DURATION}ms ease`;
// Force reflow to ensure transition triggers (some browsers require reading offsetHeight)
// eslint-disable-next-line no-unused-expressions
el.offsetHeight;
el.style.opacity = '1';
}
//--------------------------------------------------------------------------
// 10) Convert a frequency pattern to a friendly label, e.g. "Every 3 days"
//--------------------------------------------------------------------------
function getFriendlyFrequency(pattern) {
if (pattern === 'once' || pattern === '0') {
return 'once';
}
const numeric = parseInt(pattern, 10);
if (isNaN(numeric) || numeric <= 0) {
return 'once';
}
// e.g. "3d", "12h", "2w", "1m"
const lastChar = pattern.slice(-1).toLowerCase();
switch (lastChar) {
case 'h':
return `Every ${numeric} hour${numeric > 1 ? 's' : ''}`;
case 'd':
return `Every ${numeric} day${numeric > 1 ? 's' : ''}`;
case 'w':
return `Every ${numeric} week${numeric > 1 ? 's' : ''}`;
case 'm':
return `Every ${numeric} month${numeric > 1 ? 's' : ''}`;
default:
return `Every ${numeric} day${numeric > 1 ? 's' : ''}`;
}
}
//--------------------------------------------------------------------------
// 11) Convert date-based pattern to a friendly label
//--------------------------------------------------------------------------
function getFriendlyDateString(pattern) {
// "before:2024-12-31T23:59:59-0500"
if (pattern.startsWith('before:')) {
const dateStr = pattern.replace('before:', '').trim();
return `Before ${dateStr}`;
}
// "after:2024-01-01T00:00:00Z"
if (pattern.startsWith('after:')) {
const dateStr = pattern.replace('after:', '').trim();
return `After ${dateStr}`;
}
// "between:DATE1,DATE2"
if (pattern.startsWith('between:')) {
const range = pattern.replace('between:', '').trim();
const [startStr, endStr] = range.split(',');
if (!startStr || !endStr) return `Between ???`;
return `Between ${startStr.trim()} and ${endStr.trim()}`;
}
return pattern;
}
})();
</script>
If you only need element timing control on a specific page rather than your entire site, you can add the script to individual page settings instead. Navigate to Pages in the Webflow Designer sidebar, select the desired page, click the gear icon to open Page Settings, then add the code to the Custom Code section in the Before </body> tag field.
The functionality will work exactly the same, but will only be active on that specific page.
After adding the script, you can set up your elements:
For basic timing control:
The brix-element-timing-visibility attribute accepts several timing patterns:
Frequency-based patterns
Date-based patterns
Date-based patterns use ISO 8601 format with timezone offset. The 'T' separates the date and time, and the timezone offset (e.g., '-0500' for EST) ensures precise timing across different regions.
For easier implementation, use our timing parameter generator below. This tool will help you create correctly formatted date-based parameters without worrying about ISO 8601 syntax or timezone calculations:
Need custom timing logic?
While these timing patterns cover many common scenarios, you might need more sophisticated controls for your specific use case. Whether you need conditional timing based on user behavior, complex scheduling patterns, or integration with your business logic, our Webflow agency can help create custom solutions tailored to your needs. Feel free to reach out to discuss your requirements.
To ensure smooth element appearance:
If elements aren't appearing as expected:
Some of the most common questions we get are around specific use cases for element timing in Webflow sites. Here are clear, practical answers to help you implement the most requested timing patterns:
Add the brix-element-timing-visibility="once" attribute to your popup div block in the Webflow Designer's custom attributes panel. Set the popup's initial display state to none, and it will show exactly once per user, then never appear again.
Add the brix-element-timing-visibility="7d" attribute to your notification bar element in Webflow. The notification will appear once every week for each user, automatically handling the timing and reappearance schedule.
Use the brix-element-timing-visibility="after:2024-12-31T00:00:00-0500" attribute on your banner element, adjusting the date and timezone as needed. The banner will remain hidden until the specified date, then appear automatically.
Apply the brix-element-timing-visibility="once" attribute to your cookie consent banner element. It will appear on the user's first visit and never show again, providing a clean experience for returning visitors.
Use the brix-element-timing-visibility="between:[dates]" attribute with appropriate date ranges for each seasonal element. Create separate elements for each season and set their visibility windows accordingly.
Implementing scheduled visibility controls in Webflow gives you powerful tools to create more dynamic, engaging user experiences. Whether you're managing promotional content, user notifications, or progressive feature reveals, this solution provides the flexibility and reliability you need while maintaining clean, professional designs.
Need help implementing advanced visibility patterns or exploring custom timing solutions? Our experienced Webflow development agency specializes in creating tailored solutions that enhance your site's functionality while maintaining optimal performance. Feel free to reach out to discuss how we can help optimize your Webflow project.
Learn how to add animated number counters to your Webflow site. This step-by-step guide explains how to do it in 5 minutes or less.
Learn how to add a copy URL button to your Webflow site. Step-by-step guide for implementing easy page sharing without plugins or code.
Learn how to add copy-to-clipboard button to your Webflow site in 5 minutes or less.