CSE 134B Web Client Languages
  1. Interaction
  2. Linking
  3. Where a link goes
  4. Where a Link Goes

Interaction · Linking 02

Where a Link Goes

An href is rarely a full address. Most are fragments of one, such as page.html, ../notes/ or #summary, and the browser completes them against the address of the page they sit on. This page works through that completion step by step, shows where it goes wrong, and then follows the parts of a URL that never reach a server at all: fragments, text fragments, and the schemes that hand a link to another application.

The link introduced the <a> element and ended with the three things that happen when someone follows one: the browser resolves the URL, makes a request, and replaces the document. This page is about the first of those steps. What a click does picks up the other two. The URLs page already covers the anatomy of a URL (scheme, host, path, query and fragment) and percent-encoding, so this page assumes both and concentrates on what a link author has to decide: how to write the href so that it ends up where it should, from every page it appears on, on every server the site is ever copied to.

1. From href to URL

A browser cannot request page.html. A request needs a full address, with a scheme and a host, so before the browser does anything else with a link it turns the href into an absolute URL. It does this by resolving the href against a base URL. The base URL is the address of the document the link is in, unless the document has a <base href> element, which section 2 covers.

The rules come from the WHATWG URL Standard, and the same parser is available to scripts as new URL(href, base). That makes resolution easy to test: anything the browser does with a link, new URL() does the same way. The idea behind the rules is simple. The href says how much of the address it replaces, and everything it leaves out is copied from the base:

  • An href with a scheme (https:, mailto:) is already absolute. The base is ignored.
  • An href starting with two slashes () replaces everything after the scheme. It keeps only the base's scheme.
  • An href starting with one slash (/syllabus/) replaces the whole path. It keeps the base's scheme and host.
  • An href starting with a path segment (page.html, ../week4/) is merged with the base's path. It keeps the base's directory.
  • An href starting with ? replaces only the query.
  • An href starting with # replaces only the fragment.
  • An empty href replaces nothing but the fragment, which it drops.

The middle case, the path merge, is the one that surprises people. The browser takes the base's path up to and including its last slash, which is the base's "directory", and appends the href. Then it removes each ./, and for each ../ it removes the segment before it. The base's query and fragment are always dropped in a path merge.

Resolving ../week4/ against a page addressFour rows. The base URL, https://example.com/course/week3/notes.html?view=print#intro, is split into origin, path, query and fragment. Step one keeps the directory, https://example.com/course/week3/, dropping notes.html, the query and the fragment. Step two applies the two dots, which climb one level to https://example.com/course/. Step three appends week4/, giving https://example.com/course/week4/.base: the page's addresshttps://example.com/course/week3/notes.html?view=print#intro1. keep the directory; drop the resthttps://example.com/course/week3/2. ../ removes one segment: week3https://example.com/course/3. append the rest: week4/https://example.com/course/week4/the absolute URL the browser requests
A relative path is merged with the base's directory, not with the base itself. The file name, query and fragment of the page never survive the merge.

Worked examples

Every row below resolves against the same base, a page at https://example.com/course/week3/notes.html?view=print#intro. Read down the second column and notice how much of the base each kind of href keeps.

hrefResolves toWhat it kept from the base
page.htmlhttps://example.com/course/week3/page.htmlScheme, host and directory
./page.htmlhttps://example.com/course/week3/page.htmlThe same: ./ means "this directory" and adds nothing
../week4/https://example.com/course/week4/Scheme, host, and the directory one level up
../../https://example.com/Scheme and host; two levels up is the root
/syllabus/https://example.com/syllabus/Scheme and host only
https://cdn.example.net/x.jsThe scheme only
?view=screenhttps://example.com/course/week3/notes.html?view=screenEverything but the query; the fragment is dropped
#tophttps://example.com/course/week3/notes.html?view=print#topEverything but the fragment
(empty)https://example.com/course/week3/notes.html?view=printEverything but the fragment, which is removed
https://other.example/https://other.example/Nothing; it was already absolute

