angulartypescriptintersection-observer

IntersectionObserver causes flickering when the element is only partially in view


I'm working on an Angular 18 project where I want to animate a button when it comes into view using the IntersectionObserver API and it works.

However, if only half of the top button is in view, it starts flickering, which is the problem I am trying to solve.

Here's my component code:

export class FooterComponent implements AfterViewInit {
  @ViewChild('invite_btn') protected invite_btn!: ElementRef<HTMLAnchorElement>;

  ngAfterViewInit() {
    const observer: IntersectionObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.invite_btn.nativeElement.classList.add('animate__animated', 'animate__fadeInUp');
        } else {
          this.invite_btn.nativeElement.classList.remove('animate__animated', 'animate__fadeInUp');
        }
      });
    });

    observer.observe(this.invite_btn.nativeElement);
  }
}

I tried to use an entry.IntersectionRatio check but that didn't help, because the ratio is always below 1.0.

How can I fix that? Video example:

flickering issue showcase


Solution

  • Based on the animation CSS classes I assume that you are using Animate.css or something similar.

    The flickering appears to be caused by the translate transform applied by the fadeInOut animation.

    1. The IntersectionObserver detects that the button has entered the viewport
    2. The animation initially applies transform: translate3d(0px, 100%, 0px); which moves the element out of the viewport.
    3. The IntersectionObserver detects that the button has exited the viewport
    4. The animation progresses and brings the button back into the viewport.
    5. Go back to step 1.

    We now have an infinite loop.

    To circumvent the issue, you can wrap the button in a container element and observe it with IntersectObserver instead of the button itself.

    <!-- 
      Template of the FooterComponent
      The fadeInUp animation applies a translate transform on the button which causes isIntersecting to rapidly switch between true/false.
    
      To workaround that we introduce a container element. 
      We'll use it to test for intersection instead of the button.
     -->
    <div #invite_btn_container>
      @if(isIntersecting()) {
      <!-- 
        Remove the button completely when it is not intersecting. 
        Otherwise the button jumps down at the start of the animation.
      -->
      <a #invite_btn class="animate__animated animate__fadeInUp">Invite</a>
      }
    </div>
    
    export class FooterComponent implements AfterViewInit {
      @ViewChild('invite_btn_container')
      protected invite_btn_container!: ElementRef<HTMLElement>;
    
      isIntersecting = signal(false);
    
      ngAfterViewInit() {
        const observer: IntersectionObserver = new IntersectionObserver(
          (entries) => {
            entries.forEach((entry) => {
              // Use a signal to make sure that Change Detection is triggered.
              // Alternatively, use a regular boolean property and call ChangeDetectorRef.markForCheck()
              this.isIntersecting.set(entry.isIntersecting);
            });
          }
        );
    
        // Observe the button container for intersection, instead of the button itself
        observer.observe(this.invite_btn_container.nativeElement);
      }
    }
    

    Hope that helps :)