Skip to content
Angular ng routing 4 min read

Query Params & Fragments

Query parameters and fragments carry optional, non-hierarchical state in the URL — things like search filters, pagination, sort order, or a scroll-to anchor. Unlike route parameters, they are not part of the route’s path definition, so any route can read and write them freely. Getting them right means your application’s state survives a page reload, a shared link, or a browser back button, which is exactly what users expect.

Query params vs fragments vs route params

It helps to be precise about which part of the URL you are touching. In https://app.dev/products?category=books&page=2#reviews, the three pieces play different roles.

PartExamplePurposeRead with
Route parameter/products/42Required, identifies a resourceroute.paramMap
Query parameters?category=books&page=2Optional, modifies the view (filters, paging)route.queryParamMap
Fragment#reviewsAnchor / client-side scroll targetroute.fragment

Query params are loosely coupled to a route — you can add ?page=2 to almost any URL without declaring it in your route config.

Setting query params and fragments on navigation

Both RouterLink and the imperative Router.navigate accept queryParams and fragment options. Use the link form in templates and the imperative form in component logic.

<a
  routerLink="/products"
  [queryParams]="{ category: 'books', page: 2 }"
  fragment="reviews"
>
  Books, page 2
</a>
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-search-box',
  standalone: true,
  template: `<button (click)="search('phones')">Search phones</button>`,
})
export class SearchBoxComponent {
  private router = inject(Router);

  search(term: string): void {
    this.router.navigate(['/products'], {
      queryParams: { q: term, page: 1 },
      fragment: 'results',
    });
  }
}

The resulting URL is /products?q=phones&page=1#results.

Reading query params and fragments

Inject ActivatedRoute and subscribe to the relevant observable. The queryParamMap and fragment observables emit again whenever the URL changes without re-creating the component, which is the common case when only the query string changes.

import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';

@Component({
  selector: 'app-product-list',
  standalone: true,
  template: `
    <h2>Category: {{ category() ?? 'all' }}</h2>
    <p>Page {{ page() }}</p>
  `,
})
export class ProductListComponent {
  private route = inject(ActivatedRoute);

  // Convert the queryParamMap stream into signals for templates.
  category = toSignal(
    this.route.queryParamMap.pipe(map((p) => p.get('category'))),
    { initialValue: null },
  );

  page = toSignal(
    this.route.queryParamMap.pipe(map((p) => Number(p.get('page') ?? 1))),
    { initialValue: 1 },
  );
}

For a one-time read where you do not need to react to later changes, route.snapshot.queryParamMap.get('page') is fine. Prefer the observable/signal form whenever the same component stays mounted across query changes.

Query param values are always strings (or null). Cast them explicitly — Number(...), === 'true', etc. — and never trust the type. Arrays arrive as repeated keys: ?tag=a&tag=b is read with queryParamMap.getAll('tag').

Angular’s binding-based component inputs (withComponentInputBinding()) can also map query params straight to @Input() properties, removing the boilerplate above:

provideRouter(routes, withComponentInputBinding());
import { Input } from '@angular/core';

export class ProductListComponent {
  @Input() category?: string; // bound from ?category=
  @Input() page = '1';        // bound from ?page=
}

Preserving params across navigations

By default, navigating away drops the current query string and fragment. The queryParamsHandling strategy controls this.

ValueEffect
'' (default)Replace — drop existing query params
'merge'Merge new params into existing ones
'preserve'Keep existing params, ignore new ones
// Toggle one filter while keeping every other query param intact.
this.router.navigate([], {
  relativeTo: this.route,
  queryParams: { page: 3 },
  queryParamsHandling: 'merge',
});

Passing { sort: null } with 'merge' removes a single param. To keep the fragment across a navigation, set preserveFragment: true.

<a routerLink="/products" queryParamsHandling="merge" [preserveFragment]="true">
  Refine
</a>

Fragments and scrolling

A fragment is most useful as a scroll anchor. Enable automatic anchor scrolling once at bootstrap, and the router will scroll to the element whose id matches the fragment after navigation.

import { provideRouter, withInMemoryScrolling } from '@angular/router';

provideRouter(routes,
  withInMemoryScrolling({ anchorScrolling: 'enabled', scrollPositionRestoration: 'enabled' }),
);
<section id="reviews">
  <h2>Customer reviews</h2>
</section>

Now navigating to /products#reviews scrolls straight to that section. For manual control, read route.fragment and call scrollIntoView() yourself.

this.route.fragment.subscribe((id) => {
  if (id) document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
});

Output:

Navigated to /products?category=books&page=2#reviews
Page scrolled to element #reviews

A practical filter example

Treating the URL as the single source of truth for filter state means the back button, bookmarks, and shared links all “just work.”

applyFilter(category: string): void {
  this.router.navigate([], {
    relativeTo: this.route,
    queryParams: { category, page: 1 }, // reset paging on new filter
    queryParamsHandling: 'merge',
  });
}

The component re-reads queryParamMap, recomputes the product list, and the URL reflects exactly what the user sees.

Best practices

  • Treat the URL (query params + fragment) as the source of truth for view state like filters, search, and pagination — never duplicate it in component state.
  • Always coerce query param values; they are strings or null, never numbers or booleans.
  • Use queryParamsHandling: 'merge' when changing one filter so you do not clobber the others, and pass null to remove a param.
  • Prefer queryParamMap/fragment observables (or withComponentInputBinding()) over snapshot when the component survives query changes.
  • Enable withInMemoryScrolling({ anchorScrolling: 'enabled' }) instead of hand-rolling fragment scroll logic.
  • Keep query params flat and short — they are user-visible and length-limited; store large state elsewhere.
Last updated June 14, 2026
Was this helpful?