Courses Learn Blog Contact
Get Started →
Lesson 12

Day 12: What the Browser Can Already Do

Permissions, clipboard, Intl dates, the URL as shareable state, IntersectionObserver and file previews: capabilities you do not have to install.

Webbo3 Level 2: JavaScript Development 17 min read Free

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.

JSapp.js
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 localhost for 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

JSapp.js
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

JSapp.js
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'
}));
Console

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.

JSapp.js
// "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));
Console

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.

JSapp.js
console.log(new Date(2026, 0, 1).toDateString());   // month 0 is January
console.log(new Date('2026-03-15').toISOString());  // ISO parses reliably
Console

4. 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.

JSapp.js
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());
Console
JSapp.js
// 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.

JSapp.js
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));
  • rootMargin starts the work slightly before the element arrives, so the user never sees it happen.
  • unobserve once 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.
HTMLindex.html
<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

JSapp.js
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.

  1. Put the search and page in the URL, and prove it by sending the link to yourself and opening it fresh.
  2. A Copy link button with visible confirmation.
  3. Format every date in the data with toLocaleDateString, and add a relative "joined 3 days ago".
  4. Lazy-load the images with the attribute, and give every one a width and height.
  5. 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.

Learn this with a real instructor

Free lessons take you a long way on your own. The full programmes add live classes, assignments, Skill Points and a certificate — built for African students.