Skip to main content

🧭 Navigator and Location Objects

Two small global objects punch far above their weight. navigator tells you about the browser, device, and connection and is the gateway to many hardware APIs. location lets you read the current URL piece by piece, redirect the user, and store application state right in the address bar. Together they're the browser's instrument panel.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Read browser, device, and connection details from the navigator object
  • Explain why feature detection beats parsing the user-agent string
  • Break a URL into its parts (protocol, host, pathname, search, hash) with location
  • Parse and build query strings with URLSearchParams
  • Store shareable UI state in the URL using the History API
  • Avoid XSS and open-redirect pitfalls when handling URL input

Estimated Time: 35–45 minutes  •  Difficulty: Intermediate

Hands-on: Build a filter bar whose state lives in the URL and survives a refresh and a share.

In This Lesson

Two Objects on the Window

The global window hangs several helper objects off itself. Two you'll touch constantly are window.navigator and window.location (you can drop the window. prefix and write navigator or location directly).

💡 A useful analogy: navigator is the ship's captain — it knows the vessel's capabilities and status. location is the navigation chart — it knows exactly where you are (the URL), what its parts mean, and can set a new course (redirect).
flowchart TD W[window] --> N[navigator] W --> L[location] W --> H[history] W --> D[document] N --> N1[browser & device info] N --> N2[connection info] N --> N3[gateway to device APIs] L --> L1[URL parts] L --> L2[navigation methods] L --> L3[query & hash]

User-Agent vs. Feature Detection

navigator.userAgent is a long, historically messy string like:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36

It's tempting to parse it to decide "is this Chrome?" — but this is fragile. User-agent strings are routinely spoofed, deliberately frozen by browser vendors, and full of legacy tokens (notice a Chrome UA claims to be Mozilla and Safari). Basing behaviour on it leads to sites that break the moment a string changes.

⚠️ Detect features, not browsers

// ❌ Brittle — breaks when UA strings change or are spoofed
if (navigator.userAgent.includes('Chrome')) { /* ... */ }

// ✅ Robust — ask whether the capability exists
if ('share' in navigator) {
  await navigator.share({ url: location.href });
}

Reserve user-agent inspection for coarse analytics or logging, never for deciding whether a capability is available. When you truly need device class, the modern, structured replacement is the User-Agent Client Hints API (navigator.userAgentData) where supported.

The Location Object

The location object represents the current URL and lets you both read its parts and navigate. Consider this URL:

https://shop.example.com:8080/products/shoes?color=red&size=10#reviews
Anatomy of a URL as location properties A URL split into protocol, host with hostname and port, pathname, search query string, and hash fragment, each mapped to a location property. protocol https: host shop.example.com:8080 pathname /products/shoes search ?color=red&size=10 hash #reviews origin = protocol + host = "https://shop.example.com:8080" href = the entire URL string
Figure 1 — Each part of a URL maps to a location property. origin combines protocol and host; href is the whole thing.
location.href;     // full URL string
location.protocol; // 'https:'
location.host;     // 'shop.example.com:8080'
location.hostname; // 'shop.example.com'
location.port;     // '8080'
location.pathname; // '/products/shoes'
location.search;   // '?color=red&size=10'
location.hash;     // '#reviews'
location.origin;   // 'https://shop.example.com:8080'

Navigating

location.assign('/checkout');   // go to a new page (adds a history entry)
location.href = '/checkout';    // identical to assign()
location.replace('/login');     // navigate WITHOUT a history entry (no "back")
location.reload();              // reload the current page

📖 assign vs. replace

assign() (and setting href) leaves the current page in history, so the Back button returns to it. replace() overwrites the current entry — handy after a login redirect, so users can't "back" into the login form.

Query Strings with URLSearchParams

location.search gives you the raw query string, but you rarely want to slice it by hand. The URLSearchParams API parses, reads, edits, and re-serialises query strings correctly — including URL-encoding — so you never hand-roll string splitting again.

// For ?name=Ray&hobby=coding&hobby=music
const params = new URLSearchParams(location.search);

params.get('name');     // 'Ray'
params.get('missing');  // null
params.has('hobby');    // true
params.getAll('hobby'); // ['coding', 'music'] — repeated keys

// Iterate every pair
for (const [key, value] of params) {
  console.log(key, '=', value);
}

// Build a query string safely (encoding handled for you)
const q = new URLSearchParams({ q: 'red shoes', page: 2 });
q.toString(); // 'q=red+shoes&page=2'

✅ Why not split the string yourself?

Manual parsing forgets edge cases: URL-encoded characters (%20), + for spaces, repeated keys, empty values. URLSearchParams handles them all and is supported everywhere. Reach for it every time.

URL-Driven UI State

Here's a powerful pattern: store your UI's filter/sort/page state in the URL. The payoff is huge — the state becomes bookmarkable, shareable, and Back-button friendly. Paste the link to a coworker and they see the exact same filtered view.

The trick is history.pushState(), which updates the address bar without reloading the page. Combine it with URLSearchParams:

// Read state from the URL on load
function readState() {
  const p = new URLSearchParams(location.search);
  return {
    category: p.get('category') || 'all',
    sort:     p.get('sort')     || 'newest',
    page:     Number(p.get('page') || '1')
  };
}

// Write one value back into the URL — no reload
function setParam(key, value) {
  const p = new URLSearchParams(location.search);
  if (value === '' || value == null) p.delete(key);
  else p.set(key, value);
  const qs = p.toString();
  history.pushState({}, '', qs ? `${location.pathname}?${qs}` : location.pathname);
  render(readState());
}

// Re-render when the user presses Back/Forward
window.addEventListener('popstate', () => render(readState()));

// Wire up a control
document.querySelector('#sort').addEventListener('change', (e) => {
  setParam('sort', e.target.value);
});

render(readState()); // initial paint

💡 pushState vs. replaceState

Use pushState when the change is a distinct step the user might want to Back out of (changing a filter). Use replaceState for trivial tweaks you don't want cluttering history (updating a scroll position or a draft field).

Security: XSS & Open Redirects

URL values come from the outside — anyone can craft a link with any query string. Treat them as untrusted input. Two classic bugs come from getting this wrong.

⚠️ 1. XSS via innerHTML

Never inject a URL value into the page with innerHTML — an attacker can smuggle a <script> or event handler.

const name = new URLSearchParams(location.search).get('name');

// ❌ XSS hole
welcome.innerHTML = `Welcome, ${name}!`;

// ✅ textContent escapes automatically
welcome.textContent = `Welcome, ${name}!`;

⚠️ 2. Open redirect

Redirecting to a URL taken straight from a query parameter lets attackers send victims anywhere (phishing). Validate against an allow-list first.

const target = new URLSearchParams(location.search).get('redirect');

function isSafe(url) {
  try {
    const u = new URL(url, location.origin);
    return u.origin === location.origin; // same-site only
  } catch {
    return false; // not a valid URL
  }
}

// ✅ Only redirect to trusted destinations
if (target && isSafe(target)) location.assign(target);
else location.assign('/'); // safe default

The theme across both: output-encode anything from a URL before it hits the DOM, and validate anything from a URL before it drives navigation.

Hands-on Exercise

🏋️ Build a Shareable Filter Bar

Objective: Store UI state in the URL so a filtered view can be refreshed, bookmarked, and shared.

Instructions:

  1. Make a page with a category <select> and a sort <select>, plus a text element that echoes the current state.
  2. On load, read category and sort from the URL with URLSearchParams and set the controls to match.
  3. When a control changes, update the URL with history.pushState()without reloading — and re-render the echo text.
  4. Add a popstate listener so the Back and Forward buttons restore the correct state.
  5. Test it: change filters, copy the URL, open it in a new tab, and confirm the same state loads.
  6. Stretch: add a "Clear" button that removes the params with replaceState so it doesn't add a history entry.
💡 Hint

Keep one function, render(), that reads state from the URL and updates both the controls and the echo. Call it on load, after every pushState, and inside the popstate handler — a single source of truth.

✅ Sample solution
const cat  = document.querySelector('#category');
const sort = document.querySelector('#sort');
const echo = document.querySelector('#echo');

function render() {
  const p = new URLSearchParams(location.search);
  const state = { category: p.get('category') || 'all', sort: p.get('sort') || 'newest' };
  cat.value = state.category;
  sort.value = state.sort;
  echo.textContent = `Showing ${state.category}, sorted by ${state.sort}`;
}

function update(key, value) {
  const p = new URLSearchParams(location.search);
  value === 'all' || value === 'newest' ? p.delete(key) : p.set(key, value);
  const qs = p.toString();
  history.pushState({}, '', qs ? `?${qs}` : location.pathname);
  render();
}

cat.addEventListener('change',  (e) => update('category', e.target.value));
sort.addEventListener('change', (e) => update('sort', e.target.value));
window.addEventListener('popstate', render);

document.querySelector('#clear').addEventListener('click', () => {
  history.replaceState({}, '', location.pathname);
  render();
});

render();

🎯 Quick Quiz

Question 1: Why prefer feature detection over parsing navigator.userAgent?

Question 2: For the URL https://x.com/a?q=1#top, what does location.hash return?

Question 3: Which line safely displays a name value taken from the query string?

Summary & Quiz

🎉 Key Takeaways

  • navigator reports browser, device, and connection info and gates many device APIs.
  • Prefer feature detection ('x' in navigator) over parsing the unreliable user-agent string.
  • location splits the URL into protocol, host, pathname, search, and hash, and can navigate with assign/replace.
  • URLSearchParams is the correct tool for reading and building query strings.
  • Store UI state in the URL with history.pushState for shareable, bookmarkable, Back-friendly views.
  • Treat URL values as untrusted: use textContent to avoid XSS and allow-list before redirecting.

📚 Further Reading

🚀 What's Next?

You've now covered the DOM, events, forms, storage, and the core browser objects. It's time to put it all together — next is the module weekend project, where you'll build a small app that leans on everything from this module.

🎉 Module concepts complete!

You can read the browser and drive the URL. Let's build something with it.