Building a Chrome-Style Search and Navigation Feature with Angular

This post picks up where the previous article on highlighting text from user input left off. You can find that earlier piece at https://dev.to/this-is-angular/search-and-highlight-text-feature-using-angular-l98, since it lays the groundwork for what we're about to build — a search experience with navigation controls and position markers, much like what you'd see in Chrome.

Starting from the existing codebase, the plan is to add a vertical bar on the right edge of the screen. This bar will display the positions of all highlighted text instances relative to the page height, giving you a quick visual overview. It will also include navigation arrows so you can jump between individual matches. To place the markers correctly, we need the offset from the top for each highlight, which we can obtain like this:

marker height: number = 0;
markerTop: number = 0;
@ViewChild('textContainer') textContainer!:ElementRef;

ngAfterContentInit(): void {
    setTimeout(() => {
      const textContainerRect =
        this.textContainer.nativeElement.getBoundingClientRect();
      this.markerHeight = textContainerRect.height;
      this.markerTop = textContainerRect.top;
    }, 100);
  }

Enter fullscreen mode Exit fullscreen mode
<div *ngIf="markerHeight>0 &&markerTop>0 " class="marker" [ngStyle]="{'height':markerHeight+'px','top':markerTop+'px'}" ></div>
Enter fullscreen mode Exit fullscreen mode

With the markers in place, the interface will look something like this on the screen:

Search text and navigate -Chrome alike feature with marker locations using Angular — figure 1

The next step is figuring out how to pinpoint each highlighted occurrence. Every match carries the highlighted-text class, which we haven't changed from the earlier implementation. You can rename this class to suit your own preferences if needed. We're going to write a method that scans for all elements with this class after the highlighting logic has run, then stores their vertical positions into an array. That array drives the tick marks on the sidebar.

getMarkerTicks() {
    setTimeout(() => {

      this.searchElements =
        this.textContainer.nativeElement.querySelectorAll('.highlighted-text');
      const markerTicks = this.searchElements.forEach((element: any) => {


        this.markerTicks.push(
          (this.markerHeight / this.scrollHeight) *
            element.getBoundingClientRect().top
        );
      });
      this.activeIndex = 1;

      this.textContainer.nativeElement
        .querySelectorAll('.highlighted-text')[0]
        .scrollIntoView({ block: 'center', behavior: 'smooth' });
    }, 100);
  }
Enter fullscreen mode Exit fullscreen mode

Once we have the positions, we can build the HTML that renders a tick for each distinct match location:

    <div class="markersTicks" *ngFor="let m of markerTicks" [ngStyle]="{'top':m+'px'}" ></div>

Enter fullscreen mode Exit fullscreen mode

To round out the interface, we still need a way to display the current match count and provide controls for moving through them. A disabled input field to show the counter, paired with two arrow buttons, fits this purpose well:

  <input class="input-navigator" type="text" disabled value="{{ activeIndex + ' of '+  markerTicks.length}}" >
    <div class="nav-btns">
      <img class="nav-btn" (click)="moveToNext()" src="../assets/down-arrow.png" alt="">
      <img  class="nav-btn" (click)="moveToBack()"  src="../assets/up-arrow.png" alt="">
    </div>
Enter fullscreen mode Exit fullscreen mode

Of course, the navigation arrows need logic behind them. These functions will handle moving the selection to the previous or next highlighted item:

moveToNext() {
    const highlightedSpans =
      this.textContainer.nativeElement.querySelectorAll('.highlighted-text');
    if (highlightedSpans.length > 0) {
      if (this.activeIndex === highlightedSpans.length) {
        this.activeIndex = 0;
      }
      this.activeIndex++;

      this.searchElements[this.activeIndex - 1].focus();

    }
  }
  moveToBack() {
    const highlightedSpans =
      this.textContainer.nativeElement.querySelectorAll('.highlighted-text');

    if (highlightedSpans.length > 0) {
      if (this.activeIndex === 1) {
        this.activeIndex = highlightedSpans.length + 1;
      }
      this.activeIndex--;
      this.searchElements[this.activeIndex - 1].focus();
    }
  }
Enter fullscreen mode Exit fullscreen mode

With everything wired together, the application is now fully capable of searching and cycling through results:

done screen

The complete source is available on GitHub, and you can try the live demo at https://nikhild64.github.io/highlight-text-navigator/.

If this approach helped you, consider sharing it with others. For any thoughts, feedback, or questions, feel free to reach me on Twitter or drop a comment below.

Until next time — happy coding!