The scheme-relative form, , belongs to the years when sites served both HTTP and HTTPS; write https:// today, as the URLs page explains. The query-only form suits filters and pages of results. The empty href reloads the page itself, which is rarely what anyone meant; it usually marks a value left out by mistake.

Two more rules complete the picture. A ../ that would climb above the root is ignored, so ../../../../x from /a/ is just /x. And characters that cannot appear in a URL, such as spaces or non-ASCII letters, are percent-encoded as the URL is built: /my notes/café becomes /my%20notes/caf%C3%A9. The browser repairs the address, but it is better not to need repair: name files without spaces.

The demo resolves any href against any base with new URL(), and the table under it holds these examples and a few traps. Each one also says what following the link would do: request a new document, only scroll, or hand the URL to another application.

The same resolution applies to every URL in a document, not only links: src on images and scripts, href on <link> elements, a form's action, and the url() values in an inline style. Inside an external stylesheet the rules are the same, but the base is the stylesheet's own address, not the page's. That is why a background image path that works in a <style> element can break when the same CSS moves to a file in another directory.

2. The base element, and why to leave it out

The <base> element changes the base URL for the whole document. With it, every relative URL on the page resolves against the address it names, instead of against the page's own address:

<head> <meta charset="utf-8"> <base href="https://example.com/docs/v2/"> <title>Install</title> <link rel="stylesheet" href="styles.css"> </head> <body> <!-- https://example.com/docs/v2/setup/ --> <a href="setup/">Setup</a> <!-- https://example.com/docs/v2/#faq, not this page's #faq --> <a href="#faq">FAQ</a> </body>

It looks like a convenience: write short paths and set the directory once. In practice it is a frequent source of broken pages, and most pages should not use it. These are its traps.

Fragment links stop being same-page links

A link such as href="#faq" resolves against the base like any other, so with the base above it points to https://example.com/docs/v2/#faq. On any page other than the base address itself, that is a different document: clicking it loads the base page and scrolls to its #faq, if it has one. Every table of contents, skip link and "back to top" link on the page quietly becomes a link to another page. The fix is to write the full page path in each fragment link, which removes the convenience <base> was meant to provide.

Every relative URL moves, not just links

The base applies to every URL attribute in the document: images, scripts, stylesheets, form actions, <link rel="canonical">, and the URLs a script resolves with new URL(x, document.baseURI). A base added to fix the links in one part of a page moves all of these too. Some consumers ignore it entirely: social-media preview cards read Open Graph tags such as og:image without applying the base, so those need absolute URLs either way.

Only one, first in the head, and mind the slash

A document may contain only one <base>. If there are several, browsers use the first href and ignore the rest, so a second one added by a template or a plugin silently does nothing. The element also has to come before any element that uses a URL, because elements that appear before it have already resolved their URLs against the page address. Put it right after <meta charset>, or leave it out.

And the base is resolved by the same rules as everything else, so its trailing slash matters. An old version of this course's slides set <base href="http://www.bigcompany.com/htmldocs">, with no final slash. The base's directory is then /, not /htmldocs/, and href="page.html" resolves to /page.html. A base for a directory needs the slash; the resolver demo has this case as a preset.

The target attribute

<base> can also carry a target, which becomes the default target of every link and form on the page. <base target="_blank"> opens every link in a new tab, without any warning on the links themselves, which Links people can use argues against. It is defensible in one place: a document shown inside an <iframe>, where <base target="_top"> makes its links replace the whole tab instead of loading inside the frame.

When a base is reasonable

A single-page application that serves the same HTML from many addresses (/app/, /app/users/42) needs its relative asset URLs to resolve the same way from all of them, and frameworks often write a <base href="/app/"> for that reason. For an ordinary site, root-relative links, the subject of the next section, do the same job without moving fragment links.

For a link to another page on the same site there are three reasonable ways to write the href, and each one survives a different kind of move. The URLs page lists them; this section is about choosing.

FormExampleDepends onBreaks when
Absolutehttps://cse134.site/structure/urls/Nothing; always the same addressThe site moves to another domain, or the link should stay on a staging copy
Root-relative/structure/urls/The page's scheme and hostThe site is served from a subdirectory, or opened from disk with file://
Relative../../structure/urls/The page's own position in the siteThe page that holds the link moves to another directory

