Angular Example: Smooth Scroll

ts
import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  signal,
  viewChild,
} from '@angular/core'
import { elementScroll, injectVirtualizer } from '@tanstack/angular-virtual'

function easeInOutQuint(t: number) {
  return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t
}

@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>
      This smooth scroll example uses the <code>scrollToFn</code> to implement a
      custom scrolling function for the methods like
      <code>scrollToIndex</code> and <code>scrollToOffset</code>
    </p>
    <div>
      <button (click)="scrollToRandomIndex()">
        Scroll To Random Index ({{ randomIndex() }})
      </button>
    </div>
    <br />
    <div #scrollElement class="list scroll-container">
      <div
        style="position: relative; width: 100%;"
        [style.height.px]="virtualizer.getTotalSize()"
      >
        @for (row of virtualizer.getVirtualItems(); track row.index) {
          <div
            [class.list-item-even]="row.index % 2 === 0"
            [class.list-item-odd]="row.index % 2 !== 0"
            style="position: absolute; top: 0; left: 0; width: 100%;"
            [style.height.px]="row.size"
            [style.transform]="'translateY(' + row.start + 'px)'"
          >
            Row {{ row.index }}
          </div>
        }
      </div>
    </div>
  `,
  styles: `
    .scroll-container {
      height: 200px;
      width: 400px;
      overflow: auto;
    }
  `,
})
export class AppComponent {
  scrollElement = viewChild<ElementRef<HTMLDivElement>>('scrollElement')

  scrollingTime = signal(0)

  virtualizer = injectVirtualizer(() => ({
    scrollElement: this.scrollElement(),
    count: 10000,
    estimateSize: () => 35,
    overscan: 5,
    scrollToFn: (offset, options, instance) => {
      const duration = 1000
      const start = this.scrollElement()!.nativeElement.scrollTop
      const startTime = Date.now()
      this.scrollingTime.set(startTime)

      const run = () => {
        if (this.scrollingTime() !== startTime) return
        const now = Date.now()
        const elapsed = now - startTime
        const progress = easeInOutQuint(Math.min(elapsed / duration, 1))
        const interpolated = start + (offset - start) * progress

        if (elapsed < duration) {
          elementScroll(interpolated, options, instance)
          requestAnimationFrame(run)
        } else {
          elementScroll(interpolated, options, instance)
        }
      }
      requestAnimationFrame(run)
    },
  }))

  randomIndex = signal(Math.floor(Math.random() * 10000))

  scrollToRandomIndex() {
    this.virtualizer.scrollToIndex(this.randomIndex())
    this.randomIndex.set(Math.floor(Math.random() * 10000))
  }
}
import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  signal,
  viewChild,
} from '@angular/core'
import { elementScroll, injectVirtualizer } from '@tanstack/angular-virtual'

function easeInOutQuint(t: number) {
  return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t
}

@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>
      This smooth scroll example uses the <code>scrollToFn</code> to implement a
      custom scrolling function for the methods like
      <code>scrollToIndex</code> and <code>scrollToOffset</code>
    </p>
    <div>
      <button (click)="scrollToRandomIndex()">
        Scroll To Random Index ({{ randomIndex() }})
      </button>
    </div>
    <br />
    <div #scrollElement class="list scroll-container">
      <div
        style="position: relative; width: 100%;"
        [style.height.px]="virtualizer.getTotalSize()"
      >
        @for (row of virtualizer.getVirtualItems(); track row.index) {
          <div
            [class.list-item-even]="row.index % 2 === 0"
            [class.list-item-odd]="row.index % 2 !== 0"
            style="position: absolute; top: 0; left: 0; width: 100%;"
            [style.height.px]="row.size"
            [style.transform]="'translateY(' + row.start + 'px)'"
          >
            Row {{ row.index }}
          </div>
        }
      </div>
    </div>
  `,
  styles: `
    .scroll-container {
      height: 200px;
      width: 400px;
      overflow: auto;
    }
  `,
})
export class AppComponent {
  scrollElement = viewChild<ElementRef<HTMLDivElement>>('scrollElement')

  scrollingTime = signal(0)

  virtualizer = injectVirtualizer(() => ({
    scrollElement: this.scrollElement(),
    count: 10000,
    estimateSize: () => 35,
    overscan: 5,
    scrollToFn: (offset, options, instance) => {
      const duration = 1000
      const start = this.scrollElement()!.nativeElement.scrollTop
      const startTime = Date.now()
      this.scrollingTime.set(startTime)

      const run = () => {
        if (this.scrollingTime() !== startTime) return
        const now = Date.now()
        const elapsed = now - startTime
        const progress = easeInOutQuint(Math.min(elapsed / duration, 1))
        const interpolated = start + (offset - start) * progress

        if (elapsed < duration) {
          elementScroll(interpolated, options, instance)
          requestAnimationFrame(run)
        } else {
          elementScroll(interpolated, options, instance)
        }
      }
      requestAnimationFrame(run)
    },
  }))

  randomIndex = signal(Math.floor(Math.random() * 10000))

  scrollToRandomIndex() {
    this.virtualizer.scrollToIndex(this.randomIndex())
    this.randomIndex.set(Math.floor(Math.random() * 10000))
  }
}
Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.

Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.