Webbo3 Level 2 · JavaScript Development · Day 12 of 20
What the Browser Can Already Do
Location, the clipboard, dates, the address bar, lazy loading and files: things you do not have to build or install.
By the end of today you can
- Ask for a permission properly, and handle all three outcomes
- Copy to the clipboard with visible confirmation
- Format dates and "3 days ago" for people with
Intl, without a library
- Keep a filtered view in the URL so a link reproduces it
- Do work only when something scrolls into view
- Check a chosen file in the browser, and know why that is not security
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 install something you first ask whether the browser already does it. Very often it does.
1. Permissions: The Pattern Behind All of Them
Definition
Some browser features, such as location, notifications and the camera, need the user's permission. Every one of them has three possible outcomes, and code that handles only one is broken code.
Real-Life Example
Knocking on a neighbour's door to borrow a ladder. They might not own one (not supported). They might say yes. They might say no. You have to be ready for all three before you knock.
Types: The Three Outcomes
| Outcome | What your code must do |
| Not supported | Say so plainly, and offer another way (for example, typing a city) |
| Granted | Use the result |
| Refused or failed | Explain in plain words, and say how to allow it later |
toFixed(3) rounds a number to three decimal places. The error object's code is 1 for refused, 2 for unavailable and 3 for too slow.
What this code doesWe want a location button that handles every outcome. First, when the user allows it (the browser was given a position in Lagos).
index.html
1
2
<button id="find">Use my location</button>
<p id="message"></p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const message = document.querySelector('#message');
function findMe() {
// 1. The feature may not exist at all
if (!('geolocation' in navigator)) {
message.textContent = 'Your browser cannot do location.';
return;
}
message.textContent = 'Finding you...';
navigator.geolocation.getCurrentPosition(
// 2. They said yes
(position) => {
const { latitude, longitude } = position.coords;
message.textContent = `You are near ${latitude.toFixed(3)}, ${longitude.toFixed(3)}`;
},
// 3. They said no, or it failed
(error) => {
const reasons = {
1: 'You blocked location access. You can allow it from the address bar.',
2: 'Your position is not available right now.',
3: 'That took too long. Try again.'
};
message.textContent = reasons[error.code] ?? 'Could not get your location.';
},
// Never wait forever
{ timeout: 8000 }
);
}
// Ask only when the user presses the button
document.querySelector('#find').addEventListener('click', findMe);
BROWSERindex.html · after clicking and allowing location
What this code doesWe want to see the same code when the user refuses.
index.html
1
2
<button id="find">Use my location</button>
<p id="message"></p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const message = document.querySelector('#message');
function findMe() {
// 1. The feature may not exist at all
if (!('geolocation' in navigator)) {
message.textContent = 'Your browser cannot do location.';
return;
}
message.textContent = 'Finding you...';
navigator.geolocation.getCurrentPosition(
// 2. They said yes
(position) => {
const { latitude, longitude } = position.coords;
message.textContent = `You are near ${latitude.toFixed(3)}, ${longitude.toFixed(3)}`;
},
// 3. They said no, or it failed
(error) => {
const reasons = {
1: 'You blocked location access. You can allow it from the address bar.',
2: 'Your position is not available right now.',
3: 'That took too long. Try again.'
};
message.textContent = reasons[error.code] ?? 'Could not get your location.';
},
// Never wait forever
{ timeout: 8000 }
);
}
// Ask only when the user presses the button
document.querySelector('#find').addEventListener('click', findMe);
BROWSERindex.html · after clicking and refusing
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.
Two More Rules
These features only work over HTTPS (and on localhost while you develop), which is why your deployed site must be on https from Day 18. And always pass a timeout, or a device that cannot find a position leaves your message saying "Finding you..." forever.
Picture This
Picture knocking on the neighbour's door with a reason ready ("may I borrow your ladder to fix the gutter?"), and a plan for no ladder, a yes, and a no.
2. The Clipboard
Definition
navigator.clipboard.writeText(text) copies text for the user, so they can paste it anywhere. It returns a Promise (Day 7), and only works when it is started by the user's own click.
Real-Life Example
A shop assistant writing the price on a slip and handing it to you, instead of making you copy it down yourself. And they say "here you go", so you know it happened.
What this code doesWe want a Copy link button that copies the link and confirms it visibly. The console reads the clipboard back to prove the copy really happened.
index.html
1
<button id="copy">Copy link</button>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const button = document.querySelector('#copy');
button.addEventListener('click', async () => {
try {
// Copy the link
await navigator.clipboard.writeText('https://webbo3.com/courses/javascript');
// Tell the user it worked
button.textContent = 'Copied';
// Prove it: read the clipboard back
console.log('clipboard now holds:', await navigator.clipboard.readText());
} catch (error) {
// Blocked: offer the keyboard instead
button.textContent = 'Press Ctrl+C';
}
});
BROWSERindex.html · after clicking Copy link
CONSOLE BROWSER DEVTOOLS
clipboard now holds: https://webbo3.com/courses/javascript
Common Mistake: No Feedback
A copy button that shows nothing gets pressed four times, because the user cannot tell it worked. Always confirm visibly, then change the label back after a moment.
Picture This
Picture the assistant handing you the slip and saying "here you go". The words are as important as the slip.
3. Dates, Without a Library
Definition
Intl is built into every browser and into Node. It formats dates, times and "3 days ago" in the language and region you ask for, including month names and time zones.
Real-Life Example
The same date is "15 March 2026" to a Nigerian reader and "March 15, 2026" to an American one. A good receptionist writes it the way the reader expects. Intl is that receptionist.
Types
toISOString(): the exact form for computers and APIs.
toLocaleDateString() and toLocaleString(): dates for people, in their style.
Intl.RelativeTimeFormat: "yesterday", "3 days ago", "in 3 days".
What this code doesWe want to show one date three ways: exactly for an API, and readably for a person in Lagos.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// One moment in time, written in the exact computer form
const joined = new Date('2026-03-15T09:30:00Z');
// For machines and APIs
console.log(joined.toISOString());
// For a Nigerian reader: day, full month name, year
console.log(joined.toLocaleDateString('en-NG', {
day: 'numeric', month: 'long', year: 'numeric'
}));
// Date and time, shown in Lagos time
console.log(joined.toLocaleString('en-NG', {
dateStyle: 'medium', timeStyle: 'short', timeZone: 'Africa/Lagos'
}));
OUTPUT TERMINAL
➜ webbo3 node app.js
2026-03-15T09:30:00.000Z
15 March 2026
15 Mar 2026, 10:30
There are 86,400,000 milliseconds in a day, so dividing the gap between two dates by that gives the number of days. Math.round rounds to the nearest whole day.
What this code doesWe want "yesterday" and "3 days ago" worked out from real dates, without a library.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// A formatter for relative times, in English
const relative = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
// Days between two dates, as words
function ago(from, to) {
const days = Math.round((from - to) / 86400000);
return relative.format(days, 'day');
}
// Measure against a fixed "now" so the example always prints the same
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));
OUTPUT TERMINAL
➜ webbo3 node app.js
yesterday
3 days ago
in 3 days
Date Arithmetic Has Sharp Edges
Months are counted from zero in this form, so new Date(2026, 0, 1) is January. Text like "15/03/2026" is not read reliably by every browser: use the ISO form "2026-03-15".
What this code doesWe want to see the zero-based month surprise, and the date form that is always read correctly.
app.js
1
2
3
4
5
// Month 0 means January
console.log(new Date(2026, 0, 1).toDateString());
// The ISO form is read the same way everywhere
console.log(new Date('2026-03-15').toISOString());
OUTPUT TERMINAL
➜ webbo3 node app.js
Thu Jan 01 2026
2026-03-15T00:00:00.000Z
Picture This
Picture the receptionist writing each date the way the reader expects, while the filing system underneath keeps one exact form.
4. The URL as State
Definition
Putting the search and page into the URL (the address bar) makes a view shareable: the link reproduces exactly what you were looking at. new URL(address) reads and edits addresses, and history.replaceState changes the address bar without reloading.
Real-Life Example
A librarian's reference: "Shelf C, row 4, third book." Anyone with that note walks straight to the same book, without you standing beside them to explain.
Reading and Changing a URL
What this code doesWe want to read the parts of an address, then change its options safely.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
// A full address, with two options after the ?
const url = new URL('https://webbo3.com/students?city=Lagos&page=2');
// Read its parts
console.log(url.pathname);
console.log(url.searchParams.get('city'));
console.log(url.searchParams.get('missing'));
// Change the options: spaces are encoded for you
url.searchParams.set('page', '3');
url.searchParams.set('search', 'chidinma okafor');
url.searchParams.delete('city');
console.log(url.toString());
OUTPUT TERMINAL
➜ webbo3 node app.js
/students
Lagos
null
https://webbo3.com/students?page=3&search=chidinma+okafor
Making a Shared Link Work
A student sends this link to a friend. When the friend opens it, the page reads the search from the address and shows the same results.
What this code doesWe want the page to read the search from the address when it opens, and keep the address updated as the user types, without adding history steps.
index.html
1
2
<input id="search" placeholder="Search">
<p id="shown"></p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const input = document.querySelector('#search');
const shown = document.querySelector('#shown');
// When the page opens: read the search from the address
const params = new URLSearchParams(window.location.search);
input.value = params.get('q') ?? '';
shown.textContent = `Showing results for: ${input.value}`;
// As the user types: write it back into the address, no reload
input.addEventListener('input', () => {
const url = new URL(window.location);
url.searchParams.set('q', input.value);
history.replaceState(null, '', url);
shown.textContent = `Showing results for: ${input.value}`;
console.log('address is now', window.location.search);
});
BROWSERindex.html?q=lagos · opened from a shared link, then changed the search to "lagos state"
CONSOLE BROWSER DEVTOOLS
address is now ?q=lagos+state
pushState or replaceState?
pushState adds a history step, so the Back button undoes it: right for moving between pages of results. replaceState edits the current step: right for a search box, or the user has to press Back once for every letter they typed.
Picture This
Picture the librarian's note: "Shelf C, row 4". The URL is that note for your page. Anyone holding it lands on exactly the same view.
5. Doing Work Only When It Is Visible
Definition
IntersectionObserver tells you when an element scrolls into view, so you can load or animate it only then. The old way, listening to every scroll and measuring positions, runs constantly and makes pages slow.
Real-Life Example
Motion-sensor lights in a corridor. They switch on as you approach each section, instead of lighting the whole building all night in case somebody walks through.
What this code doesWe want each card to be marked as seen only when it actually scrolls into view. The picture is after scrolling down to the last card.
index.html
1
2
3
4
5
<div class="card">Card 1</div>
<div class="spacer"></div>
<div class="card">Card 2</div>
<div class="spacer"></div>
<div class="card" id="last">Card 3</div>
style.css
1
2
3
.spacer { height: 1500px; }
.card { padding: 10px; border: 1px solid #ccc; }
.is-visible { background-color: #e0f2f1; }
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Called by the browser when watched elements come into view
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add('is-visible');
entry.target.textContent += ' (seen)';
console.log(entry.target.textContent);
// Stop watching this one: its work is done
observer.unobserve(entry.target);
});
}, { rootMargin: '100px' });
// Watch every card
document.querySelectorAll('.card').forEach((card) => observer.observe(card));
BROWSERindex.html · after scrolling to the last card
CONSOLE BROWSER DEVTOOLS
Card 1 (seen)
Card 3 (seen)
Card 2 was never on screen, so it was never marked, and no work was done for it. rootMargin 100px starts the work slightly before an element arrives, so the user never sees it happen.
What this code doesWe want an image the browser only downloads when it is about to scroll into view.
index.html
1
<img src="team.jpg" alt="The Webbo3 team" width="800" height="533" loading="lazy">
NOTE
For images you need no JavaScript at all: loading="lazy" does it. width and height let the browser save the right space, so the page does not jump as the image arrives.
Picture This
Picture the motion-sensor corridor: light only where someone is, and nothing wasted on the empty stretches.
6. Files, Without Uploading Anything
Definition
An <input type="file"> lets the user choose a file. JavaScript can read its name, type and size straight away, in the browser, before anything is uploaded.
Real-Life Example
A receptionist glancing at your document before sending it upstairs: "This is a photo, and it is the right size, fine." It saves you a trip if it is obviously wrong. It is not a security check; upstairs still checks properly.
startsWith('image/') checks the start of the type, which is "image/png", "image/jpeg" and so on for every image.
What this code doesWe want to check a chosen file in the browser. First the user picks a PDF by mistake.
index.html
1
2
<input type="file" id="photo">
<p id="message"></p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const input = document.querySelector('#photo');
const message = document.querySelector('#message');
input.addEventListener('change', () => {
const file = input.files[0];
if (!file) return;
// Images only
if (!file.type.startsWith('image/')) {
message.textContent = `${file.name} is not an image. Images only, please.`;
return;
}
// Under 2MB (2 x 1024 x 1024 bytes)
if (file.size > 2 * 1024 * 1024) {
message.textContent = 'That image is over 2MB. Please choose a smaller one.';
return;
}
message.textContent = `${file.name} looks good (${file.size} bytes).`;
});
BROWSERindex.html · after choosing notes.pdf
What this code doesWe want to see the same check accept a small image.
index.html
1
2
<input type="file" id="photo">
<p id="message"></p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const input = document.querySelector('#photo');
const message = document.querySelector('#message');
input.addEventListener('change', () => {
const file = input.files[0];
if (!file) return;
// Images only
if (!file.type.startsWith('image/')) {
message.textContent = `${file.name} is not an image. Images only, please.`;
return;
}
// Under 2MB (2 x 1024 x 1024 bytes)
if (file.size > 2 * 1024 * 1024) {
message.textContent = 'That image is over 2MB. Please choose a smaller one.';
return;
}
message.textContent = `${file.name} looks good (${file.size} bytes).`;
});
BROWSERindex.html · after choosing avatar.png
Checking the Type in the Browser Is a Courtesy, Not Security
file.type comes from the file name and is trivial to fake. The check is there to give an honest user a fast, kind message. The server must check again and never trust what the browser said. Validate in the browser for kindness, and on the server for safety: that is Day 17 in one line.
Picture This
Picture the receptionist's glance: helpful, quick, and never the final word.
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, 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 that has no context, and the browser remembers the refusal, 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 counted from zero in that form; the day 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, such as a search box.
Q4. Your file input checks file.type. Is the upload now safe?
No. file.type comes from the file name and is trivial to fake. The browser check is a quick, kind message; the server must check independently.
Tick Before You Move On
-
☐
Every permission request happens on a click, and I handled a refusal
-
☐
My copy button confirms visibly
-
☐
Dates are formatted with Intl, not by cutting up strings
-
☐
My filtered view is in the URL, and a shared link reproduces it
-
☐
Every image has width, height and loading="lazy"
-
☐
I know why a browser file check is not security
Quick Recap
Every permission has three outcomes: unsupported, granted and refused.
The clipboard needs a user click, and always a visible "Copied".
Intl formats dates and relative times with no library; months count from zero.
The URL is state that gives you sharing and the Back button for free.
IntersectionObserver instead of scroll listeners; loading="lazy" for images.
Browser checks are a courtesy; the server checks for safety.
Final Scenario
A student shares your course explorer with a friend on WhatsApp. The friend taps the link and sees exactly the same filtered list, with dates like "joined yesterday", images that appear as they scroll, and a "Near me" button that asked politely and coped when they said no.
None of it needed a library. It was all already in the browser, waiting to be used.
Before you install something, ask: does the browser already do this? Very often, it does.
Tomorrow, Day 13: forms, UX and accessibility. The day you find out whether anything you have built can be used without a mouse.