Root-relative for links within a site

For most sites, root-relative links are the right default for internal links. A link written as /structure/urls/ means the same page from every page on the site, at any depth, so a navigation menu can be copied into every page unchanged. It also works on any host the site is served from: a local development server at http://localhost:8080, a staging copy at a preview domain, and the real domain all resolve /structure/urls/ against their own host. That is why this site writes its links this way.

Root-relative links have two weaknesses. They assume the site lives at the root of its host. A site published at https://example.github.io/project/, as GitHub Pages does for project sites, finds that /structure/urls/ points outside it, to https://example.github.io/structure/urls/. Static site generators usually have a "base path" setting for this, which rewrites the links at build time. And they fail when a page is opened straight from disk. With a file:// address, the root is the root of the file system, so /structure/urls/ means a folder at the top of the computer's disk. Serve the files with a local web server, even a one-line one, and the problem disappears.

Relative for things that move together

Relative links depend on where the linking page is. That is a weakness when pages move independently, and a strength when a group of files moves as one. A tutorial folder with its own images, a set of demo pages that link to each other, or documentation that has to work both on a server and opened from disk all suit relative links: copy the folder anywhere, to any depth on any host, and its internal links keep working. The demos on this site link to each other this way, with href="fragments" rather than a path from the root.

The cost is that every relative link has to be right for the page it is on. Moving a page one directory deeper breaks each ../ in it, and a link copied from a page at one depth to a page at another silently points somewhere else.

Absolute when the link leaves the site

Absolute URLs are required for links to other sites, and for any link that will be read somewhere other than the page it was written on. The receiving software has no base URL to resolve a relative link against, or it has the wrong one:

  • Feeds. RSS and Atom readers show a post's content on their own pages. Relative links and images in the content resolve against the reader's address, or are dropped.
  • Email. An HTML email has no address of its own. Every link and image in it must be absolute.
  • Metadata and sharing. <link rel="canonical">, Open Graph images and sitemaps should all use absolute URLs, because search engines and preview cards read them out of context.
  • Copied content. Text pasted into a chat, a document or another site carries its links with it. A relative link loses its meaning once it leaves its page.

An absolute link to your own site is the one that ties a page to a single host. A staging copy whose links all say https://cse134.site/ sends anyone testing it to the live site, where they test the wrong thing. Generate absolute self-links from one setting at build time, so that each copy of the site writes its own host into them.

4. Trailing slashes and directory URLs

Web servers usually serve a directory's index.html for an address that names the directory, so /structure/urls/ returns src/structure/urls/index.html. The address of that page, the one relative links resolve against, depends on whether it was requested with a final slash or without one, and the two resolve relative links differently.

The same link from /a/b and from /a/b/Two columns. On the left, a page whose address is /a/b, without a final slash. Its directory is /a/, so the link href c resolves to /a/c, a sibling of b. On the right, the page at /a/b/, with the slash. Its directory is /a/b/, so the same link resolves to /a/b/c, a child of b. A note says the server may serve the same file at both addresses.page at /a/bdirectory: everything to the last //a/href="c"/a/ca sibling of bpage at /a/b/directory: everything to the last //a/b/href="c"/a/b/ca child of bthe server may send the same index.html for both addresses;the browser resolves its links against whichever address it asked for
One slash changes the base's directory, and with it every relative link on the page.

Without the slash, b looks like a file in the directory /a/, and a link to c means its sibling. With the slash, b is the directory, and c is inside it. If the server sent the same file for both addresses, the page's relative links, images and stylesheets would work at one address and break at the other.

Servers deal with this by choosing one form, the canonical form, and redirecting the other to it. Most choose the slash for directories. cse134.site answers /structure/urls with a 307 redirect to /structure/urls/, as What a click does shows, so relative links on its pages always resolve against the slashed address. The redirect makes every address work, and it still costs each visitor an extra round trip. The rule for authors is short: link to the canonical form. Write /structure/urls/, with the slash, and the redirect never happens.

