diff --git a/projects/v3/src/app/components/list-item/list-item.component.html b/projects/v3/src/app/components/list-item/list-item.component.html
index 55833a6bf..8604d62fe 100644
--- a/projects/v3/src/app/components/list-item/list-item.component.html
+++ b/projects/v3/src/app/components/list-item/list-item.component.html
@@ -1,8 +1,11 @@
 <ion-item [id]="id"
   [ngClass]="{'active': active, 'hasCallToActionBtn': callToActionBtn}"
   [lines]="lines"
-  tabindex="0"
-  role="listitem">
+  [button]="button"
+  [attr.tabindex]="button ? 0 : null"
+  [attr.role]="itemRole || (button ? 'button' : 'listitem')"
+  [attr.aria-selected]="ariaSelected === undefined ? null : (ariaSelected ? 'true' : 'false')"
+  [attr.aria-current]="ariaCurrent || null">
   <ng-container *ngIf="!loading; else loadingSkeleton">
     <div class="icon-container activitypage" *ngIf="!leadImage">
       <ion-icon *ngIf="leadingIcon"
diff --git a/projects/v3/src/app/components/list-item/list-item.component.ts b/projects/v3/src/app/components/list-item/list-item.component.ts
index bedc067f0..c645b24d5 100644
--- a/projects/v3/src/app/components/list-item/list-item.component.ts
+++ b/projects/v3/src/app/components/list-item/list-item.component.ts
@@ -49,6 +49,10 @@ export class ListItemComponent {
 
   // used if there are ending action buttons
   @Input() endingActionBtnIcons: string[];
+  @Input() itemRole: string = 'listitem';
+  @Input() ariaSelected?: boolean;
+  @Input() ariaCurrent?: string;
+  @Input() button = false;
   // named as "any" to support any callback parameter format
   @Output() anyBtnClick = new EventEmitter<any>();
   @Output() actionBtnClick = new EventEmitter<number>();
diff --git a/projects/v3/src/app/components/review-list/review-list.component.html b/projects/v3/src/app/components/review-list/review-list.component.html
index 579b338b8..ed6185048 100644
--- a/projects/v3/src/app/components/review-list/review-list.component.html
+++ b/projects/v3/src/app/components/review-list/review-list.component.html
@@ -1,4 +1,23 @@
-<ion-segment (ionChange)="switchStatus()" value="pending">
+<div class="sr-only" [id]="searchLabelId" i18n>Search reviews</div>
+<div class="sr-only" [id]="searchHintId" i18n>Type a review name to filter the list.</div>
+
+<ion-searchbar
+  class="review-search"
+  [value]="searchTerm"
+  (ionInput)="onSearchTermChange($event.detail.value)"
+  [debounce]="200"
+  [attr.aria-labelledby]="searchLabelId"
+  [attr.aria-describedby]="searchHintId"
+  i18n-placeholder
+  placeholder="Search reviews"
+></ion-searchbar>
+
+<ion-segment
+  (ionChange)="switchStatus($event)"
+  [value]="segmentValue"
+  [attr.aria-controls]="listId"
+  aria-label="Filter reviews by status" i18n-aria-label
+>
   <ion-segment-button value="pending" mode="ios">
     <ion-label i18n>Pending</ion-label>
   </ion-segment-button>
@@ -7,11 +26,18 @@
   </ion-segment-button>
 </ion-segment>
 
-<ion-list *ngIf="reviews">
-  <ng-container *ngFor="let review of reviews">
+<h2 class="sr-only" [id]="listLabelId" i18n>Review list</h2>
+
+<ion-list *ngIf="reviews"
+  [attr.id]="listId"
+  role="list"
+  [attr.aria-labelledby]="listLabelId"
+>
+  <ng-container *ngFor="let review of filteredReviews; trackBy: trackBySubmission">
     <app-list-item
+      #reviewItem
       class="focusable"
-      *ngIf="review.isDone === showDone"
+      [id]="'review-' + review.submissionId"
       [title]="review.name"
       [subtitle1]="review.submitterName"
       subtitle1Color="grey-75"
@@ -19,23 +45,32 @@
       subtitle2Color="grey-75"
       leadingIcon="eye"
       [active]="currentReview && currentReview.submissionId === review.submissionId"
+      [button]="true"
+      [ariaSelected]="currentReview && currentReview.submissionId === review.submissionId"
+      [ariaCurrent]="currentReview && currentReview.submissionId === review.submissionId ? 'true' : null"
       lines="full"
       (click)="goto(review)"
       (keydown)="goto(review, $event)"
       [endingText]="review.date"
       endingTextColor="grey-75"
-      tabindex="0"
-      role="button"
     ></app-list-item>
   </ng-container>
 </ion-list>
 
-<ion-list *ngIf="reviews === null">
+<p *ngIf="resultsAnnouncement" class="sr-only" aria-live="polite">{{ resultsAnnouncement }}</p>
+
+<ion-list *ngIf="reviews === null" role="status" aria-live="polite">
   <app-list-item [loading]="true"></app-list-item>
   <app-list-item [loading]="true"></app-list-item>
 </ion-list>
 
-<div *ngIf="noReviews" class="ion-text-center no-review">
+<div *ngIf="hasSearchWithoutResults" class="ion-text-center no-review" role="status" aria-live="polite">
+  <ion-icon class="large-icon" color="primary" name="search-outline"></ion-icon>
+  <p class="body-1" i18n>No reviews match your search.</p>
+  <ion-text class="subtitle-2" color="grey-75" i18n>Try a different search term to find a review.</ion-text>
+</div>
+
+<div *ngIf="!hasSearchWithoutResults && noReviews" class="ion-text-center no-review" role="status" aria-live="polite">
   <ion-icon class="large-icon" color="primary" name="chatbox-ellipses-outline"></ion-icon>
   <p class="body-1" i18n="Sample: You have no pending/completed review yet!">You have no {{ noReviews }} review yet!</p>
   <ion-text class="subtitle-2" color="grey-75" i18n>Reviews show up here, so you can easily view them here later.</ion-text>
diff --git a/projects/v3/src/app/components/review-list/review-list.component.scss b/projects/v3/src/app/components/review-list/review-list.component.scss
index ab76c1d68..3cc6270f7 100644
--- a/projects/v3/src/app/components/review-list/review-list.component.scss
+++ b/projects/v3/src/app/components/review-list/review-list.component.scss
@@ -12,7 +12,28 @@ ion-segment {
     border: 1px solid var(--ion-color-primary);
   }
 }
