The HTML base tag: href, target and what it breaks

One line in the head changes where every relative address on the page points. That is useful when you mean it, and a quiet source of broken links and jump links when you do not.

The <base> element sets the address that every relative URL on the page starts from. With <base href="https://example.com/docs/"> in the head, <a href="page.html"> goes to https://example.com/docs/page.html, wherever the page itself lives. Its other attribute, target, sets where links open by default.

Try it. Pick a base or type your own, and watch where each link in the table would go.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Where a base tag sends each link</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .page { font-size: 13px; color: #5b6270; margin: 0 0 8px; overflow-wrap: anywhere; }
  label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; }
  input { width: 100%; box-sizing: border-box; font: 14px ui-monospace, Consolas, monospace; padding: 8px; border: 1px solid #c9cdd4; border-radius: 8px; }
  .presets { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 12px; }
  .presets button { font: 12px system-ui, sans-serif; padding: 5px 9px; border: 1px solid #c9cdd4; border-radius: 99px; background: #fff; cursor: pointer; }
  table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 10px; overflow: hidden; font-size: 12.5px; }
  th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid #eceef2; vertical-align: top; }
  th { background: #eef1f5; font-size: 12px; }
  td code { font: 12px ui-monospace, Consolas, monospace; overflow-wrap: anywhere; }
  tr.moved td { background: #fff4ec; }
  tr.moved td:last-child { color: #9a3412; }
</style>
</head>
<body>
<p class="page">This page lives at <b id="page">https://example.com/blog/post.html</b></p>

<label for="base">&lt;base href="…"&gt;</label>
<input id="base" value="https://example.com/docs/" spellcheck="false">
<div class="presets">
  <button data-v="">No base</button>
  <button data-v="https://example.com/docs/">docs/</button>
  <button data-v="https://example.com/docs">docs (no slash)</button>
  <button data-v="https://cdn.example.org/v2/">Another site</button>
</div>

<table>
  <thead><tr><th>Link in the page</th><th>Where it goes</th></tr></thead>
  <tbody id="rows"></tbody>
</table>

<script>
  const PAGE = document.getElementById('page').textContent;
  const LINKS = ['page.html', 'img/logo.png', '../index.html', '/about.html', '?page=2', '#contact'];

  // A scratch document with a real <base> and <a>: the browser does the resolving
  const doc = document.implementation.createHTMLDocument('');
  const base = doc.head.appendChild(doc.createElement('base'));
  const a = doc.body.appendChild(doc.createElement('a'));

  function resolve(href, from) {
    base.setAttribute('href', from);
    a.setAttribute('href', href);
    return a.href;
  }

  function render() {
    const value = document.getElementById('base').value.trim();
    // A relative base href is itself resolved against the page address
    const from = value ? resolve(value, PAGE) : PAGE;
    document.getElementById('rows').innerHTML = LINKS.map((href) => {
      const to = resolve(href, from);
      const moved = href.startsWith('#') && to.split('#')[0] !== PAGE;
      return `<tr class="${moved ? 'moved' : ''}"><td><code>${href}</code></td>` +
        `<td><code>${to}</code>${moved ? '<br>Loads another page instead of scrolling' : ''}</td></tr>`;
    }).join('');
  }

  document.getElementById('base').addEventListener('input', render);
  document.querySelectorAll('.presets button').forEach((b) => {
    b.addEventListener('click', () => {
      document.getElementById('base').value = b.dataset.v;
      render();
    });
  });
  render();
</script>
</body>
</html>
Six ordinary links on a page at /blog/post.html. Change the base and the destinations change with it.

The row to watch is #contact. With no base, it scrolls this page. With almost any base, it loads a different page.

What the base tag looks like

It is a void element: one tag, no closing tag, and it belongs in the <head>.

<head>
  <meta charset="utf-8">
  <base href="https://example.com/docs/" target="_self">
  <link rel="stylesheet" href="style.css">
</head>

It has two attributes, and either one alone is fine:

Attribute What it sets Example
href The starting address for every relative URL <base href="/docs/">
target Where links and forms open when they have no target of their own <base target="_blank">

Only the first <base> with an href counts, and only the first with a target. A second base tag is silently ignored.

The standard also asks for the base to come before any other element that uses an address, so put it near the top of the head. The head tag guide shows where it sits among the rest.

Without a base, a relative address starts from the folder the page sits in. With one, it starts from the base instead.

The same img/logo.png, resolved from the page's folder and from the base.
The same img/logo.png, resolved from the page's folder and from the base.

Everything that takes an address follows the base, not just links:

  • <a href> and <area href>
  • <img src>, <video src>, <source>
  • <link href> for stylesheets and icons, and <script src>
  • <form action>
  • url() in a <style> block or style attribute
  • fetch('api/items') in scripts

Each kind of relative address behaves the way it would from a page at the base address:

In the page With base https://example.com/docs/
page.html https://example.com/docs/page.html
../index.html https://example.com/index.html
/about.html https://example.com/about.html
?page=2 https://example.com/docs/?page=2
#contact https://example.com/docs/#contact
https://other.example/ Unchanged. Full addresses ignore the base

A base href can itself be relative, such as <base href="assets/">. It is resolved against the page address first. For the wider picture of the four kinds of path, see relative vs absolute paths.

The trailing slash matters

A base address is read exactly like a page address. The part after the last / is treated as a file name and dropped when a relative link is added.

Without the trailing slash, docs is treated as a file and the link lands one folder too high.
Without the trailing slash, docs is treated as a file and the link lands one folder too high.

So <base href="https://example.com/docs"> sends page.html to https://example.com/page.html, not into docs. The browser shows no warning. Pick the "docs (no slash)" button in the first example to see it.

The #anchor trap

This is the base tag's best-known surprise. An in-page link like <a href="#faq"> is a relative address too, and it resolves against the base like any other.

#faq resolves against the base. Writing the page's own path in front keeps it on the page.
#faq resolves against the base. Writing the page's own path in front keeps it on the page.

If the base is not this exact page, the result is another page's address plus #faq. Clicking it leaves the page instead of scrolling. Three ways out:

  1. Write the page's own address before the #. <a href="/blog/post.html#faq"> resolves to the current address, so the browser only scrolls. If the base is on another site, use the full address instead.
  2. Handle jump links in JavaScript. Catch clicks on a[href^="#"], call preventDefault() and then scrollIntoView() on the target element.
  3. Drop the base. Use full or root-relative addresses for the few links that needed it.

The same thing happens with href="" and ?page=2: both point at the base, not at the current page. Linking to an anchor on the same page covers ids and jump links in general.

One case uses this behaviour on purpose. A page shown with srcdoc takes its base address from the parent page, so its # links resolve against the parent page. Adding <base href="about:srcdoc"> makes them resolve inside the frame again.

<base target="viewer"> makes every link without its own target open in the frame or window named viewer. A target on the link itself always wins.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Every link without its own target opens in the frame named "viewer" -->
<base target="viewer">
<title>base target</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  nav { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
  nav a { font-size: 14px; padding: 7px 11px; border-radius: 8px; background: #fff; border: 1px solid #c9cdd4; color: #1d4ed8; text-decoration: none; }
  nav a.own { border-color: #e0a36a; color: #9a3412; }
  .panes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
  .pane h3 { margin: 0 0 4px; font-size: 13px; }
  iframe { width: 100%; height: 230px; box-sizing: border-box; border: 1px solid #c9cdd4; border-radius: 8px; background: #fff; }
  @media (max-width: 420px) {
    .panes { grid-template-columns: 1fr; }
    iframe { height: 110px; }
  }
</style>
</head>
<body>
<nav>
  <a href="data:text/html,<h2>Pricing</h2><p>Opened in the viewer.</p>">Pricing</a>
  <a href="data:text/html,<h2>Features</h2><p>Opened in the viewer.</p>">Features</a>
  <a href="data:text/html,<h2>Contact</h2><p>Opened in the viewer.</p>">Contact</a>
  <a class="own" href="data:text/html,<h2>Notes</h2><p>This link has target=side.</p>" target="side">Notes (own target)</a>
</nav>
<div class="panes">
  <div class="pane"><h3>name="viewer" (from &lt;base&gt;)</h3><iframe name="viewer" title="viewer"></iframe></div>
  <div class="pane"><h3>name="side" (link's own target)</h3><iframe name="side" title="side"></iframe></div>
</div>
</body>
</html>
The first three links have no target and open in the viewer. The fourth has target="side" and goes there instead.

This is handy for a menu that loads pages into one frame.

The common use, <base target="_blank">, has a side effect: it also applies to in-page jump links. A #faq link then opens the same page in a new tab rather than scrolling. Give those links target="_self".

Opening links in a new tab shows a narrower way to target only external links.

A finished example: check a page before adding a base

The checker below reads a page's HTML the way a browser would. It uses the first base with an href, flags any extra ones, warns when the base does not end in /, and marks every # link that would leave the page.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Base tag link checker</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  label { display: block; font-size: 13px; font-weight: 600; margin: 0 0 4px; }
  input, textarea { width: 100%; box-sizing: border-box; font: 12.5px/1.45 ui-monospace, Consolas, monospace; padding: 8px; border: 1px solid #c9cdd4; border-radius: 8px; }
  textarea { height: 150px; resize: vertical; }
  button { margin: 10px 0; font: 600 14px system-ui, sans-serif; padding: 8px 14px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
  .row { margin-bottom: 10px; }
  ul { list-style: none; margin: 0; padding: 0; font-size: 12.5px; }
  li { background: #fff; border-radius: 8px; padding: 7px 9px; margin-bottom: 6px; border-left: 4px solid #3aa55d; overflow-wrap: anywhere; }
  li.warn { border-left-color: #e0782f; background: #fff4ec; }
  li code { font: 12px ui-monospace, Consolas, monospace; }
  li small { display: block; color: #9a3412; margin-top: 2px; }
</style>
</head>
<body>
<div class="row">
  <label for="page">Page address</label>
  <input id="page" value="https://example.com/blog/post.html" spellcheck="false">
</div>
<label for="src">Page HTML</label>
<textarea id="src" spellcheck="false">&lt;head&gt;
  &lt;base href="https://example.com/assets"&gt;
  &lt;base href="https://example.com/old/"&gt;
&lt;/head&gt;
&lt;a href="#faq"&gt;FAQ&lt;/a&gt;
&lt;a href="/blog/post.html#faq"&gt;FAQ (fixed)&lt;/a&gt;
&lt;img src="logo.png"&gt;
&lt;a href="guide.html"&gt;Guide&lt;/a&gt;</textarea>
<button id="check">Check links</button>
<ul id="out"></ul>

<script>
  // Scratch document: a real <base> and <a> do the resolving
  const doc = document.implementation.createHTMLDocument('');
  const base = doc.head.appendChild(doc.createElement('base'));
  const a = doc.body.appendChild(doc.createElement('a'));
  function resolve(href, from) {
    base.setAttribute('href', from);
    a.setAttribute('href', href);
    return a.href;
  }

  const esc = (s) => s.replace(/[&<>"]/g, (c) => '&#' + c.charCodeAt(0) + ';');

  function check() {
    const page = document.getElementById('page').value.trim();
    const html = new DOMParser().parseFromString(document.getElementById('src').value, 'text/html');
    const bases = [...html.querySelectorAll('base[href]')];
    const items = [];
    let from = page;

    if (bases.length) {
      const href = bases[0].getAttribute('href');
      from = resolve(href, page);
      const path = new URL(from).pathname;
      items.push({ text: `Base: <code>${esc(from)}</code>` });
      if (!path.endsWith('/')) {
        items.push({ warn: 'The base does not end in /, so its last part is dropped.', text: `<code>${esc(href)}</code> acts like <code>${esc(resolve('./', from))}</code>` });
      }
      bases.slice(1).forEach((b) => items.push({ warn: 'Ignored: only the first base href counts.', text: `<code>${esc(b.getAttribute('href'))}</code>` }));
    }

    html.querySelectorAll('a[href], img[src], link[href], script[src], form[action]').forEach((el) => {
      const attr = el.hasAttribute('href') ? 'href' : el.hasAttribute('src') ? 'src' : 'action';
      const raw = el.getAttribute(attr);
      const to = resolve(raw, from);
      let warn = '';
      if (raw.startsWith('#') && to.split('#')[0] !== page.split('#')[0]) {
        // Suggest the page's own address plus the #id (path only if the base is on the same site)
        const own = new URL(from).origin === new URL(page).origin ? new URL(page).pathname : page.split('#')[0];
        warn = `Leaves this page. Write it as ${esc(own + raw)}`;
      }
      items.push({ warn, text: `<code>${esc(raw)}</code> → <code>${esc(to)}</code>` });
    });

    document.getElementById('out').innerHTML = items.map((i) =>
      `<li class="${i.warn ? 'warn' : ''}">${i.text}${i.warn ? `<small>${i.warn}</small>` : ''}</li>`).join('');
  }

  document.getElementById('check').addEventListener('click', check);
  check();
</script>
</body>
</html>
Paste your own head and links. Each address is resolved by a real base element, and problems are marked in orange.

The resolving trick is worth copying. A scratch document from document.implementation.createHTMLDocument() holds one <base> and one <a>. Set both attributes and read a.href, and the browser does the work:

const doc = document.implementation.createHTMLDocument('');
const base = doc.head.appendChild(doc.createElement('base'));
const a = doc.body.appendChild(doc.createElement('a'));

function resolve(href, from) {
  base.setAttribute('href', from);
  a.setAttribute('href', href);
  return a.href;
}

In a script on a normal page, document.baseURI gives the current base. Pass it to new URL(path, document.baseURI), because new URL(path) alone does not use the base and throws on a relative path.

Base tag and security

Because one tag redirects every relative script and stylesheet, an injected <base> is an attack path. A Content Security Policy with base-uri 'self' or base-uri 'none' limits which base addresses the page accepts.

When it does not work

What you see Cause Fix
A #section link loads another page #id resolves against the base Write the page's own address before the #
Links land one folder too high The base href has no trailing / End the folder with /
The base seems to be ignored Another <base href> comes first Keep a single base, first in the head
Images and CSS vanish after adding a base Their files are not at the base address Move them there, or use full addresses
A link starting with / skips the base folder Root-relative paths keep only the base's domain Drop the leading /
In-page links open new tabs <base target="_blank"> applies to them too Add target="_self" to those links
new URL('x') throws in a script It does not read the base Use new URL('x', document.baseURI)
A form posts to the wrong place Its relative action resolves against the base Give the form a full or root-relative action

Base tag behaviour is easier to show than to explain. The people you send a page to can click the links and see where they go, which a screenshot cannot do.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the examples above work for whoever opens the link. If you change the code later, the same link shows the new version.

Questions people ask

What does the base tag do in HTML?

It sets the starting address for every relative URL in the document (href) and the default place links open (target). It goes in the head, has no closing tag, and applies to links, images, stylesheets, scripts and forms alike.

Can a page have more than one base tag?

The HTML standard allows one. If there are several, the browser uses the first base with an href for addresses and the first base with a target for targets. The others do nothing.

Why do my #anchor links load a different page after adding a base tag?

An address like #faq is relative, so it is resolved against the base address, not the current page. Unless the base points at this exact page, the result is another page plus #faq. Write the page's own path before the #, for example /blog/post.html#faq.

Does the base tag affect JavaScript?

Partly. Relative addresses passed to fetch(), and read from properties such as a.href or img.src, resolve against the base. new URL('x') with no second argument does not use it and throws; pass document.baseURI as the second argument.

Does the base tag affect url() in CSS?

In a style block or style attribute on the page, yes: url(img/bg.png) resolves against the base. In an external stylesheet, url() resolves against the stylesheet's own address instead.

Keep reading