The same goes for http:// versus https://, www. or not, and index.html versus the directory. Paths are case-sensitive on most servers too, so /About/ and /about/ may be different pages. Copy addresses from the address bar after any redirects, rather than typing them from memory.

5. Fragments

The fragment is the part of a URL after the #. It identifies a part of the document rather than a document, and it is handled entirely by the browser: it is never sent to the server. A request for https://cse134.site/structure/urls/#fragment asks the server for /structure/urls/ and nothing more, and the browser finds #fragment itself once the page has arrived. What a click does shows the request, with the fragment missing from the Referer too. The URLs page covers other uses of fragments, such as hash-based routing in single-page applications.

Ids are targets

Any element with an id can be the target of a fragment, with no other markup. To find the element for a fragment, the browser looks for:

  1. the first element whose id equals the fragment exactly;
  2. failing that, the first <a> element whose name equals it;
  3. failing that, the same two searches again with the fragment percent-decoded, so #caf%C3%A9 finds id="café";
  4. failing all of those, if the fragment is top, in any capitalization, the top of the document.

If nothing matches, the address changes and a history entry is added, but the page does not scroll. An empty fragment, href="#", also means the top of the document; that is why href="#" on something that is not really a link makes the page jump to the top when it is clicked.

<nav aria-label="On this page"> <a href="#install">Install</a> <a href="#configure">Configure</a> </nav> <h2 id="install">Install</h2> … <h2 id="configure">Configure</h2> … <!-- Works with no id="top": the browser treats #top as the top of the page --> <a href="#top">Back to the top</a> <!-- From another page: the path, then the fragment --> <a href="/docs/setup/#configure">Configuring the tool</a>

Fragments are case-sensitive. Ids are compared exactly, so #Install does not find id="install". Keep ids lowercase, with hyphens between words, and the question never comes up. Ids also have to be unique within the page, because only the first match is ever found. Once published, an id is part of an address that other people link to, so changing it breaks their links as surely as renaming the page would. This site's headings have ids so that any section can be linked; the heading-links element around this article adds the visible link next to each one.

The legacy name on a

Before every element could carry an id, the way to make a target was an empty anchor with a name: <a name="install"></a>. Browsers still honor it, after looking for an id, so old pages keep working, but the attribute is obsolete in HTML today and validators flag it. Put the id on the heading or section itself. That also makes the element the target, so :target can style it.

Scroll and focus

When a fragment matches an element, the browser does three things. It makes the element visible if it can (opening a closed <details> that contains it, in browsers that implement the current standard). It scrolls the element to the top of the viewport. And it moves focus:

  • If the element can take focus, because it is a link, a form control, or has tabindex="-1", it is focused, and a screen reader starts reading there.
  • If it cannot, which is the case for most headings and sections, focus is not placed on it, but the browser moves the sequential focus navigation starting point to it. The next press of Tab moves to the first focusable element after the target, not the one after the link that was clicked.

The second behavior is what makes skip links and tables of contents work for keyboard users without script. Older browsers got it wrong, hence the old advice to put tabindex="-1" on every skip-link target; today it is worth adding only when the target should really receive focus.

A fragment link to the same page sends no request, but it is still a navigation. It updates the address bar and adds an entry to the tab's history, so the back button returns to where the person was, and a hashchange event fires for scripts that care.

:target and scroll-margin

The :target pseudo-class matches the element the current fragment points to. It is useful for showing people where they landed, since a jump to the middle of a long page is easy to lose track of. A sticky header causes the other common problem: the browser scrolls the target to the top of the viewport, right underneath the header. scroll-margin-block-start on the target tells the browser to stop that much short:

@layer components { :root { --header-size: 4rem; } [id] { scroll-margin-block-start: calc(var(--header-size) + 1rem); } section:target, h2:target { background: var(--highlight); border-inline-start: 0.25rem solid var(--accent); } @media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } } }

The logical property scroll-margin-block-start follows the writing mode; in English it is the same as scroll-margin-top, which older articles use. It is supported in all current browsers. Smooth scrolling is wrapped in a prefers-reduced-motion query, so people who have asked their system for less motion get the instant jump.

