🧭 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:navigatoris the ship's captain — it knows the vessel's capabilities and status.locationis the navigation chart — it knows exactly where you are (the URL), what its parts mean, and can set a new course (redirect).
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
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:
- Make a page with a category
<select>and a sort<select>, plus a text element that echoes the current state. - On load, read
categoryandsortfrom the URL withURLSearchParamsand set the controls to match. - When a control changes, update the URL with
history.pushState()— without reloading — and re-render the echo text. - Add a
popstatelistener so the Back and Forward buttons restore the correct state. - Test it: change filters, copy the URL, open it in a new tab, and confirm the same state loads.
- Stretch: add a "Clear" button that removes the params with
replaceStateso 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, andhash, and can navigate withassign/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
textContentto 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.