-.focusable:focus {
-  border: 1px solid var(--ion-color-primary);
-  display: block;
+
+.review-search {
+  padding: 0;
+}
+.sr-only {
+  border: 0;
+  clip: rect(0 0 0 0);
+  clip-path: inset(50%);
+  height: 1px;
+  margin: -1px;
+  overflow: hidden;
+  padding: 0;
+  position: absolute;
+  width: 1px;
+  white-space: nowrap;
+}
+
+.focusable ::ng-deep ion-item:focus-visible {
+  outline: 2px solid var(--ion-color-primary);
+  outline-offset: 2px;
+}
+
+.focusable ::ng-deep ion-item.active {
+  border-left: 4px solid var(--ion-color-primary);
 }
diff --git a/projects/v3/src/app/components/review-list/review-list.component.spec.ts b/projects/v3/src/app/components/review-list/review-list.component.spec.ts
index 282c2a126..1a4217a26 100644
--- a/projects/v3/src/app/components/review-list/review-list.component.spec.ts
+++ b/projects/v3/src/app/components/review-list/review-list.component.spec.ts
@@ -1,4 +1,6 @@
 import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
+import { FormsModule } from '@angular/forms';
 import { IonicModule } from '@ionic/angular';
 
 import { ReviewListComponent } from './review-list.component';
@@ -10,7 +12,8 @@ describe('ReviewListComponent', () => {
   beforeEach(waitForAsync(() => {
     TestBed.configureTestingModule({
       declarations: [ ReviewListComponent ],
-      imports: [IonicModule.forRoot()]
+      imports: [IonicModule.forRoot(), FormsModule],
+      schemas: [CUSTOM_ELEMENTS_SCHEMA],
     }).compileComponents();
 
     fixture = TestBed.createComponent(ReviewListComponent);
@@ -52,15 +55,26 @@ describe('ReviewListComponent', () => {
 
   describe('switchStatus()', () => {
     it('should switch status', () => {
-      component.reviews = [{
-        isDone: true,
-      } as any];
-      component.showDone = false;
+      component.reviews = [
+        { isDone: false, name: 'Pending review', submissionId: 1 } as any,
+        { isDone: true, name: 'Completed review', submissionId: 2 } as any,
+      ];
+      component.currentReview = component.reviews[0];
+      component.ngOnChanges({
+        reviews: new SimpleChange(null, component.reviews, true),
+        currentReview: new SimpleChange(null, component.currentReview, true),
+      });
       component.goToFirstOnSwitch = true;
       const spy = spyOn(component.navigate, 'emit');
-      component.switchStatus();
-      expect(spy).toHaveBeenCalled();
+      component.switchStatus({
+        detail: {
+          value: 'completed',
+        },
+      } as any);
+      expect(spy).toHaveBeenCalledWith(component.reviews[1]);
       expect(component.showDone).toBeTrue();
+      expect(component.segmentValue).toBe('completed');
+      expect(component.resultsAnnouncement).toContain('completed');
     });
   });
 
@@ -73,6 +87,7 @@ describe('ReviewListComponent', () => {
       component.reviews = [{
         isDone: true,
       } as any];
+      component.ngOnChanges({ reviews: new SimpleChange(null, component.reviews, false) });
       expect(component.noReviews).toEqual('');
     });
 
@@ -81,6 +96,7 @@ describe('ReviewListComponent', () => {
         { isDone: false } as any
       ];
       component.showDone = true;
+      component.ngOnChanges({ reviews: new SimpleChange(null, component.reviews, false) });
       expect(component.noReviews).toEqual('completed');
     });
 
@@ -89,7 +105,37 @@ describe('ReviewListComponent', () => {
         { isDone: true } as any
       ];
       component.showDone = false;
+      component.ngOnChanges({ reviews: new SimpleChange(null, component.reviews, false) });
       expect(component.noReviews).toEqual('pending');
     });
+
+    it('should hide default message when searching', () => {
+      component.reviews = [
+        { isDone: true, name: 'Completed review', submissionId: 2 } as any,
+      ];
+      component.showDone = true;
+      component.ngOnChanges({ reviews: new SimpleChange(null, component.reviews, false) });
+      component.onSearchTermChange('');
+      component.onSearchTermChange('missing');
+      expect(component.noReviews).toEqual('');
+      expect(component.hasSearchWithoutResults).toBeTrue();
+      expect(component.resultsAnnouncement).toContain('No');
+    });
+  });
+
+  describe('onSearchTermChange()', () => {
+    it('should filter reviews by title', () => {
+      component.reviews = [
+        { isDone: false, name: 'First review', submissionId: 1 } as any,
+        { isDone: false, name: 'Second', submissionId: 2 } as any,
+      ];
+      component.showDone = false;
+      component.ngOnChanges({ reviews: new SimpleChange(null, component.reviews, false) });
+      component.onSearchTermChange('');
+      component.onSearchTermChange('second');
+      expect(component.filteredReviews.length).toBe(1);
+      expect(component.filteredReviews[0].name).toBe('Second');
+      expect(component.resultsAnnouncement).toContain('1');
+    });
   });
 });
diff --git a/projects/v3/src/app/components/review-list/review-list.component.ts b/projects/v3/src/app/components/review-list/review-list.component.ts
index b5bcf1476..ae5f2e9f4 100644
--- a/projects/v3/src/app/components/review-list/review-list.component.ts
+++ b/projects/v3/src/app/components/review-list/review-list.component.ts
@@ -1,22 +1,63 @@
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import {
+  AfterViewInit,
+  Component,
+  ElementRef,
+  EventEmitter,
+  Input,
+  OnChanges,
+  OnInit,
+  Output,
+  QueryList,
+  SimpleChanges,
+  ViewChildren,
+} from '@angular/core';
 import { Review } from '@v3/app/services/review.service';
+import { SegmentChangeEventDetail, SegmentValue } from '@ionic/angular';
 
 @Component({
   selector: 'app-review-list',
   templateUrl: './review-list.component.html',
   styleUrls: ['./review-list.component.scss'],
 })
-export class ReviewListComponent implements OnInit {
+export class ReviewListComponent implements OnInit, OnChanges, AfterViewInit {
   @Input() reviews: Review[];
   @Input() currentReview: Review;
   @Input() goToFirstOnSwitch: boolean;
   @Output() navigate = new EventEmitter();
+  @ViewChildren('reviewItem', { read: ElementRef }) reviewItems: QueryList<ElementRef<HTMLElement>>;
   public showDone = false;
-
-  constructor() { }
+  public searchTerm = '';
+  public filteredReviews: Review[] = [];
+  public segmentValue: 'pending' | 'completed' = 'pending';
+  public resultsAnnouncement = '';
+  private readonly idSuffix = Math.random().toString(36).slice(2, 9);
+  readonly listLabelId = `review-list-heading-${this.idSuffix}`;
+  readonly listId = `review-listbox-${this.idSuffix}`;
+  readonly searchLabelId = `review-search-label-${this.idSuffix}`;
+  readonly searchHintId = `review-search-hint-${this.idSuffix}`;
+  private focusPending = false;
 
   ngOnInit() {
     this.showDone = false;
+    this.applyFilters();
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes['currentReview'] && this.currentReview) {
+      this.setSegmentByCurrentReview();
+      this.focusPending = true;
+    }
+
+    if (changes['reviews'] || changes['currentReview']) {
+      this.applyFilters();
+    }
+  }
+
+  ngAfterViewInit() {
+    this.reviewItems.changes.subscribe(() => {
+      this.tryFocusActiveReview();
+    });
+    this.tryFocusActiveReview();
   }
 
   // go to the review
@@ -30,12 +71,29 @@ export class ReviewListComponent implements OnInit {
     this.navigate.emit(review);
   }
 
-  switchStatus() {
-    this.showDone = !this.showDone;
+  switchStatus(event: CustomEvent<SegmentChangeEventDetail>) {
+    if (!event) {
+      return;
+    }
+
+    const value = this.parseSegmentValue(event.detail?.value);
+
+    const segment: 'pending' | 'completed' = value === 'completed' ? 'completed' : 'pending';
+    this.segmentValue = segment;
+    this.showDone = segment === 'completed';
+    this.applyFilters();
+    this.focusPending = true;
+
+    const nextReview = this.filteredReviews[0];
     if (this.goToFirstOnSwitch) {
-      this.navigate.emit(this.reviews.find(review => {
-        return review.isDone === this.showDone;
-      }));
+      if (nextReview) {
+        this.navigate.emit(nextReview);
+      }
+      return;
+    }
+
+    if (this.currentReview && this.currentReview.isDone !== this.showDone && nextReview) {
+      this.navigate.emit(nextReview);
     }
   }
 
@@ -44,12 +102,118 @@ export class ReviewListComponent implements OnInit {
     if (this.reviews === null) {
       return '';
     }
-    const review = (this.reviews || []).find(review => {
-      return review.isDone === this.showDone;
-    });
-    if (review) {
+    if (this.searchTerm && this.filteredReviews.length === 0) {
+      return '';
+    }
+    if (this.filteredReviews.length > 0) {
       return '';
     }
     return this.showDone ? $localize`completed` : $localize`pending`;
   }
+
+  get hasSearchWithoutResults(): boolean {
+    return !!this.searchTerm && Array.isArray(this.reviews) && this.filteredReviews.length === 0;
+  }
+
+  onSearchTermChange(value: string) {
+    this.searchTerm = (value || '').trim();
+    this.applyFilters();
+  }
+
+  trackBySubmission(_: number, review: Review) {
+    return review?.submissionId;
+  }
+
+  private setSegmentByCurrentReview() {
+    if (!this.currentReview) {
+      return;
+    }
+    this.segmentValue = this.currentReview.isDone ? 'completed' : 'pending';
+    this.showDone = this.currentReview.isDone === true;
+  }
+
+  private applyFilters() {
+    if (!this.reviews) {
+      this.filteredReviews = [];
+      this.updateResultsAnnouncement();
+      return;
+    }
+
+    const term = this.searchTerm.toLowerCase();
+    this.filteredReviews = this.reviews.filter(review => {
+      if (!review) {
+        return false;
+      }
+      const matchesStatus = review.isDone === this.showDone;
+      if (!matchesStatus) {
+        return false;
+      }
+      if (!term) {
+        return true;
+      }
+      return (review.name || '').toLowerCase().includes(term);
+    });
+    this.updateResultsAnnouncement();
+  }
+
+  private parseSegmentValue(value: SegmentValue): string {
+    return typeof value === 'string' ? value : 'pending';
+  }
+
+  private tryFocusActiveReview() {
+    if (!this.focusPending || !this.currentReview || !this.reviewItems) {
+      return;
+    }
+
+    const index = this.filteredReviews.findIndex(review => {
+      return review?.submissionId === this.currentReview?.submissionId;
+    });
+
+    if (index === -1) {
+      this.focusPending = false;
+      return;
+    }
+
+    const items = this.reviewItems.toArray();
+    const element = items[index]?.nativeElement;
+
+    if (!element) {
+      this.focusPending = false;
+      return;
+    }
+
+    setTimeout(() => {
+      if (typeof element.scrollIntoView === 'function') {
+        element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
+      }
+      if (typeof element.focus === 'function') {
+        element.focus();
+      }
+      this.focusPending = false;
+    });
+  }
+
+  private updateResultsAnnouncement() {
+    if (!Array.isArray(this.reviews)) {
+      this.resultsAnnouncement = '';
+      return;
+    }
+
+    const statusLabel = this.showDone ? $localize`completed` : $localize`pending`;
+    const count = this.filteredReviews.length;
+
+    if (count === 0) {
+      this.resultsAnnouncement = this.searchTerm
+        ? $localize`No ${statusLabel} reviews match your search.`
+        : $localize`No ${statusLabel} reviews available.`;
+      return;
+    }
+
+    if (count === 1) {
+      this.resultsAnnouncement = $localize`1 ${statusLabel} review available.`;
+      return;
+    }
+
+    this.resultsAnnouncement = $localize`${count} ${statusLabel} reviews available.`;
+  }
 }
diff --git a/projects/v3/src/app/pages/home/home.page.html b/projects/v3/src/app/pages/home/home.page.html
index 73d71d88a..67d52df0b 100644
--- a/projects/v3/src/app/pages/home/home.page.html
+++ b/projects/v3/src/app/pages/home/home.page.html
@@ -135,11 +135,42 @@
 <ng-template #activityList>
   <h3 class="for-accessibility"
     i18n>Activity list</h3>
+
+  <div class="activity-search-container">
+    <ion-searchbar [(ngModel)]="activitySearchText"
+      (ionInput)="filterActivities()"
+      (ionClear)="clearSearch()"
+      placeholder="Filter by activity title or description"
+      i18n-placeholder
+      mode="md"
+      debounce="300"
+      animated="true"
+      aria-label="Filter activities by title or description"
+      i18n-aria-label>
+    </ion-searchbar>
+
+    <div *ngIf="activitySearchText && filteredMilestones"
+      class="search-results-info"
+      role="status"
+      aria-live="polite">
+      <ion-text color="medium"
+        class="caption">
+        <ng-container *ngIf="getFilteredActivityCount() > 0"
+          i18n>
+          Showing {{ getFilteredActivityCount() }} {{ getFilteredActivityCount() === 1 ? 'activity' : 'activities' }}
+        </ng-container>
+      </ion-text>
+    </div>
+  </div>
+
   <div #activities
     id="activity-list-item"
-    *ngIf="milestones !== null; else loadingMilestones">
-    <ng-container *ngIf="milestones?.length > 0; else emptyMilestones">
-      <ng-container *ngFor="let milestone of milestones">
+    *ngIf="milestones !== null; else loadingMilestones"
+    role="region"
+    [attr.aria-label]="activitySearchText ? 'Filtered activity list' : 'Activity list'"
+    i18n-aria-label>
+    <ng-container *ngIf="filteredMilestones?.length > 0; else emptyMilestones">
+      <ng-container *ngFor="let milestone of filteredMilestones">
         <ion-item class="headline-4 milestone-name"
           [ngClass]="{'locked': milestone.isLocked}"
           lines="none"
@@ -241,27 +272,6 @@
     i18n-subtitle1></app-list-item>
 </ng-template>
 
-<ng-template #emptyMilestones>
-  <ion-item lines="none"
-    class="ion-text-center"
-    role="status"
-    aria-live="polite">
-    <ion-label>
-      <p class="body-1"
-        i18n>No activities available at the moment.</p>
-      <ion-button (click)="updateDashboard()"
-        fill="clear"
-        size="small"
-        aria-label="Retry loading activities" i18n-aria-label>
-        <ion-icon name="refresh"
-          slot="start"
-          aria-hidden="true"></ion-icon>
-        <span i18n>Retry</span>
-      </ion-button>
-    </ion-label>
-  </ion-item>
-</ng-template>
-
 <ng-template #pulseChecks>
   <h3 class="for-accessibility"
     i18n>Pulse checks and skills</h3>
@@ -361,6 +371,42 @@ <h4 class="headline-4" i18n>Skill Strength</h4>
   </ng-container>
 </ng-template>
 
+<ng-template #emptyMilestones>
+  <ion-item lines="none"
+    class="ion-text-center"
+    role="status"
+    aria-live="polite">
+    <ion-label>
+      <p class="body-1"
+        *ngIf="!activitySearchText"
+        i18n>No activities available at the moment.</p>
+      <p class="body-1"
+        *ngIf="activitySearchText"
+        i18n>No activities found matching "{{ activitySearchText }}".</p>
+      <ion-button *ngIf="!activitySearchText"
+        (click)="updateDashboard()"
+        fill="clear"
+        size="small"
+        aria-label="Retry loading activities" i18n-aria-label>
+        <ion-icon name="refresh"
+          slot="start"
+          aria-hidden="true"></ion-icon>
+        <span i18n>Retry</span>
+      </ion-button>
+      <ion-button *ngIf="activitySearchText"
+        (click)="clearSearch()"
+        fill="clear"
+        size="small"
+        aria-label="Clear search filter" i18n-aria-label>
+        <ion-icon name="close"
+          slot="start"
+          aria-hidden="true"></ion-icon>
+        <span i18n>Clear Filter</span>
+      </ion-button>
+    </ion-label>
+  </ion-item>
+</ng-template>
+
 <ng-template #loadingMilestones>
   <div role="status" aria-label="Loading activities" i18n-aria-label>
     <ion-item class="headline-4 milestone-name" lines="none">
diff --git a/projects/v3/src/app/pages/home/home.page.scss b/projects/v3/src/app/pages/home/home.page.scss
index 6ad37eaab..003126880 100644
--- a/projects/v3/src/app/pages/home/home.page.scss
+++ b/projects/v3/src/app/pages/home/home.page.scss
@@ -76,6 +76,31 @@ ion-content.scrollable-desktop {
   display: block;
 }
 
+.activity-search-container {
+  padding: 8px 0;
+  margin-bottom: 8px;
+
+  ion-searchbar {
+    --background: var(--ion-color-light);
+    --border-radius: 8px;
+    --box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+    padding: 0;
+
+    &::part(clear-button) {
+      color: var(--ion-color-primary);
+    }
+  }
+
+  .search-results-info {
+    padding: 4px 16px;
+    text-align: center;
+
+    ion-text {
+      font-size: 0.875rem;
+    }
+  }
+}
+
 
 
 .ontrack-header {
diff --git a/projects/v3/src/app/pages/home/home.page.spec.ts b/projects/v3/src/app/pages/home/home.page.spec.ts
index 18acc964a..8da02f2b3 100644
--- a/projects/v3/src/app/pages/home/home.page.spec.ts
+++ b/projects/v3/src/app/pages/home/home.page.spec.ts
@@ -270,4 +270,333 @@ describe('HomePage', () => {
       expect(component.pulseCheckSkills).toEqual([]);
     });
   });
-});
+
+  describe('filterActivities', () => {
+    const mockMilestones = [
+      {
+        id: 1,
+        name: 'Milestone 1',
+        description: 'First milestone',
+        isLocked: false,
+        activities: [
+          {
+            id: 1,
+            name: 'Activity 1',
+            description: 'First activity about project planning',
+            isLocked: false,
+            leadImage: '',
+            progress: 0.5
+          },
+          {
+            id: 2,
+            name: 'Activity 2',
+            description: 'Second activity about design',
+            isLocked: false,
+            leadImage: '',
+            progress: 0
+          }
+        ],
+        unlockConditions: []
+      },
+      {
+        id: 2,
+        name: 'Milestone 2',
+        description: 'Second milestone',
+        isLocked: false,
+        activities: [
+          {
+            id: 3,
+            name: 'Development Task',
+            description: 'Build the application component',
+            isLocked: true,
+            leadImage: '',
+            progress: 0
+          }
+        ],
+        unlockConditions: []
+      }
+    ];
+
+    beforeEach(() => {
+      component.milestones = mockMilestones;
+    });
+
+    it('should set filtered milestones to null when milestones are null', () => {
+      component.milestones = null;
+      component.activitySearchText = 'test';
+      component.filterActivities();
+      expect(component.filteredMilestones).toBeNull();
+    });
+
+    it('should return all milestones when search text is empty', () => {
+      component.activitySearchText = '';
+      component.filterActivities();
+      expect(component.filteredMilestones).toEqual(mockMilestones);
+    });
+
+    it('should return all milestones when search text is only whitespace', () => {
+      component.activitySearchText = '   ';
+      component.filterActivities();
+      expect(component.filteredMilestones).toEqual(mockMilestones);
+    });
+
+    it('should filter activities by name match (case insensitive)', () => {
+      component.activitySearchText = 'activity 1';
+      component.filterActivities();
+
+      expect(component.filteredMilestones.length).toBe(1);
+      expect(component.filteredMilestones[0].activities.length).toBe(1);
+      expect(component.filteredMilestones[0].activities[0].id).toBe(1);
+    });
+
+    it('should filter activities by description match (case insensitive)', () => {
+      component.activitySearchText = 'planning';
+      component.filterActivities();
+
+      expect(component.filteredMilestones.length).toBe(1);
+      expect(component.filteredMilestones[0].activities.length).toBe(1);
+      expect(component.filteredMilestones[0].activities[0].id).toBe(1);
+    });
+
+    it('should filter activities by partial name match', () => {
+      component.activitySearchText = 'Activity';
+      component.filterActivities();
+
+      expect(component.filteredMilestones.length).toBe(1);
+      expect(component.filteredMilestones[0].activities.length).toBe(2);
+    });
+
+    it('should filter activities by partial description match', () => {
+      component.activitySearchText = 'about';
+      component.filterActivities();
+
+      expect(component.filteredMilestones.length).toBe(1);
+      expect(component.filteredMilestones[0].activities.length).toBe(2);
+    });
+
+    it('should handle search with uppercase text', () => {
+      component.activitySearchText = 'DESIGN';
+      component.filterActivities();
+
+      expect(component.filteredMilestones.length).toBe(1);
+      expect(component.filteredMilestones[0].activities.length).toBe(1);
+      expect(component.filteredMilestones[0].activities[0].id).toBe(2);
+    });
+
+    it('should filter activities matching either name or description', () => {
+      component.activitySearchText = 'development';
+      component.filterActivities();
+
+      expect(component.filteredMilestones).toEqual([]);
+    });
+
+    it('should return empty milestones array when no activities match', () => {
+      component.activitySearchText = 'nonexistent';
+      component.filterActivities();
+
+      expect(component.filteredMilestones).toEqual([]);
+    });
+
+    it('should only include milestones with matching activities', () => {
+      component.activitySearchText = 'first';
+      component.filterActivities();
+
+      expect(component.filteredMilestones.length).toBe(1);
+      expect(component.filteredMilestones[0].id).toBe(1);
+    });
+
+    it('should preserve milestone structure in filtered results', () => {
+      component.activitySearchText = 'activity';
+      component.filterActivities();
+
+      expect(component.filteredMilestones[0].id).toBeDefined();
+      expect(component.filteredMilestones[0].name).toBeDefined();
+      expect(component.filteredMilestones[0].activities).toBeDefined();
+    });
+
+    it('should handle activities with missing description property', () => {
+      const milestonesWithMissingDesc = [{
+        id: 1,
+        name: 'Milestone',
+        description: 'desc',
+        isLocked: false,
+        activities: [
+          {
+            id: 1,
+            name: 'Activity',
+            description: undefined,
+            isLocked: false,
+            leadImage: ''
+          }
+        ],
+        unlockConditions: []
+      }];
+
+      component.milestones = milestonesWithMissingDesc;
+      component.activitySearchText = 'activity';
+      component.filterActivities();
+
+      expect(component.filteredMilestones.length).toBe(1);
+      expect(component.filteredMilestones[0].activities.length).toBe(1);
+    });
+
+    it('should handle multiple activities matching same search term', () => {
+      component.activitySearchText = 'a';
+      component.filterActivities();
+
+      expect(component.filteredMilestones.length).toBe(1);
+      expect(component.filteredMilestones[0].activities.length).toBe(2);
+    });
+
+    it('should skip locked activities even when they match search text', () => {
+      component.activitySearchText = 'task';
+      component.filterActivities();
+
+      expect(component.filteredMilestones).toEqual([]);
+    });
+
+    it('should trim whitespace from search text', () => {
+      component.activitySearchText = '  activity 1  ';
+      component.filterActivities();
+
+      expect(component.filteredMilestones.length).toBe(1);
+      expect(component.filteredMilestones[0].activities.length).toBe(1);
+    });
+  });
+
+  describe('clearSearch', () => {
+    const mockMilestones = [
+      {
+        id: 1,
+        name: 'Milestone 1',
+        description: 'First milestone',
+        isLocked: false,
+        activities: [
+          {
+            id: 1,
+            name: 'Activity 1',
+            description: 'First activity',
+            isLocked: false,
+            leadImage: ''
+          }
+        ],
+        unlockConditions: []
+      }
+    ];
+
+    beforeEach(() => {
+      component.milestones = mockMilestones;
+    });
+
+    it('should clear search text', () => {
+      component.activitySearchText = 'test search';
+      component.clearSearch();
+
+      expect(component.activitySearchText).toBe('');
+    });
+
+    it('should reset filtered milestones to all milestones', () => {
+      component.activitySearchText = 'test';
+      component.filterActivities();
+      component.clearSearch();
+
+      expect(component.filteredMilestones).toEqual(mockMilestones);
+    });
+
+    it('should call filterActivities when clearing search', () => {
+      spyOn(component, 'filterActivities');
+      component.clearSearch();
+
+      expect(component.filterActivities).toHaveBeenCalled();
+    });
+  });
+
+  describe('getFilteredActivityCount', () => {
+    it('should return 0 when filtered milestones is null', () => {
+      component.filteredMilestones = null;
+
+      expect(component.getFilteredActivityCount()).toBe(0);
+    });
+
+    it('should return 0 when there are no filtered milestones', () => {
+      component.filteredMilestones = [];
+
+      expect(component.getFilteredActivityCount()).toBe(0);
+    });
+
+    it('should return correct count of activities from single milestone', () => {
+      component.filteredMilestones = [
+        {
+          id: 1,
+          name: 'Milestone 1',
+          description: 'desc',
+          isLocked: false,
+          activities: [
+            { id: 1, name: 'Activity 1', description: 'desc', isLocked: false, leadImage: '' },
+            { id: 2, name: 'Activity 2', description: 'desc', isLocked: false, leadImage: '' }
+          ],
+          unlockConditions: []
+        }
+      ];
+
+      expect(component.getFilteredActivityCount()).toBe(2);
+    });
+
+    it('should return correct count of activities from multiple milestones', () => {
+      component.filteredMilestones = [
+        {
+          id: 1,
+          name: 'Milestone 1',
+          description: 'desc',
+          isLocked: false,
+          activities: [
+            { id: 1, name: 'Activity 1', description: 'desc', isLocked: false, leadImage: '' },
+            { id: 2, name: 'Activity 2', description: 'desc', isLocked: false, leadImage: '' }
+          ],
+          unlockConditions: []
+        },
+        {
+          id: 2,
+          name: 'Milestone 2',
+          description: 'desc',
+          isLocked: false,
+          activities: [
+            { id: 3, name: 'Activity 3', description: 'desc', isLocked: false, leadImage: '' }
+          ],
+          unlockConditions: []
+        }
+      ];
+
+      expect(component.getFilteredActivityCount()).toBe(3);
+    });
+
+    it('should handle milestone with no activities', () => {
+      component.filteredMilestones = [
+        {
+          id: 1,
+          name: 'Milestone 1',
+          description: 'desc',
+          isLocked: false,
+          activities: [],
+          unlockConditions: []
+        }
+      ];
+
+      expect(component.getFilteredActivityCount()).toBe(0);
+    });
+
+    it('should handle milestone with undefined activities', () => {
+      component.filteredMilestones = [
+        {
+          id: 1,
+          name: 'Milestone 1',
+          description: 'desc',
+          isLocked: false,
+          activities: undefined,
+          unlockConditions: []
+        }
+      ];
+
+      expect(component.getFilteredActivityCount()).toBe(0);
+    });
+
diff --git a/projects/v3/src/app/pages/home/home.page.ts b/projects/v3/src/app/pages/home/home.page.ts
index c11e5708a..714533fb9 100644
--- a/projects/v3/src/app/pages/home/home.page.ts
+++ b/projects/v3/src/app/pages/home/home.page.ts
@@ -46,7 +46,7 @@ export class HomePage implements OnInit, OnDestroy, AfterViewChecked {
   // default card image (gracefully show broken url)
   defaultLeadImage: string = "";
 
-  lastVisitedActivityId: number = null;
+  lastVisitedActivityId?: number;
   bookmarkedActivities: {
     [key: number]: boolean;
   } = {};
@@ -58,6 +58,10 @@ export class HomePage implements OnInit, OnDestroy, AfterViewChecked {
   @ViewChild('activities', { static: false }) activities!: ElementRef;
   pulseCheckSkills: PulseCheckSkill[] = [];
 
+  // activity search/filter
+  activitySearchText = '';
+  filteredMilestones: Milestone[] | null = null;
+
   // Expose Math to template
   Math = Math;
 
@@ -121,6 +125,7 @@ export class HomePage implements OnInit, OnDestroy, AfterViewChecked {
       ).subscribe(
         (milestones) => {
           this.milestones = milestones;
+          this.filterActivities(); // apply filter when load
         }
       );
 
@@ -527,4 +532,62 @@ export class HomePage implements OnInit, OnDestroy, AfterViewChecked {
       },
     );
   }
+
+  /**
+   * filter activities based on search text
+   * searches through activity title and description
+   */
+  filterActivities(): void {
+    if (!this.milestones) {
+      this.filteredMilestones = null;
+      return;
+    }
+
+    const searchText = this.activitySearchText.toLowerCase().trim();
+
+    if (!searchText) {
+      this.filteredMilestones = this.milestones;
+      return;
+    }
+
+    // filter milestones and their activities
+    this.filteredMilestones = this.milestones
+      .map(milestone => {
+        const filteredActivities = milestone.activities?.filter(activity => {
+          const titleMatch = activity.name?.toLowerCase().includes(searchText);
+          const descriptionMatch = activity.description?.toLowerCase().includes(searchText);
+          return titleMatch || descriptionMatch;
+        }) ?? [];
+
+        // only include milestone if it has matching activities
+        if (filteredActivities.length > 0) {
+          return {
+            ...milestone,
+            activities: filteredActivities
+          };
+        }
+        return null;
+      })
+      .filter(milestone => milestone !== null);
+  }
+
+  /**
+   * clear search input and reset filter
+   */
+  clearSearch(): void {
+    this.activitySearchText = '';
+    this.filterActivities();
+  }
+
+  /**
+   * get total count of filtered activities across all milestones
+   */
+  getFilteredActivityCount(): number {
+    if (!this.filteredMilestones) {
+      return 0;
+    }
+    return this.filteredMilestones.reduce((total, milestone) => {
+      return total + (milestone.activities?.length || 0);
+    }, 0);
+  }
 }
diff --git a/projects/v3/src/app/services/home.service.ts b/projects/v3/src/app/services/home.service.ts
index 32cf8fea9..d660ee419 100644
--- a/projects/v3/src/app/services/home.service.ts
+++ b/projects/v3/src/app/services/home.service.ts
@@ -42,6 +42,7 @@ export interface Milestone {
   activities?: {
     id: number;
     name: string;
+    description: string;
     isLocked: boolean;
     leadImage: string;
     progress?: number;
@@ -183,7 +184,7 @@ export class HomeService {
           description
           isLocked
           activities {
-            id name isLocked leadImage
+            id name isLocked leadImage description
             unlockConditions {
               name
               action