The demo is a long page with a sticky bar of fragment links. Each section highlights when it is the target, and a readout shows what the page's scripts can see: location.hash, the :target element and the focused element.

6. Text fragments

An id fragment only works if the page's author put an id where someone wants to link. Text fragments remove that dependency: they point to a passage of text, wherever it is, and the browser finds it, scrolls to it and highlights it. The syntax goes after the #, behind a marker, :~:, that no ordinary id is likely to contain:

https://cse134.site/interaction/linking/destinations/#:~:text=never%20sent%20to%20the%20server https://cse134.site/interaction/linking/destinations/#:~:text=most%20pages,use%20it https://example.com/page#:~:text=the%20-,result,-%20is https://example.com/page#:~:text=first%20passage&text=second%20passage https://example.com/page#install:~:text=npm
The parts of a text directiveThe string #:~:text=prefix-,start,end,-suffix is split into labeled parts. The fragment directive marker colon tilde colon. text equals. An optional prefix followed by a dash, which must appear just before the match. The required start text. An optional end text after a comma, which turns the match into a range from start to end. An optional suffix preceded by a dash, which must appear just after the match. Only start to end is highlighted.#:~:text=prefix-,start,end,-suffixdirectivemarkera textdirectiveoptional: mustcome just beforerequired:where the match startsoptional: makesa range to hereoptional: mustcome just afterhighlightedeach part is percent-encoded, including any - , or & in the text itself;several directives join with &, and matching ignores case
A text directive. Only the start, or the range from start to end, is highlighted; the prefix and suffix only decide which occurrence matches.

The parts work like this:

  • text=start highlights the first occurrence of start on the page. Matching ignores case.
  • text=start,end highlights a range: from the first start to the next end after it. A long passage can be linked with a few words from each end, instead of all of it.
  • prefix-, and ,-suffix say what must come immediately before or after the match. They pick out one occurrence of a common phrase; neither is highlighted. In the third example above, only a "result" preceded by "the" and followed by "is" matches.
  • Several text= directives joined by & highlight several passages.
  • The text is percent-encoded, as encodeURIComponent() does it, and a dash, comma or ampersand that is part of the text itself must be encoded too, as %2D, %2C and %26, so that it is not read as syntax.
  • An ordinary fragment can come first, as in the last example. If the text is not found, a browser that supports text fragments scrolls to #install instead. (A browser without support looks for an id of install:~:text=npm, finds none, and opens the page at the top.)

Each start, end, prefix and suffix has to fall within a single block of text, such as one paragraph or list item, but a range can run across several. If nothing matches, the directive is ignored and the page opens as it would have without it.

What they are for

Text fragments are a linking feature for readers, not for authors. Their main uses are citing a sentence in a long document, sharing the paragraph that answers a question, and linking into pages the linker does not control. Chromium browsers have a "Copy link to highlight" command in the context menu that builds the URL from a selection, and other browsers have added similar commands. Search engines use them too: a search result that jumps to the matching passage and highlights it is a text fragment link. Authors can style the highlight with the ::target-text pseudo-element, which the fragment lab uses.

They are fragile in a way id fragments are not: if the wording changes, the link finds nothing and the reader lands at the top of the right page. For a link meant to last, a stable id is better, and pages meant to be cited should give their sections ids.

Restrictions, and why

A link that makes a page search its own text could leak what is on that page. An attacker who could make a page scroll, and detect whether it scrolled, could test whether a page shows a particular name, diagnosis or balance, one guess at a time. The specification closes off that attack with restrictions, which also explain behavior that otherwise looks like a bug:

  • The page cannot see the directive. The browser removes everything from :~: onward before the page's scripts run, so location.hash does not include it, and the page cannot tell what was searched for or whether it was found. The address bar may still show it.
  • A person must be involved. The directive is applied only to navigations that come from a user action, such as a click or a typed address, not to a script that navigates on its own.
  • Top-level documents only. Text inside an <iframe> is not searched, and a frame that navigates does not apply a directive. That is why the fragment lab above has to be opened in its own tab for its text fragment links.
  • Cross-origin pages need isolation. When the link comes from another origin, the directive is applied only if the new page is not sharing a window group with other pages, the same isolation that noopener gives a new tab.
  • The page loads first. Text fragments take effect when a document loads. In Chrome, a link to #:~:text= on the page already open does not scroll, which is why the lab's text fragment links change the query as well: that makes them a new load of the same page.

