Build a footer with HTML and CSS

A good footer is one footer element as a child of body, a grid of link columns that stacks on a phone, and text that stays readable on a dark background.

A page footer is a <footer> element placed as a child of <body>, after <main>.

In that position it is the page's contentinfo landmark, the part screen reader users can jump to for contact details, links and the copyright line. The same tag inside an <article> is just that article's footer.

Try it first. Move the page footer around and watch which footers count as landmarks.

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>Which footer is a landmark?</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .controls { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 12px; font-size: 14px; }
  .controls label { display: flex; align-items: center; gap: 6px; }
  .wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  @media (max-width: 560px) { .wrap { grid-template-columns: 1fr; } }
  .box { background: #fff; border: 1px solid #dde1e7; border-radius: 10px; padding: 10px; }
  .box h2 { margin: 0 0 8px; font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: #5b6472; }
  /* outline every element of the sample page so you can see the nesting */
  #sample * { display: block; margin: 4px 0; padding: 4px 8px; border: 1px dashed #b8c0cc; border-radius: 6px; font-size: 13px; }
  #sample footer { border-color: #2563eb; background: #eff5ff; }
  #tree { list-style: none; margin: 0; padding: 0; font: 13px/1.5 ui-monospace, Consolas, monospace; }
  #tree li { padding: 3px 6px; border-radius: 5px; }
  .land { background: #e3f5e9; color: #0f5132; }
  .none { background: #f1f2f4; color: #5b6472; }
  #warn { margin: 8px 0 0; font-size: 13px; color: #9a3412; min-height: 1.4em; }
</style>
</head>
<body>
<div class="controls">
  <label>Page footer sits in
    <select id="where">
      <option value="body">body</option>
      <option value="div">a plain div</option>
      <option value="main">main</option>
      <option value="section">section</option>
    </select>
  </label>
  <label><input type="checkbox" id="second"> Add a second page footer</label>
</div>

<div class="wrap">
  <div class="box"><h2>Sample page</h2><div id="sample"></div></div>
  <div class="box"><h2>Roles (built from the rule)</h2><ul id="tree"></ul><p id="warn"></p></div>
</div>

<script>
  const sample = document.getElementById('sample');
  const where = document.getElementById('where');
  const second = document.getElementById('second');

  // The rule: header and footer are landmarks only when no
  // article, aside, main, nav or section sits between them and body.
  const SCOPES = 'article, aside, main, nav, section';
  function roleOf(el) {
    const tag = el.tagName.toLowerCase();
    if (tag === 'main') return 'main';
    if (tag === 'article') return 'article';
    if (tag === 'header' || tag === 'footer') {
      if (el.parentElement.closest(SCOPES)) return null;  // scoped: not a landmark
      return tag === 'header' ? 'banner' : 'contentinfo';
    }
    return undefined;  // not listed
  }

  function build() {
    const pageFooter = '<footer>page footer: © 2026 Acme</footer>';
    const extra = second.checked ? '<footer>second page footer</footer>' : '';
    const article = '<article>article<footer>article footer: by Ana, 3 May</footer></article>';
    let html;
    if (where.value === 'main') html = `<header>header</header><main>main${article}${pageFooter}</main>`;
    else if (where.value === 'section') html = `<header>header</header><main>main${article}</main><section>section${pageFooter}</section>`;
    else if (where.value === 'div') html = `<header>header</header><main>main${article}</main><div>div${pageFooter}</div>`;
    else html = `<header>header</header><main>main${article}</main>${pageFooter}`;
    sample.innerHTML = html + extra;

    const tree = document.getElementById('tree');
    tree.innerHTML = '';
    let count = 0;
    sample.querySelectorAll('*').forEach((el) => {
      const role = roleOf(el);
      if (role === undefined) return;
      const depth = (function d(n) { let k = 0; while ((n = n.parentElement) !== sample) k++; return k; })(el);
      const li = document.createElement('li');
      const tag = '<' + el.tagName.toLowerCase() + '>';
      li.textContent = '  '.repeat(depth) + tag + '  ' + (role ? role + (role === 'article' ? '' : ' landmark') : 'not a landmark');
      li.className = role && role !== 'article' ? 'land' : 'none';
      li.dataset.role = role || 'none';
      if (role === 'contentinfo') count++;
      tree.append(li);
    });
    document.getElementById('warn').textContent =
      count === 0 ? 'No contentinfo landmark: the page footer is inside a scoping element.'
      : count > 1 ? count + ' contentinfo landmarks. A page should have one.' : '';
  }

  where.addEventListener('change', build);
  second.addEventListener('change', build);
  build();
</script>
</body>
</html>
A sample page and the roles its header, main, article and footers get. Move the page footer, or add a second one.

The list on the right is built from one rule in the script, the same rule the HTML accessibility mapping uses. Scroll down for the columns, the layout and a finished footer.

Page footer vs article footer

<footer> holds information about its nearest section: who wrote it, when, related links, copyright. Which section that is depends on where the tag sits.

The same tag, two meanings. Only the footer with no scoping element above it is the page footer.
The same tag, two meanings. Only the footer with no scoping element above it is the page footer.

If there is an article, aside, main, nav or section between the footer and body, it belongs to that element. Otherwise it belongs to the page and gets the contentinfo role.

Where the footer sits Role Typical content
Child of body contentinfo landmark Site links, contact, copyright
Inside a plain div in body contentinfo landmark Same, a div does not scope it
Inside article No landmark Author, date, tags of that post
Inside section or aside No landmark Notes for that section
Inside main No landmark Nothing site-wide should go here

Use one page footer. Two footers at body level produce two contentinfo landmarks, and the demo above warns about it. The standard also says a footer may not contain another header or footer.

Pick the element that says what each part is. The browser and assistive technology then know without extra attributes.

The common parts of a site footer, and the element each one uses.
The common parts of a site footer, and the element each one uses.
<footer class="site-footer">
  <nav aria-label="Product">
    <h2>Product</h2>
    <ul><li><a href="/pricing">Pricing</a></li></ul>
  </nav>
  <address>
    <a href="mailto:hello@example.com">hello@example.com</a>
  </address>
  <small>© 2026 Acme Ltd.</small>
</footer>
  • Link columns: nav with an aria-label when a column repeats major navigation.
  • <address>: contact details of the page or article owner, not any address in the text.
  • <small>: small print such as copyright and legal lines. CSS still sets its size.

A short list of terms and privacy links can stay a plain list without nav. The nav tag explains when a nav is worth it.

One grid line makes four columns on a laptop and one on a phone, with no media query.

.cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: 18px 24px;
}

minmax(10rem, 1fr) says a column is at least 10rem wide. auto-fit makes as many columns as fit and wraps the rest to the next row. Drag the width slider and watch the count.

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>Footer columns that stack</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .controls { font-size: 14px; margin-bottom: 12px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
  .controls input { width: 180px; }

  .site-footer { background: #fff; border: 1px solid #dde1e7; border-radius: 10px; padding: 18px; }
  /* as many 10rem columns as fit; on a phone that is one */
  .cols {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
    gap: 18px 24px;
  }
  .cols h2 { font-size: 14px; margin: 0 0 6px; }
  .cols ul { list-style: none; margin: 0; padding: 0; }
  .cols li { margin: 4px 0; }
  .cols a { color: #1d4ed8; font-size: 14px; }

  form { margin-top: 18px; padding-top: 14px; border-top: 1px solid #e5e7eb; display: flex; flex-wrap: wrap; gap: 8px; align-items: end; }
  form label { display: grid; gap: 4px; font-size: 14px; flex: 1 1 14rem; }
  form input { font: inherit; padding: 8px 10px; border: 1px solid #b8c0cc; border-radius: 6px; }
  form button { font: inherit; padding: 8px 14px; border: 0; border-radius: 6px; background: #1d4ed8; color: #fff; cursor: pointer; }
  #sent { font-size: 13px; color: #0f5132; margin: 8px 0 0; min-height: 1.4em; }
  small { display: block; margin-top: 12px; color: #4b5563; }
</style>
</head>
<body>
<div class="controls">
  <label for="w">Footer width</label>
  <input type="range" id="w" min="260" max="900" value="900">
  <span id="info"></span>
</div>

<footer class="site-footer" id="footer">
  <div class="cols">
    <div><h2>Product</h2><ul><li><a href="#">Features</a></li><li><a href="#">Pricing</a></li><li><a href="#">Changelog</a></li></ul></div>
    <div><h2>Company</h2><ul><li><a href="#">About</a></li><li><a href="#">Jobs</a></li><li><a href="#">Press</a></li></ul></div>
    <div><h2>Help</h2><ul><li><a href="#">Docs</a></li><li><a href="#">Contact</a></li><li><a href="#">Status</a></li></ul></div>
    <div><h2>Legal</h2><ul><li><a href="#">Terms</a></li><li><a href="#">Privacy</a></li><li><a href="#">Cookies</a></li></ul></div>
  </div>

  <form id="news">
    <label>Monthly newsletter
      <input type="email" name="email" required placeholder="you@example.com" autocomplete="email">
    </label>
    <button>Subscribe</button>
  </form>
  <p id="sent" role="status"></p>

  <small>© 2026 Acme Ltd.</small>
</footer>

<script>
  const footer = document.getElementById('footer');
  const range = document.getElementById('w');
  const info = document.getElementById('info');

  // Count the columns the grid actually made
  function report() {
    const cols = getComputedStyle(footer.querySelector('.cols')).gridTemplateColumns.split(' ').length;
    info.textContent = footer.offsetWidth + 'px wide, ' + cols + (cols === 1 ? ' column' : ' columns');
  }
  range.addEventListener('input', () => { footer.style.maxWidth = range.value + 'px'; report(); });
  addEventListener('resize', report);
  report();

  // Demo only: show what would be sent instead of sending it
  document.getElementById('news').addEventListener('submit', (e) => {
    e.preventDefault();
    const data = new FormData(e.target);
    document.getElementById('sent').textContent = 'Would send: email=' + data.get('email');
    e.target.reset();
  });
</script>
</body>
</html>
Four link columns and a newsletter form. The slider narrows the footer; the form shows what it would send instead of sending.
Four columns side by side on a wide screen, one column on a phone, from the same CSS line.
Four columns side by side on a wide screen, one column on a phone, from the same CSS line.

The newsletter form is a real form with a label and type="email", so the browser checks the address before submit. The demo stops at preventDefault(). On a live site, point the form at your mailing service:

<form action="https://your-list-service.example/subscribe" method="post">

On a page with little content, the footer rises to wherever the content ends and leaves an empty band below it. Make body a flex column at least one window tall, and let main take the spare height:

body { min-height: 100vh; display: flex; flex-direction: column; }
main { flex: 1; }

On a long page there is no spare height, so the footer simply follows the content. The main tag guide walks through this layout with a sidebar.

If the footer must stay on screen while the page scrolls, that is a different layout, covered in fixed header and footer.

An icon-only link has no text, so a screen reader has nothing to announce, or reads the file name. Give the link a name and hide the drawing:

<a href="https://example.com/feed" aria-label="RSS feed">
  <svg viewBox="0 0 24 24" aria-hidden="true">...</svg>
</a>

aria-label covers the attribute in detail.

A back-to-top link is one line: <a href="#top">Back to top</a>.

The HTML standard treats the fragment #top as the top of the document even with no element of that id. For a button that appears after scrolling, see back to top button.

Dark footers are common, and their small grey text is often too faint. WCAG asks for a contrast ratio of at least 4.5:1 for normal-size text and 3:1 for large text.

Text Colour on #111827 Contrast Passes 4.5:1
Body text #d1d5db 12.0:1 Yes
Copyright in small #9ca3af 7.0:1 Yes
Grey often used #4b5563 2.4:1 No

Smaller type needs the same ratio, not a lower one. Keep link underlines, or another cue that is not colour alone, and give links a visible :focus-visible outline.

Everything above in one page: a sticky footer layout, contact details in address, three labelled navs, named social icons, the copyright in small and a back-to-top link.

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>A complete site footer</title>
<style>
  * { box-sizing: border-box; }
  /* sticky footer: body is a column at least one window tall, main takes the spare height */
  body { margin: 0; min-height: 100vh; display: flex; flex-direction: column; font-family: system-ui, sans-serif; color: #1d2330; background: #f4f5f7; }
  main { flex: 1; padding: 16px 20px; }
  header { padding: 12px 20px; background: #fff; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
  header b { font-size: 18px; }
  header button { font: inherit; font-size: 14px; padding: 6px 10px; border: 1px solid #b8c0cc; border-radius: 6px; background: #fff; cursor: pointer; }
  main p { max-width: 60ch; line-height: 1.55; }

  /* dark footer: every text colour below passes 4.5:1 on #111827 */
  .site-footer { background: #111827; color: #d1d5db; padding: 28px 20px 18px; font-size: 14px; }
  .site-footer a { color: #f3f4f6; }
  .site-footer a:focus-visible { outline: 2px solid #93c5fd; outline-offset: 2px; }
  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 22px 28px; }
  .brand { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 6px; }
  address { font-style: normal; line-height: 1.6; }
  .site-footer h2 { font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: #fff; margin: 0 0 8px; }
  .site-footer ul { list-style: none; margin: 0; padding: 0; }
  .site-footer li { margin: 6px 0; }
  .social { display: flex; gap: 10px; margin-top: 12px; }
  .social a { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; background: #1f2937; }
  .social svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
  .bottom { margin-top: 24px; padding-top: 14px; border-top: 1px solid #374151; display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; }
  .bottom small { color: #9ca3af; font-size: 13px; }
</style>
</head>
<body>
<header id="top">
  <b>Acme</b>
  <button id="len" aria-pressed="false">Make the page long</button>
</header>

<main id="content">
  <h1>Short page</h1>
  <p>This page has one paragraph, and the footer still sits at the bottom of the window.</p>
</main>

<footer class="site-footer">
  <div class="grid">
    <div>
      <p class="brand">Acme</p>
      <address>
        12 Harbour Road, Leeds<br>
        <a href="mailto:hello@example.com">hello@example.com</a><br>
        <a href="tel:+441130000000">+44 113 000 0000</a>
      </address>
      <div class="social">
        <!-- the name lives on the link; the drawing is hidden from screen readers -->
        <a href="#" aria-label="RSS feed"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></svg></a>
        <a href="#" aria-label="Email newsletter"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg></a>
        <a href="#" aria-label="Video channel"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="2" y="5" width="20" height="14" rx="3"/><path d="m10 9 5 3-5 3z"/></svg></a>
      </div>
    </div>
    <nav aria-label="Product">
      <h2>Product</h2>
      <ul><li><a href="#">Features</a></li><li><a href="#">Pricing</a></li><li><a href="#">Changelog</a></li></ul>
    </nav>
    <nav aria-label="Company">
      <h2>Company</h2>
      <ul><li><a href="#">About</a></li><li><a href="#">Jobs</a></li><li><a href="#">Press</a></li></ul>
    </nav>
    <nav aria-label="Help">
      <h2>Help</h2>
      <ul><li><a href="#">Docs</a></li><li><a href="#">Contact</a></li><li><a href="#">Status</a></li></ul>
    </nav>
  </div>

  <div class="bottom">
    <small>© <span id="year">2026</span> Acme Ltd. All rights reserved.</small>
    <a href="#top">Back to top ↑</a>
  </div>
</footer>

<script>
  document.getElementById('year').textContent = new Date().getFullYear();

  // Demo toggle: add paragraphs to see the footer follow long content
  const btn = document.getElementById('len');
  const main = document.getElementById('content');
  const short = main.innerHTML;
  btn.addEventListener('click', () => {
    const long = btn.getAttribute('aria-pressed') === 'false';
    btn.setAttribute('aria-pressed', long);
    btn.textContent = long ? 'Make the page short' : 'Make the page long';
    main.innerHTML = long
      ? '<h1>Long page</h1>' + '<p>A long page pushes the footer below the window. Scroll down, then use Back to top.</p>'.repeat(14)
      : short;
  });
</script>
</body>
</html>
Toggle the page length. Short: the footer sits at the bottom. Long: it follows the content, and Back to top returns you up.

When it does not work

What you see Cause Fix
Footer floats mid-page on short pages Nothing takes the spare height Flex column on body, flex: 1 on main
Two or more contentinfo landmarks Several footers at body level Keep one page footer, scope the rest in article or section
No contentinfo landmark at all Page footer is inside main or section Move it to be a child of body
Screen reader says "link" with no name Icon link without text aria-label on the link, aria-hidden on the SVG
Landmark list shows several unnamed navigations Footer navs without labels aria-label on each nav, or drop nav for short lists
Copyright line hard to read Grey small text below 4.5:1 Lighten the text or darken the background
Columns squeeze into slivers on a phone Fixed repeat(4, 1fr) repeat(auto-fit, minmax(10rem, 1fr))

A footer is easiest to review on the device where people will see it. A screenshot shows one width only, and a zipped template needs someone to open it locally.

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 reviewers can narrow the window, tab through the links and try the form. If you change the code later, the same link shows the new version.

Questions people ask

Can a page have more than one footer element?

Yes. Every article or section can have its own footer for its author, date or tags. Only one footer should sit outside article, aside, main, nav and section, because that one becomes the page's contentinfo landmark.

Does the footer have to be the last element on the page?

No. The HTML standard describes footer by what it contains, information about its section, not by position. In practice the page footer usually comes last, and a sticky footer layout keeps it at the bottom of short pages.

Should I add role="contentinfo" to the footer?

Not to a footer that is already a child of body, since it gets that role on its own. The attribute is only useful when the footer has to live inside a wrapper such as main, or when an old template uses a div.

Should the copyright line use the small element?

Yes. The HTML standard defines small for side comments such as small print, including copyright and legal restrictions. It does not change the meaning of the text, and you can still style its size with CSS.

Do footer links need to be inside a nav?

Not always. A short list such as terms, privacy and contact can be a plain list in the footer. Use nav when a column repeats major site navigation, and give each nav an aria-label so the landmarks can be told apart.

Keep reading