OiO.lk Blog CSS Nuxt Gradient Border Timer Flickering in Firefox and Safari
CSS

Nuxt Gradient Border Timer Flickering in Firefox and Safari


I’m developing an application where I have a component that displays a gradient border timer around the entire viewport. The border animates over time to represent a countdown, and when it reaches a certain point, it starts blinking. The component works perfectly in Chrome and Edge, but in Firefox and Safari, the border flickers erratically, almost like a defective neon light.

I’ve tried several approaches to fix this issue, including changing how the SVG is embedded and how the animations are handled, but the problem persists.

Here is the code for my LayoutsGradientBorderTimer component:

<template>
    <div v-show="status" class="gradient-border" :style="[borderStyle, { opacity: borderOpacity }]"></div>
</template>

<script setup>
    import { ref, reactive, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
    import { gsap } from 'gsap';
    import { useSettingsStore } from '@/stores/settings';

    const settingsStore = useSettingsStore();

    const props = defineProps({
        status: {
            type: Boolean,
            required: true,
        },
        colors: {
            type: Array,
            required: true,
        },
        borderWidth: {
            type: Number,
            default: 10,
        },
    });

    const borderOpacity = ref(1);
    const blinkTriggered = ref(false);

    let timerTween = null;
    let blinkingTween = null;
    let vibrationInterval = null;

    // Utilisation de reactive pour borderStyle
    const borderStyle = reactive({
        'border-image-source': '',
        'border-image-slice': 1,
        'border-width': `${props.borderWidth}px`,
        'border-style': 'solid',
    });

    const createSVGUrl = (progress) => {
        const gradientStops = props.colors.map((color, index) => 
            `<stop offset="${(index / (props.colors.length - 1)) * 100}%" stop-color="${color}" />`
        ).join(' ');

        const svg = `
            <svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
                <defs>
                    <linearGradient id="grad" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="100">
                        ${gradientStops}
                    </linearGradient>
                </defs>
                <path
                    d="M50,0 L100,0 L100,100 L0,100 L0,0 L50,0"
                    stroke="url(#grad)"
                    stroke-width="${props.borderWidth}"
                    fill="none"
                    stroke-dasharray="400"
                    stroke-dashoffset="${-progress}"
                    stroke-linejoin="miter"
                    stroke-linecap="butt"
                />
            </svg>
        `;
        
        return `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}")`;
    };

    const startTimer = (duration, initialProgress = 0) => {
        const totalProgress = 400;
        const initialBorderProgress = totalProgress * initialProgress;
        const progressObject = { progress: initialBorderProgress };

        if (timerTween) {
            timerTween.kill();
        }

        timerTween = gsap.to(progressObject, {
            progress: totalProgress,
            duration,
            ease: 'none',
            onUpdate: () => {
                // Met à jour la source de l'image de la bordure
                borderStyle['border-image-source'] = createSVGUrl(progressObject.progress);
                handleBlinking(progressObject.progress, totalProgress);
            },
            onComplete: () => {
                endTimer();
            },
        });
    };

    const handleBlinking = (progress, totalProgress) => {
        const remainingProgress = (totalProgress - progress) / totalProgress;
        if (remainingProgress <= 1 / 3 && !blinkTriggered.value) {
            blinkTriggered.value = true;
            startBlinking();
        }
    };

    const resetTimer = () => {
        stopBlinking();
        nextTick(() => {
            const remainingTime = settingsStore.borderRealTime;
            const progress = 1 - (remainingTime / settingsStore.gradientBorderDuration);
            startTimer(remainingTime, progress);
        });
    };

    const endTimer = () => {
        stopBlinking();
        settingsStore.setBlinkingStatus(false);
    };

    const startBlinking = () => {
        if (blinkingTween) blinkingTween.kill();
        blinkingTween = gsap.to(borderOpacity, {
            value: 0,
            duration: 0.5,
            repeat: -1,
            yoyo: true,
            ease: 'power1.inOut'
        });
        settingsStore.setBlinkingStatus(true);
        startVibration();
    };

    const stopBlinking = () => {
        if (blinkingTween) {
            blinkingTween.kill();
            blinkingTween = null;
            borderOpacity.value = 1;
            blinkTriggered.value = false;
        }
        settingsStore.setBlinkingStatus(false);
        stopVibration();
    };

    const startVibration = () => {
        if (!vibrationInterval && settingsStore.vibrationEnabled) {
            vibrationInterval = setInterval(async () => {
                try {
                    await settingsStore.triggerPhoneVibration();
                } catch (error) {
                    console.error('Erreur lors de la vibration du téléphone :', error);
                    clearInterval(vibrationInterval);
                    vibrationInterval = null;
                    settingsStore.disableVibration();
                }
            }, 700);
        }
    };

    const stopVibration = () => {
        if (vibrationInterval) {
            clearInterval(vibrationInterval);
            vibrationInterval = null;
        }
    };

    // Watcher sur le prop borderWidth pour mettre à jour dynamiquement la largeur de la bordure
    watch(() => props.borderWidth, (newWidth) => {
        // Met à jour la largeur de la bordure dans le style
        borderStyle['border-width'] = `${newWidth}px`;
        // Redémarre le timer pour prendre en compte la nouvelle largeur dans le SVG
        if (props.status) {
            resetTimer();
        }
    });

    // Synchronisation avec les changements du status et de la durée
    watch(() => props.status, (newVal) => {
        if (newVal) {
            const remainingTime = settingsStore.borderRealTime;
            const progress = 1 - (remainingTime / settingsStore.gradientBorderDuration);
            startTimer(remainingTime, progress);
        } else {
            timerTween?.kill();
        }
    });

    watch(() => settingsStore.gradientBorderDuration, (newDuration) => {
        if (props.status) {
            resetTimer();
        }
    });

    watch(() => settingsStore.borderReset, (newVal) => {
        if (newVal) {
            resetTimer();
        }
    });

    onMounted(() => {
        // Initialisation de la largeur de la bordure
        borderStyle['border-width'] = `${props.borderWidth}px`;

        if (props.status) {
            const remainingTime = settingsStore.borderRealTime > 0 ? settingsStore.borderRealTime : settingsStore.gradientBorderDuration;
            const progress = 1 - (remainingTime / settingsStore.gradientBorderDuration);
            startTimer(remainingTime, progress);
        }
    });

    onBeforeUnmount(() => {
        if (timerTween) {
            timerTween.kill();
        }
    });
</script>

<style scoped>
    .gradient-border {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        border-style: solid;
        border-image-slice: 1;
        z-index: 30;
        pointer-events: none;
    }
</style>

Additional Information:

  • Vue Version: latest
  • Nuxt.js Version: 3.12.4
  • GSAP Version: 3.12.5
  • Browsers Affected: Firefox (v92+), Safari (v14+)
  • Browsers Working: Chrome, Edge

Question:

How can I fix the flickering issue of my animated gradient border timer in Firefox and Safari? Is there a known compatibility problem with animating SVG properties in these browsers, or am I missing something in my implementation? Any guidance on making this component work smoothly across all major browsers would be greatly appreciated.

What I’ve tried

Using Data URLs with Base64 Encoding:

Initially, I was updating the border-image-source CSS property with an SVG encoded as a data URL. I tried encoding the SVG in Base64 to improve compatibility:

const createSVGUrl = (progress) => {
    // ... SVG generation code ...
    const svgBase64 = btoa(unescape(encodeURIComponent(svg)));
    return `url("data:image/svg+xml;base64,${svgBase64}")`;
};

However, this approach still resulted in flickering on Firefox and Safari.

Embedding the SVG Directly in the DOM:

Based on some suggestions, I changed the component to embed the SVG directly in the DOM and animate its properties instead of updating the CSS border-image-source. The updated component is shown above.

Unfortunately, even with this change, the flickering issue persists on Firefox and Safari.

Checking for CSS Conflicts:

I reviewed the CSS to ensure there are no conflicts or styles that might interfere with the SVG rendering. The styles seem straightforward, and I couldn’t identify any issues.

Simplifying the SVG:

I tried simplifying the SVG to a basic rectangle without gradients or animations to see if the issue was related to SVG complexity. The flickering still occurred, suggesting the problem isn’t with the SVG content.

Testing Without Animations:

I removed the GSAP animations to check if they were causing the flickering. The border displayed correctly without animations, which indicates that the issue might be related to how GSAP interacts with the SVG in these browsers.



You need to sign in to view this answers

Exit mobile version