A site that does not want text fragments applied to its pages can send the response header Document-Policy: force-load-at-top, which Chromium browsers honor. It also turns off scrolling to ordinary fragments on load, so it is rarely worth the cost.

Browser support

Chrome added text fragments in version 80, in 2020, and Edge followed with it. Safari added them in 16.1, in 2022, and Firefox in 131, in October 2024, so all current browsers support them. The ::target-text pseudo-element arrived later in Safari, in 18.2. A browser without support treats the whole thing as an ordinary fragment that matches nothing, and opens the page at the top, so a text fragment link never does worse than a plain link to the page. Scripts can test for support with 'fragmentDirective' in document.

7. Schemes beyond HTTP

Everything so far has been about https: links, which the browser fetches itself. A link's scheme can name other things, and for many of them the browser does not make a request at all. It hands the URL to whatever the operating system has registered for that scheme: a mail program, the phone dialer, an app. What a click does places that handoff in the sequence of a click. Here is what each scheme carries.

mailto:

A mailto: link opens a new message in the person's mail program, with the address filled in. Other header fields go in a query: subject, cc, bcc and body. Several recipients are separated by commas.

<a href="mailto:ta@example.com">ta@example.com</a> <a href="mailto:ta@example.com,tutor@example.com?cc=instructor@example.com&amp;subject=Week%203%20question&amp;body=Section%3A%20%0D%0AQuestion%3A%20"> Email the TAs about week 3 (ta@example.com) </a>

The values must be percent-encoded. A space is %20, not +, which mail programs show as a literal plus sign. A line break in the body is %0D%0A, and &, ? and # inside a value must be encoded so they are not read as URL syntax. Build the values with encodeURIComponent() rather than by hand. In the HTML, the & between fields is written &amp;, like any ampersand in an attribute, though browsers forgive the bare form.

A mailto: link only works for someone with a mail program set up to handle it. On a shared or work computer, or for someone who reads mail in a browser tab that has not registered as a handler, clicking it does nothing visible, or opens a mail program they never use. Make the address itself the link text, or show it next to the link, so the person can copy it whatever happens. And publishing an address in a page publishes it to spammers' harvesting tools as well; the old slides for this course called mailto: "useful for mailing links in a Web page and also for spammers". For anything more than a contact address, a form is usually the better tool, as Forms explains.

tel: and sms:

A tel: link starts a call; on a phone it opens the dialer with the number filled in, and on a computer it may offer a calling app or a phone linked to the computer. Write the number in full international form, with + and the country code, so it works for someone dialing from anywhere. Hyphens, dots and parentheses are allowed as visual separators and are ignored, so the link can match how the number is printed:

<!-- The link text in local format, the href in international format --> <a href="tel:+1-858-555-0142">(858) 555-0142</a> <!-- A text message, with a prefilled body --> <a href="sms:+18585550142?body=Running%2010%20minutes%20late">Text the front desk</a>

sms: opens a new text message, and its body parameter suggests the text. Messaging apps differ in how they read the parameters, so test a prefilled body on the phones that matter before relying on it. Both schemes make most sense on a phone. On a desktop they often do nothing, so the number should always be visible as text, as it is in the first link above.

data: and blob:

A data: URL contains its resource instead of pointing to one: data:text/plain,Hello is a tiny text file. The URLs page covers their syntax and their uses for small images. As link targets they are restricted: Chrome (since version 60), Firefox (59) and Safari (14) block a page from navigating the whole tab to a data: URL, because phishing pages used them to show a fake login form whose address bar showed no domain at all. A data: link with the download attribute still works, and saves the content as a file.

A blob: URL is made by a script, with URL.createObjectURL(), and names a block of data held in that page's memory, such as a file the person chose or an image a script drew. It looks like blob:https://cse134.site/6f1c…, it means nothing to any other page or device, and it stops working when the page closes or the script calls URL.revokeObjectURL(). In links, it is mostly used as the href of an <a download> that saves something the page built.

