Search code examples
javascriptangular

Trying to make mobil scrollabe tabs


I'm trying to find an example of a component which works similar to how Uber Eats handles categories in a restaurant, meaning you have a header with tabs, and clicking a particular tab takes you to a section of the menu. Scrolling to a section also updates the header, and finally, since you can have as many tabs as the restaurant adds, the tabs need to scroll to the left while you scroll down.

Has anyone done something similar? Any libraries I could use? I think I could achieve this effect by using scroll events and adding css classes conditionally, but I would rather have a starting point instead of going at it blindly.

Link to video of the component I'm refering to: https://imgur.com/a/RB74zam


Solution

  • In the end you can use angular material in combination with angular cdk to build your own component.

    Learn more about Angular Material

    So, we create a simple overview component like this:

    HTML

    A simple container with the mat-tab-group. We need to set the selectedIndex and we need to handle the selectedTabChange event, too. Like this:

    <div class="container">
      <mat-tab-group
        [selectedIndex]="selectedTab"
        (selectedTabChange)="onTabChange($event.index)"
      >
        <mat-tab label="Section 1"></mat-tab>
        <mat-tab label="Section 2"></mat-tab>
        <mat-tab label="Section 3"></mat-tab>
      </mat-tab-group>
    
      <div class="scroll-container" cdkScrollable (scroll)="onScroll($event)">
        <section id="section1" #section1>
          <h2>Section 1</h2>
          <p>Content from Section 1...</p>
        </section>
        <section id="section2" #section2>
          <h2>Section 2</h2>
          <p>Content from Section 2...</p>
        </section>
        <section id="section3" #section3>
          <h2>Section 3</h2>
          <p>Content from Section 3...</p>
        </section>
      </div>
    </div>
    

    CSS

    Some styles for the content...

    .container {
      position: relative;
      height: 100vh;
      overflow: hidden;
    }
    
    .scroll-container {
      height: calc(100vh - 48px);
      overflow-y: scroll;
    }
    
    section {
      height: calc(100vh - 48px);
    
      box-sizing: border-box;
    }
    

    Code (TS)

    Here is the logic. First we need to diffent the cases do we change the index by our code or do we change the index by click on any tab. Next is to get the childrens position and set the header correctly.

    import { Component, ElementRef, ViewChild } from '@angular/core';
    import { MatTabsModule } from '@angular/material/tabs';
    import { ScrollingModule } from '@angular/cdk/scrolling';
    import { CommonModule } from '@angular/common';
    
    @Component({
      selector: 'app-overview',
      standalone: true,
      imports: [CommonModule, MatTabsModule, ScrollingModule],
      templateUrl: './overview.component.html',
      styleUrl: './overview.component.css',
    })
    export class OverviewComponent {
      selectedTab = 0;
      scrolling = false;
    
      @ViewChild('section1') section1!: ElementRef;
      @ViewChild('section2') section2!: ElementRef;
      @ViewChild('section3') section3!: ElementRef;
    
      sectionOffsets: number[] = [];
    
      ngAfterViewInit() {
        this.sectionOffsets = [
          this.section1.nativeElement.offsetTop,
          this.section2.nativeElement.offsetTop,
          this.section3.nativeElement.offsetTop,
        ];
      }
    
      onScroll(event: Event) {
        const scrollOffset = (event.target as HTMLElement).scrollTop;
        const buffer = 100; // Little buffer to detect the correct header
        this.scrolling = true;
    
        if (scrollOffset >= this.sectionOffsets[2] - buffer) {
          this.selectedTab = 2;
        } else if (scrollOffset >= this.sectionOffsets[1] - buffer) {
          this.selectedTab = 1;
        } else {
          this.selectedTab = 0;
        }
    
        console.log(this.selectedTab);
      }
    
      onTabChange(index: number) {
        if (!this.scrolling) {
          this.selectedTab = index;
    
          // Scrollen zur entsprechenden Sektion
          if (index === 0) {
            this.section1.nativeElement.scrollIntoView({ behavior: 'smooth' });
          } else if (index === 1) {
            this.section2.nativeElement.scrollIntoView({ behavior: 'smooth' });
          } else if (index === 2) {
            this.section3.nativeElement.scrollIntoView({ behavior: 'smooth' });
          }
        }
        this.scrolling = false;
      }
    }
    

    You will find a complete example in Stackblitz here.

    This is the result:

    enter image description here