Webbo3 Level 2 · JavaScript Development · Day 12 of 20
What the Browser Can Already Do
Geolocation, clipboard, dates, URLs, history, files and lazy loading: capabilities you do not have to build or install.
By the end of today you can
- Ask permission properly, and handle being refused
- Read and write the clipboard
- Format a date for a human without a library
- Read and change the URL so a filtered view can be shared
- Load images and content only when they come into view
Beginners install a library for things the browser has done natively for years. Today is a tour, not a manual: the point is not to memorise these but to know they exist, so that the next time you are about to search npm you ask whether the browser already does it. Very often it does.
1. Permissions: the pattern behind all of them
Geolocation, notifications, camera and clipboard-read all ask the user first. Three outcomes, and code that only handles one of them is broken code.
function findMe() {
if (!('geolocation' in navigator)) { // 1. the feature may not exist
return showMessage('Your browser cannot do location.');
}
navigator.geolocation.getCurrentPosition(
(position) => { // 2. they said yes
const { latitude, longitude } = position.coords;
showMessage(`You are near ${latitude.toFixed(3)}, ${longitude.toFixed(3)}`);
},
(error) => { // 3. they said no, or it failed
const reasons = {
1: 'You blocked location access. You can allow it in the address bar.',
2: 'Your position is not available right now.',
3: 'That took too long. Try again.'
};
showMessage(reasons[error.code] ?? 'Could not get your location.');
},
{ timeout: 8000 }
);
}Never ask on page load
A permission prompt the moment the page opens is refused by most people, and once refused you cannot ask again: the browser remembers. Ask when the user presses a button that obviously needs it, so the request has a reason attached. This is the single biggest difference between a prompt that gets accepted and one that does not.
- These APIs need HTTPS. They work on
localhostfor development and nowhere else insecure, which is why your deployed page must be on https from Day 18. - Always pass a
timeout. Without one, a device that cannot get a fix leaves your spinner turning forever.
2. Clipboard
async function copyLink(url, button) {
try {
await navigator.clipboard.writeText(url);
button.textContent = 'Copied';
setTimeout(() => { button.textContent = 'Copy link'; }, 1500);
} catch (error) {
button.textContent = 'Press Ctrl+C'; // blocked, or not focused
}
}Writing needs no permission prompt in most browsers, but it does need the click to have come from the user: call it on a timer and it is silently refused. Always confirm visually. A copy button that gives no feedback gets pressed four times.
3. Dates, without a library
const joined = new Date('2026-03-15T09:30:00Z');
console.log(joined.toISOString()); // machines and APIs
console.log(joined.toLocaleDateString('en-NG', {
day: 'numeric', month: 'long', year: 'numeric'
}));
console.log(joined.toLocaleString('en-NG', {
dateStyle: 'medium', timeStyle: 'short', timeZone: 'Africa/Lagos'
}));Intl is built into every browser and into Node, and it handles month names, ordering and time zones for you. This is the "do I need a library" question answered: for formatting, no.
// "3 days ago" without a library either
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
function ago(from, to) {
const days = Math.round((from - to) / 86400000);
return rtf.format(days, 'day');
}
const now = new Date('2026-09-12T12:00:00Z');
console.log(ago(new Date('2026-09-11T12:00:00Z'), now));
console.log(ago(new Date('2026-09-09T12:00:00Z'), now));
console.log(ago(new Date('2026-09-15T12:00:00Z'), now));Date arithmetic has sharp edges
Months are zero-based, so new Date(2026, 0, 1) is January. Parsing "15/03/2026" is not reliable across browsers: use the ISO form "2026-03-15". And an ISO string with no Z is read as local time, so two users in different countries see different days. When a real project does heavy date maths, that is when a library earns its place.
console.log(new Date(2026, 0, 1).toDateString()); // month 0 is January
console.log(new Date('2026-03-15').toISOString()); // ISO parses reliably4. The URL as state
Yesterday you kept the search and filter in a state object. Put them in the URL as well and the view becomes shareable, bookmarkable, and survives the back button. This is free and almost nobody does it.
const url = new URL('https://webbo3.com/students?city=Lagos&page=2');
console.log(url.pathname);
console.log(url.searchParams.get('city'));
console.log(url.searchParams.get('missing')); // null, not an error
url.searchParams.set('page', '3');
url.searchParams.set('search', 'chidinma okafor'); // spaces encoded for you
url.searchParams.delete('city');
console.log(url.toString());// write the current view into the URL without reloading the page
function syncUrl() {
const url = new URL(window.location);
state.search ? url.searchParams.set('q', state.search) : url.searchParams.delete('q');
url.searchParams.set('page', state.page);
history.replaceState(null, '', url); // replaceState: no new history entry
}
// read it back when the page opens, so a shared link works
function readUrl() {
const params = new URLSearchParams(window.location.search);
state.search = params.get('q') ?? '';
state.page = Number(params.get('page')) || 1;
}pushState or replaceState
pushState adds a history entry, so the back button undoes the change: right for moving between pages of results. replaceState edits the current entry: right for a search box, or every keystroke becomes something to press back through.
5. IntersectionObserver: doing work only when it is visible
The old way to know whether something is on screen was to listen to scroll and measure positions, which runs constantly and is slow. IntersectionObserver tells the browser what you care about and lets it tell you.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add('is-visible'); // fade it in
observer.unobserve(entry.target); // done with this one
});
}, { rootMargin: '100px' }); // start 100px early
document.querySelectorAll('.card').forEach((card) => observer.observe(card));rootMarginstarts the work slightly before the element arrives, so the user never sees it happen.unobserveonce you are finished with an element, or you keep being told about it.- For images you often need none of this:
<img loading="lazy">is one attribute and the browser does the rest.
<img src="student.jpg" alt="Chidinma at her laptop" loading="lazy" width="400" height="300">width and height on that tag are not decoration. Without them the browser does not know how much room to leave and the page jumps as each image loads, which is the single most irritating thing a slow page does.
6. Files, without uploading anything
input.addEventListener('change', () => {
const file = input.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) return showError('Images only.');
if (file.size > 2 * 1024 * 1024) return showError('Under 2MB please.');
const url = URL.createObjectURL(file); // a local preview, no upload
preview.src = url;
preview.onload = () => URL.revokeObjectURL(url); // release it afterwards
});Checking the type in the browser is a courtesy, not security
file.type comes from the file name and can be lied about trivially. It is there to give the user a fast, kind message. The server must check again, and never trust what the browser said. That principle, validate in the browser for kindness and on the server for safety, is Day 17 in one line.
Do this now
Add three of these to your Day 9 API browser. Not all of them: choose the ones that genuinely improve it.
- Put the search and page in the URL, and prove it by sending the link to yourself and opening it fresh.
- A Copy link button with visible confirmation.
- Format every date in the data with
toLocaleDateString, and add a relative "joined 3 days ago". - Lazy-load the images with the attribute, and give every one a width and height.
- A location button that fills in a city filter, handling all three permission outcomes.
Checkpoint
Q1. Why is asking for location on page load a mistake?
Most people refuse a prompt with no context, and a refusal is remembered by the browser so you cannot ask again. Ask on a button press that obviously needs it.
Q2. `new Date(2026, 0, 1)` is which month?
January. Months are zero-based in that constructor. Day-of-month and year are not, which is exactly why it catches people.
Q3. When do you use `replaceState` rather than `pushState`?
When the change should not become a back-button step. A search box uses replaceState, or the user has to press back once per character they typed.
Q4. Your file input checks `file.type`. Is the upload now safe?
No. file.type is derived from the filename and is trivial to fake. The browser check is a fast, kind message for honest users; the server must check independently.
Tick before you move on
- ☐ My filtered view is in the URL and a shared link reproduces it
- ☐ Every permission request happens on a click, and I handled a refusal
- ☐ Dates are formatted with Intl, not by string surgery
- ☐ Every image has width, height and loading="lazy"
Quick recap
Three outcomes for every permission: unsupported, granted, refused · Intl formats dates and relative times with no library · The URL is state you get sharing and the back button for free · IntersectionObserver instead of scroll listeners; loading="lazy" for images · Browser validation is courtesy; the server validates for safety
Tomorrow, Day 13: forms and accessibility. The day you find out whether anything you have built can be used without a mouse.