HTML Guides for frameset
Learn how to identify and fix common HTML validation errors flagged by the W3C Validator — so your pages are standards-compliant and render correctly across every browser. Also check our Accessibility Guides.
A <body> start tag is stray when it appears where the parser can no longer open a body: after </body> or </html>, after a <frameset>, or inside a <template>.
A document has exactly one <body>. Once the parser has read </body> or </html>, a second <body> tag has nowhere to go. Browsers put the content that follows it back into the first body, and copy over any attributes the first <body> doesn't already have. That is why a second <body> after </body> also gets the "Start tag body seen but an element of the same type was already open" error. That error on its own means the extra <body> showed up while the first body was still open.
When the stray <body> comes after </html>, or carries an attribute the first <body> lacks (an onload handler, for example), the validator stops at that tag with "Cannot recover after last error". The rest of the page isn't checked until you remove it.
The extra tag usually comes from a template or include that wraps its output in its own <body>, such as a footer partial or a tracking snippet added after the layout has already closed the document. Move that content into the single <body>, before </body>, and drop the wrapper.
Old frameset pages trigger the error too. A <frameset> takes the place of the body, so the parser ignores a <body> that follows it, along with everything inside it. The <frameset> and <frame> elements are obsolete, so rebuild the page with one <body> and use <iframe> elements where the frames were. Inside a <template>, leave out the <body> tag and keep only its content.
Invalid example
<!DOCTYPE html>
<html lang="en">
<head>
<title>Opening hours</title>
</head>
<body>
<h1>Opening hours</h1>
<p>Monday to Friday, 9:00 to 17:00.</p>
</body>
<body>
<footer>
<p>Call us on 555-0100.</p>
</footer>
</body>
</html>
Valid example
<!DOCTYPE html>
<html lang="en">
<head>
<title>Opening hours</title>
</head>
<body>
<h1>Opening hours</h1>
<p>Monday to Friday, 9:00 to 17:00.</p>
<footer>
<p>Call us on 555-0100.</p>
</footer>
</body>
</html>
The <frame> element is obsolete, so the content each frame loads has to move into the page itself or into an <iframe> inside a normal <body>.
A <frame> only works inside a <frameset>, where it loads a separate HTML file into one pane of the browser window. The validator reports this message once for every <frame> on the page, and the <frameset> around them gets a message of its own. Moving the frames into a <body> doesn't help: the parser drops a <frame> there and reports it as a stray start tag. The frames and the frameset have to be replaced together, and when the frameset only wraps another site in a single frame, an HTTP redirect to that site replaces both.
What replaces each frame depends on what it holds. A header or menu that every page shares belongs in the page itself, written directly or pulled in with a server-side include or a template, so it lives at the same URL as the content around it. A document that has to stay separate can load in an <iframe>. Iframes take src and name just as frames did, so links with a matching target keep loading into them, and the validator stops warning that name is never allowed on <frame>. Give each iframe a title describing what it shows, since screen readers announce it.
The presentation attributes of a frame don't carry over. On an <iframe>, frameborder, scrolling, marginwidth and marginheight are obsolete too, and the validator warns about each one. Remove the border with border: 0 in CSS. Scrollbars and margins belong to the document loaded inside, so set overflow and margin in that document's own stylesheet. noresize needs no replacement, since an iframe has no draggable border between panes.
Invalid example
A header frame above a catalog frame:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Garden supplies</title>
</head>
<frameset rows="80,*">
<frame src="header.html" scrolling="no" noresize frameborder="0">
<frame src="catalog.html" frameborder="0">
</frameset>
</html>
Valid example
The header is part of the page (on a real site it would come from an include), and the catalog keeps loading from its own file in an iframe:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Garden supplies</title>
<style>
iframe {
width: 100%;
height: 80vh;
border: 0;
}
</style>
</head>
<body>
<header>
<a href="/">Garden supplies</a>
</header>
<iframe src="catalog.html" name="catalog" title="Product catalog"></iframe>
</body>
</html>
The <frameset> element is obsolete, so a page split into frames has to become a normal page with a <body>, using CSS for the layout and <iframe> or server-side includes for the shared parts.
A frameset document has a <frameset> where the <body> would go, and each <frame> inside it loads a separate HTML file into one pane of the window. The HTML standard lists <frameset>, <frame> and <noframes> as obsolete. Browsers still render framesets so old sites keep working, but the validator reports the <frameset> once and then each <frame> and <noframes> with a message of its own. The rows and cols attributes go away with the element and aren't reported separately.
Frames cause trouble beyond validation. The address bar keeps showing the frameset's URL whatever is loaded inside it, so a visitor can't bookmark or share a specific page, and search engines can send people straight to a framed page without the navigation that was meant to surround it.
The fix depends on what the frames were for. When they hold shared navigation, the usual answer is one complete page per URL, with the navigation pulled in on the server through an include, a template or a static site generator. When a pane really has to load other documents, an <iframe> inside a regular <body> does that job, sized with CSS and given a title so screen readers can announce it. A frameset with a single frame that fills the window, such as rows="100%,*", is often the "masked forwarding" a domain registrar sets up to show another site under a different domain. In that case, replace it with an HTTP 301 redirect to the real site.
Invalid example
A manual with a table of contents on the left and the chapter on the right:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Product manual</title>
</head>
<frameset cols="25%,75%">
<frame src="contents.html">
<frame src="chapter-1.html">
</frameset>
</html>
Valid example
The table of contents moves into the page, and an <iframe> loads each chapter that its links target:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Product manual</title>
<link rel="stylesheet" href="manual.css">
</head>
<body>
<nav>
<ul>
<li><a href="chapter-1.html" target="chapter">Chapter 1</a></li>
<li><a href="chapter-2.html" target="chapter">Chapter 2</a></li>
</ul>
</nav>
<iframe src="chapter-1.html" name="chapter" title="Chapter text"></iframe>
</body>
</html>
body {
display: flex;
height: 100vh;
margin: 0;
}
nav {
flex: 0 0 25%;
}
iframe {
flex: 1;
border: 0;
}
The iframe keeps the two-pane layout, but every chapter still shares one URL. Publishing each chapter as its own page, with the table of contents included on the server, gives every chapter an address people can bookmark and link to.
The <noframes> element is obsolete, so remove it and move any content worth keeping into the page's <body> as regular markup.
<noframes> held fallback content for browsers that couldn't display a <frameset>. Every browser in use today displays frames, and the rendering rules in the HTML standard hide <noframes> with display: none wherever it appears, so visitors never see what's inside it. The parser also reads its contents as raw text, the same way it reads a <style> element, so the validator doesn't check the markup inside it either.
The validator reports <noframes> wherever it sits: inside a <frameset>, after the closing </frameset>, in the <head> or in a regular <body>. In the <head> and after </frameset> it also adds an error saying the element isn't allowed there, and that error goes away with this one.
What to do depends on the page around it. In a frameset document, <noframes> goes away together with the <frameset> and its <frame> elements, which get obsolete messages of their own. The fallback often already holds a frameless version of the page, such as links to the documents the frames loaded, and that content can become the start of the new <body>. When <noframes> is left over in a page that already has a <body>, usually copied from an old template, delete it. Nothing inside it was ever displayed, so the page looks the same without it.
Don't move the fallback into an <iframe> instead. An iframe has to stay empty, and the validator reports any content placed inside it.
Invalid example
A two-frame page with a fallback for browsers without frame support:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Club newsletter</title>
</head>
<frameset cols="25%,75%">
<frame src="issues.html">
<frame src="latest.html">
<noframes>
<body>
<p>This site uses frames. Read the <a href="latest.html">latest issue</a> or browse the <a href="issues.html">past issues</a>.</p>
</body>
</noframes>
</frameset>
</html>
Valid example
The frameset becomes a <body>, the links from the fallback become the page's navigation, and an <iframe> loads the issue the second frame used to show:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Club newsletter</title>
</head>
<body>
<nav>
<a href="latest.html" target="issue">Latest issue</a>
<a href="issues.html" target="issue">Past issues</a>
</nav>
<iframe src="latest.html" name="issue" title="Newsletter issue"></iframe>
</body>
</html>
Validate at scale.
Ship accessible websites, faster.
Automated HTML & accessibility validation for large sites. Check thousands of pages against WCAG guidelines and W3C standards in minutes, not days.
1K Weekly
1,000 monthly checks. Cancel anytime.
Get 1K Weekly →Join teams across 40+ countries