App and custom schemes

Applications register schemes of their own with the operating system: zoommtg:, slack:, spotify:, vscode: and many more. A link with such a scheme asks the system to open that application with the rest of the URL as its instructions, such as a meeting to join or a file to open. Browsers usually ask the person first, since the application may act on the link at once. Mobile platforms also let an app claim an ordinary https: domain, so its links open in the app when it is installed and in the browser when it is not, a fallback a custom scheme cannot offer.

A web application can register itself as the handler for a scheme with navigator.registerProtocolHandler(). The scheme must either be on a short list of standard ones (mailto, tel, sms, geo, webcal, magnet and others) or start with web+, such as web+recipe:. Chrome and Firefox support it, only on HTTPS pages; Safari does not. This is how a webmail service becomes the handler for mailto: links, which answers the problem in the previous section for people who use one.

file:

A file: URL names a file on the computer running the browser. It is what the address bar shows when an HTML file is opened from disk, and links between local files work. Links from a web page to file: addresses do not: a page served over HTTP or HTTPS is not allowed to send the person to their own file system, and the browser blocks the link with a message in the console. A file: link in published markup names a file on its author's disk, and is a sign of a page tested on only one computer.

When nothing is registered

If no application has registered a link's scheme, browsers differ. Some do nothing visible, some show an error page, and some show a dialog saying the address could not be opened. None of them tell the page, so a page cannot detect the failure and offer an alternative. Treat every non-HTTP link as something that may not work for some visitors, and give them another route: the email address or phone number as visible text, a web version of the app's page, or instructions.

SchemeHanded toIf nothing handles it
https:The browser, which sends a requestNot applicable
mailto:The registered mail program or web mail serviceNothing visible, or the wrong program opens
tel:, sms:The phone dialer or messaging appNothing, or an offer to install a calling app
data:The browser itselfTop-level navigation blocked; downloads work
blob:The browser, from the page's memoryFails once the page or the URL is gone
App schemes, web+…The registered application or web app, usually after a promptNothing, an error page or a dialog, depending on the browser
file:The browser, for local pages onlyBlocked from web pages

One scheme is missing from that table on purpose. javascript: URLs run code instead of going anywhere, and The link explains why a button is the right element for that. And every scheme here can be misused: links that hide where they go are the subject of Links you can trust.

8. Summary

ConceptKey points
ResolutionEvery href becomes an absolute URL, resolved against the base URL: the page's address, or its <base href>. The href replaces part of the base and keeps the rest; new URL(href, base) does exactly what the browser does.
Kinds of hrefScheme: absolute. : keeps the scheme. /: keeps scheme and host. A path: merged with the base's directory, with ./ and ../ applied. ?: new query. #: new fragment. Empty: the page itself, reloaded.
The base elementMoves every relative URL, including #fragment links, which then load another page. One only, first in the head, with a trailing slash for a directory. Most pages should not use it.
Choosing a formRoot-relative for links within a site; relative for files that move together or open from disk; absolute for other sites, feeds, email, metadata and copied content. Generate absolute self-links per host.
Trailing slashes/a/b and /a/b/ resolve relative links differently. Servers redirect one to the other; link to the canonical form and skip the redirect.
FragmentsNever sent to the server. Match an id exactly (case-sensitive), then a legacy <a name>; #top and # mean the top. Scroll, then focus the target if it can take focus, or move the Tab starting point. Add history entries. Style with :target; clear sticky headers with scroll-margin-block-start.
Text fragments#:~:text=[prefix-,]start[,end][,-suffix], percent-encoded. Top-level, user-initiated loads only; hidden from the page's scripts. Chrome 80, Safari 16.1, Firefox 131. Fall back to the top of the page.
Other schemesmailto: and sms: with encoded parameters; tel: in international form; data: blocked as a top-level destination; blob: lives with its page; app schemes and web+ handlers; file: blocked from the web. Nothing registered means nothing happens, so show the address or number as text.