Tutorials
Last updated on:
Feb 5, 2025

How to control element visibility timing in Webflow

BRIX Templates Logo
Author
BRIX Templates
How to control element visibility timing in Webflow
Table of contents

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.

Why implement scheduled visibility controls in Webflow

Here are some common scenarios where visibility timing control proves invaluable:

  • Popup notifications: Display important announcements, new feature alerts, or special offers that appear once and don't annoy returning visitors.
  • Header notification bars: Show important updates or promotional banners on a schedule, like weekly specials or time-sensitive announcements.
  • Cookie consent banners: Present privacy notices just once per user, maintaining compliance without disrupting return visits.
  • Feature announcement modals: Introduce new functionality to users on a controlled schedule, avoiding overwhelming them with information.
  • Newsletter signup forms: Time the appearance of subscription prompts to optimize engagement while respecting user experience.
  • Limited-time offer cards: Display special promotions that appear periodically, like monthly deals or seasonal offerings.
Show elements by cookies local storage on Webflow

Understanding the Webflow element timing visibility attribute by BRIX Templates

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:

  • No complex coding: Simple attribute configuration handles all timing logic
  • Flexible scheduling: Choose between one-time display or periodic reappearance
  • Multi-element support: Apply timing controls to multiple elements independently, each with its own schedule
  • User-specific timing: Uses local storage to track individual user interactions
  • Smooth animations: Built-in fade effects for seamless appearance
  • Multiple timing patterns: Support for hours, days, weeks, months, and even before/after and between

How to implement visibility timing controls in Webflow

Let's break down the implementation process into clear, manageable steps.

1. How to add the timing control script to your Webflow project

1. Access your Webflow project settings

2. Navigate to the Custom Code section

3. Locate the "Before </body> tag" field

How to add visibility timing controls script in Webflow

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.

Setup visibility control element in Webflow page

The functionality will work exactly the same, but will only be active on that specific page.

2. How to configure element timing in Webflow

After adding the script, you can set up your elements:

  1. Select your target element in the Webflow Designer
  2. Open the element's Settings panel (gear icon)
  3. Locate the Custom Attributes section
  4. Add the timing configuration:

For basic timing control:

  • Name: brix-element-timing-visibility
  • Value: Your desired timing pattern (i.e. 4w, 1d, etc)
Setup show only once Webflow attribute

3. Understanding timing patterns for our Webflow attribute

The brix-element-timing-visibility attribute accepts several timing patterns:

Frequency-based patterns

  • Xh: Show once every X hours (e.g., 12h = every 12 hours)
  • Xd: Show once every X days (e.g., 3d = every 3 days)
  • Xw: Show once every X weeks (e.g., 2w = every 2 weeks)
  • Xm: Show once every X months (e.g., 1m = every month)
  • once or 0: Show only once, never reappear

Date-based patterns

  • before:YYYY-MM-DDThh:mm:ssZ: Show only before specified date/time (e.g., before:2024-12-31T23:59:59-0500 for Dec 31, 2024, 11:59 PM EST)
  • after:YYYY-MM-DDThh:mm:ssZ: Show only after specified date/time (e.g., after:2024-01-01T00:00:00-0500 for Jan 1, 2024, 12:00 AM EST)
  • between:YYYY-MM-DDThh:mm:ssZ,YYYY-MM-DDThh:mm:ssZ: Show only between specified dates/times (e.g., between:2024-01-01T00:00:00-0500,2024-12-31T23:59:59-0500 for the full year 2024 EST)

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.

4. How to handle initial element visibility on Webflow

To ensure smooth element appearance:

  1. Set your element's initial display state to none in Webflow
  2. The script will automatically handle showing the element when appropriate
  3. A built-in fade animation will smoothly reveal the element
  4. The timing will be tracked per user using local storage
Configure show only once element on Webflow

5. Steps before publishing your Webflow site with the new timing visibility option

  1. Verify all elements have proper brix-element-timing-visibility attributes with valid timing patterns
  2. Test the implementation on your staging environment (i.e. yourdomain.webflow.io)
  3. Check element behavior across different scenarios:
    • First-time visits
    • Return visits within the timing window
    • Return visits after the timing window
  4. Clear your browser's local storage to simulate fresh visits
  5. Once all tests pass, publish to your live domain (i.e. domain.com)

6. Common troubleshooting steps

If elements aren't appearing as expected:

  • Confirm your timing pattern format matches exactly (3d, 2w, etc.)
  • Verify the script is properly placed in your project's custom code section
  • Check that elements are set to display: none by default
  • Ensure you've published your site after making changes
  • Clear browser local storage to reset timing

Frequently asked questions about our Webflow attribute

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:

How to show a Webflow popup only once?

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.

How to make a notification bar appear on your Webflow site every 7 days?

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.

How to show a Webflow banner after a specific date?

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.

How to display a Webflow cookie consent banner only on first visit?

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.

How to manage seasonal content timing in your Webflow site?

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.

Conclusion

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.

BRIX Templates Logo
About BRIX Templates

At BRIX Templates we craft beautiful, modern and easy to use Webflow templates & UI Kits.

Explore our Webflow templates
Join the conversation
Join our monthly Webflow email newsletter!

Receive one monthly email newsletter with the best articles, resources, tutorials, and free cloneables from BRIX Templates!

Webflow Newsletter
Thanks for joining our Webflow email newsletter
Oops! Something went wrong while submitting the form.
BRIX Templates - Email Newsletter with Webflow ResourcesBRIX Templates - Email NewsletterBRIX Templates - Webflow Email Newsletter
How to add animated number counters to your Webflow site

How to add animated number counters to your Webflow site

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.

Feb 19, 2025
How to add URL copy button functionality in Webflow

How to add URL copy button functionality in Webflow

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.

Feb 19, 2025
How to add copy-to-clipboard functionality to a button in Webflow

How to add copy-to-clipboard functionality to a button in Webflow

Learn how to add copy-to-clipboard button to your Webflow site in 5 minutes or less.

Feb 12, 2025