CSE 134B Web Client Languages
  1. Interaction
  2. Linking
  3. What a click does
  4. What a Click Does

Interaction · Linking 03

What a Click Does

A link is one line of markup, and following it is an HTTP request. Between the click and the next page, the browser resolves an address, writes a request with a dozen headers, tells the next site where the person came from, follows whatever redirects the server sends back, and records a step in the history so the back button can undo it. This page follows one click through all of it, and shows which parts the page author controls.

The link introduced the <a> element and its href, and Where a link goes showed how that href becomes a full URL. This page starts where those two stop: the moment the URL is known and the browser has to do something with it. The URL itself is covered in depth on the URLs page; the request and response are the subject of the page-load series, whose film One URL, Seventy Requests follows everything that happens after the first response arrives. Here the focus is narrower and closer to the markup: what a link puts into the request, what can come back, and which attributes change either.

1. Activating a link

A person can follow a link in more ways than a click. They tap it on a touch screen. They move focus to it with the Tab key and press Enter. They use a screen reader's list of links, or a voice command such as "click Pricing". They open it in a new tab with a middle click, Ctrl-click or Cmd-click, or from the context menu, which also offers to copy the address, save the target or open it in a private window. A script can call link.click(). Every one of these is called activating the link, and the browser provides all of them, for free, for every <a href> on every page.

That last point is the strongest argument for using a real link. A <span> or <div> with a click handler that sets location.href can imitate one of these behaviors, the plain click, and loses the rest: there is no focus, no Enter key, no "open in new tab", no address to copy, no entry in a screen reader's links list, and no address for a search engine to follow. The link makes this case in full. Here it is enough to say that everything on this page applies to real links, and that a fake link has to rebuild each piece by hand, and usually does not.

Once a link is activated, the browser runs through the same steps every time:

  1. Fire a click event. Scripts on the page get a chance to react, and one of them can call event.preventDefault() to cancel everything that follows. This is how client-side routers take over links, a subject for Links you can trust.
  2. Resolve the URL from the href, against the document's address or its <base>.
  3. Choose where the new page goes, from target and the keys held down: this tab, a new tab, a named tab, or a parent frame.
  4. Decide whether this is a navigation at all. A link to a fragment in the same page only scrolls. A mailto: or tel: link is handed to another application. A download attribute turns the response into a saved file.
  5. Send the request, a GET for the URL, with headers the browser writes.
  6. Handle the response: render it, follow a redirect, save it as a file, show an error, or do nothing at all.
  7. Replace the document and add an entry to the tab's history, so the back button returns to the page the link was on.
From a click to the next pageSeven boxes in two rows. The first row is in the page: click event, which a script may cancel; resolve the URL from href and base; choose the target. The second row crosses the network and back: send a GET request; handle the response, which may be a redirect that loops back to sending another request; replace the document; add a history entry. Arrows join them in order.in the pageclick eventa script may cancel itresolve the URLhref + basechoose the targetthis tab, new tab, frameover the network and backGET requestheaders by the browserresponserender, redirect, savea redirect sends another requestnew documentreplaces the oldhistory entryfor the back button
Every link goes through the same steps. The page's markup decides the first three; the server decides what comes back.

The rest of this page takes the steps in order, starting with the request.

2. The request a link sends

Following a link sends a GET request for the resolved URL, with no body. The page author writes none of the request: the method is fixed, and every header is the browser's. What the request contains is still worth knowing exactly, because servers make decisions on it, and because some of it is information about the person that they never chose to share.

The course's echo endpoint, /api/echo, replies with the request it received. This is what arrived when Chrome followed an ordinary link to it from the URLs page, whose address at the time ended in ?q=secret#x:

{ "method": "GET", "path": "/api/echo", "headers": { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,…,*/*;q=0.8", "accept-encoding": "gzip, br", "accept-language": "en-US,en;q=0.9", "host": "cse134.site", "referer": "https://cse134.site/structure/urls/?q=secret", "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "same-origin", "sec-fetch-user": "?1", "upgrade-insecure-requests": "1", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) …" }, "bodyKind": "none", "body": null }

Each header says something about the click:

  • Accept lists HTML first. The browser is asking for a page, and a server that can answer the same URL in several formats uses this to choose one.
  • Accept-Language carries the person's language preferences, from their browser or operating system settings. The Web Speaks Every Language shows how a server can use it, and why it should not use it alone.
  • Referer, spelled with the original typo from the HTTP specification, is the address of the page the link was on. Note the query, ?q=secret, and the missing #x: the fragment is never sent. Section 3 is about this header.
  • The four Sec-Fetch- headers are fetch metadata. Sec-Fetch-Mode: navigate and Sec-Fetch-Dest: document say this is a page load, not an image or a script's fetch(). Sec-Fetch-Site says whether the request came from the same origin, the same site, another site, or nowhere (a typed address or a bookmark). Sec-Fetch-User: ?1 appears only when a person activated the navigation. A page cannot set any header starting with Sec-, so a server can trust these more than most, and use them to refuse requests that arrive from places they should not.
  • Cookies for the destination are sent too, when the destination has set any and its cookie rules allow it. A cookie marked SameSite=Lax (Chrome treats a cookie with no SameSite setting the same way) is sent on a link from another site, which is why following a link to a site keeps you signed in there. A cookie marked SameSite=Strict is not sent on that first click.

A link must never change anything

HTTP defines GET as a safe method: a request that only reads, and that the client can send any number of times without changing anything on the server. Browsers, search engine crawlers, link checkers, chat apps building previews and security scanners all rely on this. They follow links freely, sometimes before a person has clicked anything, because following a link is supposed to be harmless.

A link that deletes, buys, unsubscribes or signs out breaks that promise, and something will eventually follow it that was not meant to. In 2005, Google's Web Accelerator prefetched the links on pages its users were viewing, to make the next click faster. Web applications that deleted records through plain links (<a href="/items/7/delete">) lost data when the accelerator followed every delete link on an administration page. The lesson has not changed since: an action that changes something is a POST, sent by a form with a button, and a link only ever goes somewhere.

<!-- Wrong: anything that follows links will delete the item --> <a href="/items/7/delete">Delete</a> <!-- Right: a POST that only a deliberate button press sends --> <form action="/items/7/delete" method="post"> <button type="submit">Delete</button> </form>

The same rule explains why a form with method="get" is a link with parameters: it builds a URL from its fields and follows it, exactly as a link would. Search forms are rightly GET. Anything that changes data is not.

3. What the Referer tells the next site

The Referer header exists so that a server can know how people found it. Site owners use it to learn which pages send them visitors, and it once helped servers block other sites from embedding their images. It also tells every site a person visits where they were a moment ago, and a page's address can say a great deal: a search query, an account number, a medical topic, an order id, or a password-reset token sitting in a query string.

For most of the web's history, browsers sent the full address, query included, to any HTTPS destination. That changed between 2020 and 2021, when Chrome (version 85) and Firefox (version 87) changed their default referrer policy to strict-origin-when-cross-origin; Safari already trimmed cross-site referrers as part of its tracking protection. Under the current default:

  • A link to a page on the same origin sends the full address, path and query included.
  • A link to another origin sends only the origin: https://cse134.site/, with no path and no query.
  • A link from an HTTPS page to a plain HTTP page sends nothing at all.

The default is a good one, but it still sends the full address to the page's own origin, which means to its own logs, its analytics and every third-party script running on its pages. That is why Form Security says anything private belongs in a POST, not a query string.

Choosing a policy

A page can choose a different policy for one link, with the referrerpolicy attribute, or for all its links and requests, with a Referrer-Policy response header or a <meta name="referrer"> element. The header is spelled correctly, which is how HTTP's typo was fixed going forward. These are the values, for a link from https://cse134.site/interaction/linking/demos/referrer-lab?account=1042:

referrerpolicyTo the same originTo another HTTPS originTo an HTTP page
no-referrernothingnothingnothing
originoriginoriginorigin
same-originfull addressnothingnothing
strict-originoriginoriginnothing
origin-when-cross-originfull addressoriginorigin
strict-origin-when-cross-origin (default)full addressoriginnothing
no-referrer-when-downgrade (the old default)full addressfull addressnothing
unsafe-urlfull addressfull addressfull address

"Full address" never includes the fragment, or a username and password written into the URL. And browsers are increasingly reluctant to loosen the default for requests to other sites: in some situations, such as private windows, they ignore unsafe-url and no-referrer-when-downgrade for cross-site requests and send only the origin. Tightening always works; loosening is a request the browser may turn down.

<!-- One link --> <a href="https://partner.example/offer" referrerpolicy="no-referrer">Partner offer</a> <!-- One link, the older way: no Referer, and no opener (section 5) --> <a href="https://partner.example/offer" rel="noreferrer">Partner offer</a> <!-- Every link and request on the page --> <meta name="referrer" content="same-origin">

The demo has one row per policy, and two links in each row: one to /api/echo on cse134.site, and one to the same Worker at its workers.dev address, which is a different site. The demo's own address carries ?account=1042, standing in for anything private, so you can see exactly when it leaks.

When should a page tighten its policy? When its addresses carry anything private, a page should send no-referrer or same-origin, or better, keep the private thing out of the address. Password-reset and email-confirmation pages, which hold a secret token in their URL by design, should set Referrer-Policy: no-referrer so a link or an image on that page cannot carry the token to another site. Loosening the policy is rarely the right answer; if a partner needs to know a visit came from you, add an explicit parameter to the link, such as a campaign tag, and say so in your privacy notice. Links you can trust looks at those tracking parameters from the other side.

4. What comes back

The server answers the click with a status code, headers and, usually, a body. For an ordinary page the status is 200 OK, the Content-Type is text/html, and the browser renders it. But a link can get several other answers, and each one changes what the person sees.

StatusWhat the browser does with a navigation
200 OKShows the body if it can (HTML, an image, a PDF, plain text); otherwise saves it as a file. Content-Disposition: attachment asks it to save the file even when it could show it.
204 No ContentNothing. The browser keeps showing the page the link was on, and adds no history entry.
301, 302, 303, 307, 308Requests the address in the Location header instead, and shows that address in the address bar.
304 Not ModifiedUses its cached copy. Only possible when the browser asked conditionally; the film 304 follows one.
404 Not Found, 410 GoneShows the error page the server sent, or its own if the server sent none. 410 says the page was removed on purpose.
5xxShows the server's error page. Something failed at the other end; the link may work later.

Redirects

A redirect is the server saying "not here, there". The response carries a Location header with the new address and, usually, no body worth showing. The browser makes a second request for the new address, without asking the person, and the second response is what they see. This site has a real one. When the course moved its pages into five layers, the old address of the URLs page stopped existing, and the server now answers it with a redirect:

HTTP/2 301 location: https://cse134.site/structure/urls/

The five redirect codes differ in two ways: whether the move is permanent, and whether the browser keeps the original method when it follows the redirect.

CodeMeaningA POST becomesUse it for
301 Moved PermanentlyThe page lives at the new address from now ona GETPages that moved; a browser may cache it indefinitely
308 Permanent RedirectThe same, but keep the methoda POSTAn API or form endpoint that moved
302 FoundLook over there, this timea GETTemporary moves, short links that may change
307 Temporary RedirectThe same, but keep the methoda POSTTemporary moves of anything that takes a POST
303 See OtherThe answer is at another address; fetch it with GETa GETAfter a form POST succeeds: the "post, redirect, get" pattern

Why would a POST become a GET? Early browsers did that on 301 and 302 against the specification, the behavior became universal, and HTTP eventually wrote it down and added 307 and 308 for anyone who needed the method kept. A form that posts through a 301 arrives at the new address as a GET with no body, which is a surprisingly common way for a moved form to break. The 303 is the intended use of that conversion: after a form is submitted, the server answers with 303 See Other to a confirmation page, so reloading the confirmation page reloads a GET, and does not ask to send the order again.

The demo sends links and a small form through every kind of redirect on cse134.site's /go/ addresses, and shows what arrives after each hop.

What a redirect costs

Every redirect is a full round trip before the page can even start to load: the browser sends a request, waits for the answer, and only then sends the request it needed. If the redirect points at another host, add a DNS lookup and a new TLS connection too. On a fast connection that is tens of milliseconds; on a mobile network it can be most of a second, per hop, and the person sees a blank tab while it happens. Before the First Byte follows that wait in detail.

Some redirects are worth it, such as a moved page whose old address is still linked from elsewhere. Others are free to remove, because they are caused by the page's own links. This site, like most, redirects an address without its trailing slash to the one with it:

HTTP/2 307 location: /structure/urls/

A link written as href="/structure/urls" works, and costs every visitor an extra round trip. Write href="/structure/urls/" and it costs nothing. The same goes for http:// links to sites that redirect to https://, and for links to example.com that redirect to www.example.com. Link to the final address.

A short link, such as a bit.ly address, a QR code on a poster or a link wrapped by a chat app or a social network, is a redirect and nothing more: a server whose only job is to answer with a Location header. That makes every short link a round trip to a third party before the real request, a record of the click on someone else's server, and a destination nobody can see before they follow it. Links that pass through several services pick up a chain of hops. Links you can trust covers short links in full: what they are good for, what goes wrong, and how to check one before following it.

Not found and gone

A link to a page that does not exist gets a 404. It should be a real 404 status, not a "page not found" message sent with a 200, which search engines call a soft 404 and which tells every link checker the broken link is fine. A good 404 page says plainly that the page is missing, keeps the site's navigation, and offers a search. When a page was removed deliberately and has no replacement, 410 Gone says so more precisely. When it has a replacement, a 301 to the replacement is better than either, because every link anyone ever made to the old address keeps working.

5. Where the new page opens

By default, the new page replaces the current one in the same tab. The target attribute chooses another place:

targetWhere the page opens
_self (the default)The same tab or frame as the link
_blankA new tab, or a new window if the browser is set that way
_parent, _topThe frame that contains this one, or the whole tab; used by links inside an <iframe>
any other nameThe tab or frame with that name, created the first time and reused after that

The person has the last word. A middle click or Ctrl-click opens a new tab whatever the target says, and many people prefer to choose for themselves. That is why Links people can use recommends leaving most links in the same tab, and saying so when one is not.

The opener and tabnabbing

A page opened in a new tab used to get a live reference to the tab that opened it, through window.opener. That reference crosses origins: a page on another site could not read the original page, but it could send it somewhere else, with window.opener.location = "…". The attack built on this is called tabnabbing. A person follows a link to another site in a new tab; that site quietly sends the original tab to a convincing copy of its login page; when the person returns to the tab they started in, they find themselves "signed out", and sign in again on the attacker's page.

Tabnabbing through window.openerTwo tabs side by side. Tab one shows mail.example, with a link marked target underscore blank. An arrow labeled "the click opens" leads to tab two, other.example. A red arrow labeled window.opener.location goes back from tab two to tab one, which now shows a fake login page at a lookalike address. A note says that since 2021 browsers give tab two no opener unless the link asks for one with rel opener.tab 1: mail.example<a target="_blank">later: a fake login pageat a lookalike addressthe click openstab 2: other.examplewindow.opener.location = "…"sends tab 1 awaysince 2021, target="_blank" opens with no opener; rel="opener" asks for the old behavior
Tabnabbing: the new tab sends the old one somewhere else. Browsers closed the default path; a page can still reopen it with rel="opener" or window.open().

For years the fix was to add rel="noopener" to every target="_blank" link, and much advice still says so. Browsers have since made it the default: Safari 12.1, Firefox 79 and Chrome 88, all by early 2021, give a page opened by a target="_blank" link no opener unless the link asks for one with rel="opener". Writing rel="noopener" is now harmless and unnecessary on links. It still matters for window.open(), which keeps the opener unless you pass "noopener" in its third argument.

<!-- No opener: the default for target="_blank" --> <a href="https://other.example/" target="_blank">Other site (opens in a new tab)</a> <!-- An opener, on purpose: only for pages you trust, such as your own sign-in popup --> <a href="/auth/start" target="_blank" rel="opener">Sign in (opens in a new tab)</a> <!-- No opener and no Referer --> <a href="https://other.example/" target="_blank" rel="noreferrer">Other site (opens in a new tab)</a>

6. When a click is not a navigation

Some links never load a new page. The browser recognizes them at the fourth step in section 1 and does something else.

Links to the same page

A link whose URL differs from the current page's only in its fragment, such as href="#summary", sends no request. The browser scrolls to the element with that id, updates the address bar, and adds a history entry, so the back button scrolls back. Where a link goes covers fragments, the :target selector and text fragments.

Links handed to another application

mailto:, tel: and sms: links, and links with an app's own scheme, are not requested by the browser at all. It passes the URL to whatever application the person's system has registered for that scheme: a mail program, the phone dialer, a messaging app. Where a link goes covers each scheme. If nothing is registered, often the case for mailto: on a shared or work computer, nothing visible happens, which is one reason to show the email address as the link text rather than only "Email us".

The download attribute

The download attribute asks the browser to save the response as a file instead of showing it, and its value, if any, suggests a file name:

<a href="/files/syllabus-2026.pdf" download="CSE134B-syllabus.pdf"> Syllabus (PDF, 240 KB) </a>

It only works for addresses on the page's own origin, and for data: and blob: URLs a script made. For any other address the browser ignores it and follows the link normally, because one site deciding what another site's file should be called, and that it should be saved, is a security problem. The server can always ask for a download itself, with Content-Disposition: attachment, as "A response that is a file" does in the redirect demo. Either way, the link text should say that it downloads, and what: the format and size, as above.

The ping attribute

Sites like to know which links people follow. The old way to find out is a redirect: the link points at the site's own tracking address, which records the click and redirects to the real destination. That costs a round trip, hides the destination from the status bar, and breaks if the tracker is down. HTML's answer is the ping attribute:

<a href="https://partner.example/" ping="/api/clicks">Partner</a>

The link goes straight to its destination, and the browser separately sends a POST to each URL in ping, with the body PING and headers naming the page and the destination. Chrome, Edge and Safari send pings; Firefox does not, by default. The attribute is honest about the tracking, since the markup shows it, but it is still tracking, and some people object to a feature built for it. Many sites use neither, and record clicks with a script that calls navigator.sendBeacon() as the page is left.

Links that a script takes over

A click listener that calls event.preventDefault() cancels the navigation, and the script can then do whatever it likes: load content with fetch() and update the page, open a dialog, or record the click first. Single-page applications take over every link this way. It is legitimate, and it is also the easiest way to break the rest of this page: the back button, new tabs, the address bar and scroll position all become the script's job. Links you can trust ends with what a script has to keep working when it does.

7. History and the back button

Each tab keeps a list of the pages it has shown, its session history. Following a link adds an entry after the current one, and drops any entries that were ahead of it, so after going back twice and following a new link, "forward" is gone. The back button is the most used navigation control on the web, and people use it on the assumption that it undoes the last thing they did. A page meets that expectation for free, as long as its links are links.

Three details are worth knowing:

  • Redirects add no entries. Only the final address is recorded, so "back" from a page reached through a redirect skips the redirect. (A page that redirects with script, by setting location after it loads, does add an entry, and traps the back button: going back lands on the redirecting page, which sends the person forward again. Use a server redirect, or location.replace(), which replaces the current entry instead of adding one.)
  • Fragment links add entries but no requests. Back scrolls to where the person was.
  • Going back is usually instant. Browsers keep recently left pages in a back/forward cache, frozen in memory with their scroll position, form contents and script state, and restore them without a request. A page can lose that benefit: one that listens for the unload event, for example, is often excluded. The Application panel in Chrome's DevTools has a back/forward cache test that lists the reasons a page cannot be cached.

8. Making clicks faster

Everything in this page happens after the click: the request, the redirects, the response, the rendering. The fastest navigation is one where some of that is already done when the person clicks. Browsers do some of this on their own, warming up connections to sites the person visits often. A page can help in three ways.

  • Remove the redirects your own links cause, as in section 4: the trailing slash, http://, the bare domain, and tracking redirects.
  • Hint at what comes next with <link rel="preconnect"> for a host the next page will need, and <link rel="prefetch"> for a page the person is likely to visit next. Links between documents covers these.
  • Use speculation rules, a JSON block that tells the browser which links it may fetch, or fully render in the background, before they are clicked:
<script type="speculationrules"> { "prefetch": [{ "where": { "href_matches": "/*" }, "eagerness": "moderate" }] } </script>

With "eagerness": "moderate", the browser fetches a link's page when the pointer rests on the link for a moment, which is usually long enough before the click to make the page appear almost at once. Speculation rules are supported in Chromium-based browsers; other browsers ignore the block, so it is safe to add as an enhancement. Use prerender in place of prefetch and the browser renders the whole page, scripts and all, in a hidden tab, which is faster again and costs more.

And here section 2 returns. Prefetching and prerendering follow links before anyone clicks them. Every link on a page that has speculation rules may be requested by a person who never chose to visit it. That is safe only if every link is safe, and links that sign out, delete or buy are exactly the ones that must not be followed in the background. Exclude them with "not" rules if they exist, and better, make them forms.

9. Summary

ConceptKey points
ActivationClick, tap, Enter, middle-click, context menu, screen-reader link lists, voice, click(). All come free with a real <a href>; a fake link rebuilds them by hand. Steps: click event, resolve, target, navigation or not, request, response, new document and history entry.
The requestA GET with no body; every header is the browser's. Accept, Accept-Language, Referer, cookies, and fetch metadata (Sec-Fetch-Site, -Mode, -Dest, -User).
GET is safeCrawlers, previews, scanners and prefetchers follow links unasked. A link never changes anything; actions are POST forms with buttons.
RefererDefault strict-origin-when-cross-origin: full address to the same origin, origin only to others, nothing to HTTP. Never the fragment. Tighten with referrerpolicy, rel="noreferrer", Referrer-Policy or <meta name="referrer">; browsers may refuse to loosen it.
Responses200 renders or saves; 204 does nothing; 3xx redirects; 304 uses the cache; 404 and 410 are errors, sent as real statuses (no soft 404s).
Redirects301 and 308 permanent, 302 and 307 temporary; 307 and 308 keep a POST, 301 and 302 turn it into a GET; 303 after a form POST. Each hop is a round trip: link to the final address.
Short linksA redirect through a third party: an extra round trip, a record of the click, a hidden destination. Full coverage in Links you can trust.
Targets and opener_self, _blank, _parent, _top, named tabs. target="_blank" implies noopener since 2021; rel="opener" asks for it back; window.open() still needs "noopener".
Not a navigationFragments scroll; mailto: and tel: go to another app; download saves (same origin only); ping reports clicks (not in Firefox by default); preventDefault() hands the link to a script.
HistoryEach navigation adds an entry; redirects add none; fragment links add one; script redirects trap the back button unless they use location.replace(). The back/forward cache restores pages instantly.
Faster clicksRemove your own redirects; preconnect and prefetch; speculation rules (Chromium, ignored elsewhere). Anything prefetched must be safe to follow.