The main tag in HTML: what goes inside, and where it goes

The main element wraps the content a page exists for. It looks like a plain block, but it becomes the main landmark, the one place a screen reader user can jump to straight away.

The <main> tag wraps the primary content of a page: the title and the text, form or results that this page exists for. Use it once per page. Leave the site header, menu, sidebar and footer outside it.

<body>
  <header>...</header>
  <nav>...</nav>
  <main id="content">
    <h1>Release notes</h1>
    ...
  </main>
  <aside>...</aside>
  <footer>...</footer>
</body>

On screen, main is a plain block. What it adds is meaning: the browser exposes it as the main landmark.

The example lists the landmarks of a small page. Swap main for a div, add a second one, or nest it, and watch the list and the warnings change.

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>main landmark checker</title>
<style>
  body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  @media (max-width: 560px) { .wrap { grid-template-columns: 1fr; } }
  #page { background: #fff; border-radius: 10px; padding: 10px; font-size: 13px; }
  #page header, #page nav, #page main, #page aside, #page footer, #page article, #page div {
    border: 1px dashed #c3c8d1; border-radius: 8px; padding: 6px 8px; margin: 6px 0;
  }
  #page main, #page [role="main"] { border: 2px solid #16a34a; background: #f1fbf4; }
  #page div#main:not([role]) { border-color: #ea580c; background: #fff7f1; }
  .t { font: 600 11px ui-monospace, Consolas, monospace; color: #6b7280; display: block; }
  .panel { background: #fff; border-radius: 10px; padding: 10px 12px; font-size: 13px; }
  .panel h3 { margin: 0 0 8px; font-size: 14px; }
  #list { margin: 0 0 8px; padding-left: 20px; line-height: 1.6; }
  .msg { border-radius: 6px; padding: 6px 8px; margin: 6px 0 0; }
  .warn { background: #fff4ec; color: #9a3412; }
  .ok { background: #ecf8f0; color: #0f5132; }
  label { display: block; margin: 5px 0; }
</style>
</head>
<body>
<div class="wrap">
  <div id="page"></div>
  <div class="panel">
    <h3>Landmarks on this page</h3>
    <ol id="list"></ol>
    <div id="msgs"></div>
    <label><input type="checkbox" id="useDiv"> Use &lt;div id="main"&gt; instead of &lt;main&gt;</label>
    <label><input type="checkbox" id="addRole"> Add role="main" to that div</label>
    <label><input type="checkbox" id="second"> Add a second &lt;main&gt;</label>
    <label><input type="checkbox" id="hide"> Give the second main the hidden attribute</label>
    <label><input type="checkbox" id="nest"> Put main inside an &lt;article&gt;</label>
  </div>
</div>

<script>
  const page = document.getElementById('page');
  const $ = (id) => document.getElementById(id);
  // implicit landmark role of each element (top-level header/footer only)
  const roles = { HEADER: 'banner', NAV: 'navigation', MAIN: 'main', ASIDE: 'complementary', FOOTER: 'contentinfo' };

  function build() {
    const tag = $('useDiv').checked ? 'div' : 'main';
    const attrs = tag === 'div' ? ' id="main"' + ($('addRole').checked ? ' role="main"' : '') : '';
    let main = '<' + tag + attrs + '><span class="t">&lt;' + tag + attrs.replace(/"/g, '&quot;') + '&gt;</span>Article text</' + tag + '>';
    if ($('nest').checked) main = '<article><span class="t">&lt;article&gt;</span>' + main + '</article>';
    let second = '';
    if ($('second').checked) {
      const h = $('hide').checked ? ' hidden' : '';
      second = '<main' + h + '><span class="t">&lt;main' + h + '&gt;</span>Another view</main>';
    }
    page.innerHTML =
      '<header><span class="t">&lt;header&gt;</span>Acme<nav><span class="t">&lt;nav&gt;</span>Home · Docs · Blog</nav></header>' +
      main + second +
      '<aside><span class="t">&lt;aside&gt;</span>Related links</aside>' +
      '<footer><span class="t">&lt;footer&gt;</span>© Acme</footer>';
  }

  function check() {
    // every element that is or may be a landmark, skipping hidden ones
    const els = [...page.querySelectorAll('header, nav, main, aside, footer, [role]')]
      .filter((el) => !el.closest('[hidden]'));
    $('list').innerHTML = '';
    els.forEach((el) => {
      const li = document.createElement('li');
      li.textContent = el.getAttribute('role') || roles[el.tagName];
      $('list').appendChild(li);
    });

    const out = [];
    const mains = els.filter((el) => el.tagName === 'MAIN' || el.getAttribute('role') === 'main');
    if (mains.length === 0) out.push(['warn', 'No main landmark. There is nothing to jump to.']);
    if (mains.length > 1) out.push(['warn', mains.length + ' visible main landmarks. Only one main may be shown at a time.']);
    page.querySelectorAll('main').forEach((m) => {
      const bad = m.parentElement.closest('article, aside, header, footer, nav, section');
      if (bad) out.push(['warn', 'main is inside <' + bad.tagName.toLowerCase() + '>. It must not be.']);
    });
    if (!out.length) out.push(['ok', 'One main landmark, in a valid place.']);
    $('msgs').innerHTML = '';
    out.forEach(([cls, text]) => {
      const p = document.createElement('p');
      p.className = 'msg ' + cls;
      p.textContent = text;
      $('msgs').appendChild(p);
    });
  }

  function update() {
    $('addRole').disabled = !$('useDiv').checked;
    $('hide').disabled = !$('second').checked;
    build();
    check();
  }
  document.querySelectorAll('input').forEach((i) => i.addEventListener('change', update));
  update();
</script>
</body>
</html>
A landmark list read with querySelectorAll. Each checkbox breaks or fixes one rule.

What goes inside main

Ask what changes from page to page. On a blog, the post changes and the header does not. On a search page, the results change and the search box in the header may not.

Repeated parts go around main. Each region maps to one landmark.
Repeated parts go around main. Each region maps to one landmark.
  • Inside: the page's <h1>, the article, the form, the results, the in-page table of contents.
  • Outside: the logo, the site search, the site menu, a sidebar of related links, the copyright footer.

A sidebar that belongs to this one article, such as a pull quote, may sit inside main as an <aside>. A sidebar repeated on every page belongs next to main.

One visible main per page

The HTML standard allows only one main element without the hidden attribute. Two visible mains give two main landmarks, and neither is clearly the content.

Single-page apps that keep several views in the document can still use a main per view. Hide every inactive one with the hidden attribute:

<main id="inbox">...</main>
<main id="settings" hidden>...</main>

Switch views by moving hidden, not by toggling a CSS class. display: none from a class hides the view, but only the attribute satisfies the rule.

Watch for one trap: a CSS rule such as main { display: flex } overrides hidden, and both views show at once.

Where main is allowed to sit

main belongs at the top of the structure. The standard only allows it inside html, body, div, a form without an accessible name, or a custom element.

So these are all wrong: main inside <article>, <section>, <aside>, <header>, <footer> or <nav>. The reverse is fine. An article or several section elements inside main is the usual shape of a content page.

A wrapper div around everything is fine too. Layout wrappers such as <div class="page"> do not break the rule, because div is on the allowed list.

main vs div id="main"

Many older layouts use <div id="main">. It looks identical, and it still works for CSS. What it lacks is a role.

Same CSS, same look. Only the main element puts a main landmark on the page.
Same CSS, same look. Only the main element puts a main landmark on the page.
<div id="main"> <main> <section>
Landmark None main Region, only when it has a name
How many per page Any One visible Any
Can sit inside article Yes No Yes

If you cannot change the markup, role="main" on the div gives the same landmark. When you can, switch the tag. The id can stay, and it is useful for the next part.

A skip link is the first link on the page, pointing at main. A keyboard user presses Tab once, sees it, presses Enter, and lands past the whole menu. The nav tag guide covers the header side of it.

The main side needs two attributes: an id that matches the link, and tabindex="-1".

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>Skip link to main</title>
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  /* off screen until it gets keyboard focus */
  .skip {
    position: absolute; left: 8px; top: -60px; z-index: 10;
    background: #1d2330; color: #fff; padding: 10px 14px; border-radius: 8px;
  }
  .skip:focus { top: 8px; }
  header { background: #fff; padding: 12px 14px; border-bottom: 1px solid #e1e4ea; }
  nav ul { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 14px; }
  nav a { color: #1d4ed8; }
  main { padding: 14px; }
  main:focus { outline: 3px solid #16a34a; outline-offset: -3px; }
  .status { margin: 0 14px 14px; padding: 8px 10px; border-radius: 8px; background: #fff; font-size: 14px; }
  .status b { font-family: ui-monospace, Consolas, monospace; }
  label { display: block; margin: 0 14px 8px; font-size: 14px; }
</style>
</head>
<body>
<a class="skip" href="#content">Skip to main content</a>
<header>
  <b>Acme Store</b>
  <nav aria-label="Main">
    <ul>
      <li><a href="#">Phones</a></li><li><a href="#">Laptops</a></li><li><a href="#">Tablets</a></li>
      <li><a href="#">Audio</a></li><li><a href="#">Cameras</a></li><li><a href="#">Gaming</a></li>
      <li><a href="#">Watches</a></li><li><a href="#">TV</a></li><li><a href="#">Deals</a></li>
      <li><a href="#">Help</a></li>
    </ul>
  </nav>
</header>

<main id="content" tabindex="-1">
  <h1 style="margin:0 0 6px;font-size:20px">Today's deals</h1>
  <p style="margin:0 0 8px">Press Tab once, then Enter. Then press Tab again.</p>
  <a href="#">First link inside main</a>
</main>

<label><input type="checkbox" id="noTab"> Remove tabindex="-1" from main</label>
<p class="status">Focus is on: <b id="where">body</b></p>

<script>
  const main = document.getElementById('content');
  const where = document.getElementById('where');

  // show which element holds focus right now
  function report() {
    const el = document.activeElement;
    where.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : '') +
      (el.tagName === 'A' ? ' "' + el.textContent + '"' : '');
  }
  document.addEventListener('focusin', report);
  document.addEventListener('focusout', () => setTimeout(report));

  document.getElementById('noTab').addEventListener('change', (e) => {
    if (e.target.checked) main.removeAttribute('tabindex');
    else main.setAttribute('tabindex', '-1');
  });
</script>
</body>
</html>
Tab once to reveal the skip link, press Enter, then Tab again. The status line shows where focus is.
  • With tabindex="-1", Enter moves focus onto main itself. The next Tab continues from there.
  • Without it, main cannot take focus. In Chromium, focus falls back to the body, though the next Tab still continues inside main. Tick the checkbox to see it.

The value -1 lets a link or a script focus main without adding a stop to the Tab order. tabindex explains the values.

On a short page, the footer rises to wherever the content ends, and the lower half of the window stays empty. The fix is three lines of flexbox.

Without flex: 1 each box is as tall as its content. With it, main takes the spare height.
Without flex: 1 each box is as tall as its content. With it, main takes the spare height.
body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
main { flex: 1; }

Body becomes a column at least one window tall. flex: 1 hands all the spare height to main, which pushes the footer down. On a long page there is no spare height, and the footer simply follows the content. Flexbox covers the property in full.

With a sidebar, wrap main and aside in a row and give the row flex: 1 instead. That is what the finished example does.

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>Page shell with a sticky footer</title>
<style>
  * { box-sizing: border-box; }
  body {
    margin: 0; font-family: system-ui, sans-serif; color: #1d2330; background: #f4f5f7;
    min-height: 100vh;       /* at least one screen tall */
    display: flex;
    flex-direction: column;  /* header, middle, footer stacked */
  }
  header { background: #1d2330; color: #fff; padding: 10px 16px; display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center; }
  header nav { display: flex; flex-wrap: wrap; gap: 14px; }
  header a { color: #cfe0ff; }
  .middle {
    flex: 1;                 /* takes all spare height, so the footer sits at the bottom */
    display: flex; gap: 16px; padding: 16px;
  }
  main { flex: 1; background: #fff; border-radius: 10px; padding: 14px 16px; }
  aside { width: 180px; background: #fff; border-radius: 10px; padding: 14px 16px; font-size: 14px; }
  footer { background: #e6e9ef; padding: 10px 16px; font-size: 14px; }
  main h1 { margin: 0 0 8px; font-size: 20px; }
  .controls { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; margin-bottom: 8px; font-size: 14px; }
  /* the broken version, for comparison */
  body.broken .middle { flex: none; }
  /* phones: sidebar drops under the main content */
  @media (max-width: 600px) {
    .middle { flex-direction: column; padding: 10px; }
    aside { width: auto; }
  }
</style>
</head>
<body>
<header>
  <b>Acme</b>
  <nav aria-label="Main"><a href="#">Home</a><a href="#">Docs</a><a href="#">Blog</a></nav>
</header>

<div class="middle">
  <main>
    <h1>Release notes</h1>
    <div class="controls">
      <button id="more" type="button">Add a paragraph</button>
      <button id="less" type="button">Remove one</button>
      <label><input type="checkbox" id="broken"> Remove flex: 1</label>
    </div>
    <div id="text"><p>Version 2.4 is out.</p></div>
  </main>
  <aside>
    <b>On this page</b>
    <p>Related releases and links.</p>
  </aside>
</div>

<footer>© Acme · Terms · Privacy</footer>

<script>
  const text = document.getElementById('text');
  document.getElementById('more').addEventListener('click', () => {
    const p = document.createElement('p');
    p.textContent = 'Paragraph ' + (text.children.length + 1) + '. More notes about this release.';
    text.appendChild(p);
  });
  document.getElementById('less').addEventListener('click', () => {
    if (text.children.length > 1) text.lastElementChild.remove();
  });
  document.getElementById('broken').addEventListener('change', (e) => {
    document.body.classList.toggle('broken', e.target.checked);
  });
</script>
</body>
</html>
header, nav, main, aside and footer. Add or remove paragraphs; the footer stays down. On a phone the sidebar drops under main.

When it does not work

What you see Cause Fix
Two main landmarks are listed Two main elements without hidden Keep one visible, add hidden to the rest
A validator flags main main is inside article, section or header Move it up to body or a plain div
No main landmark at all Layout uses <div id="main"> Change the tag to main, or add role="main"
Footer floats in the middle of a short page Nothing takes the spare height Flex column on body, flex: 1 on main
The skip link scrolls, but focus stays behind main is not focusable Add tabindex="-1" to main
A hidden view still shows CSS display on main overrides hidden Add main[hidden] { display: none; }

Landmarks and focus are hard to show in a screenshot, and an .html attachment may open as plain code on a phone. A working page lets people press Tab and see for themselves.

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 people you send it to can try the skip link and resize the layout. If you change the code later, the same link shows the new version.

Questions people ask

Can a page have two main elements?

Only one may be visible. The HTML standard says a document must not have more than one main element without the hidden attribute. A single-page app that swaps views can keep several main elements, as long as all but one carry hidden.

What is the difference between main and div id="main"?

They look the same. A div has no role, so an id of "main" is only a name for CSS and links. The main element maps to the main landmark role, so assistive technology can list it and jump to it. role="main" on the div gives the same landmark, with an extra attribute.

What is the difference between main and section?

main appears once and holds the whole primary content of the page. section groups one themed part of the content, usually with its own heading, and a page can have many. Sections often sit inside main; main never sits inside a section.

Can main go inside header, article or aside?

No. The HTML standard only allows main inside html, body, div, a form without an accessible name, or a custom element. Put main next to header, nav, aside and footer, not inside them.

Does main change how the page looks?

No. Browsers display it as a block, like a div, with no margin, border or font change. Everything you see comes from your own CSS.

Keep reading