HTML Guides for style
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 <style> element in the <body> comes after other content inside its parent, but the HTML standard only allows it there as the parent's first child.
Since October 2026, a <style> element can sit in the <body> as long as its styles can't reach anything earlier in the page. Its style rules go inside an @scope block, which limits them to the parent of the <style> and everything inside it. Putting the <style> first in that parent means the browser has the styles before it renders any of the content they apply to.
Whitespace and comments before the <style> don't count, so indenting it or adding a comment above it is fine. Any element or text before it triggers the error, including a heading, a <link>, a <script> or another <style>. A <style> at the end of a container, or at the end of the <body>, breaks the rule as well. When two <style> elements sit next to each other, the second one is reported, so merge their rules into a single element.
To fix the error, move the <style> element up so it's the first thing inside its parent. An @scope block without a selector still applies to the same parent, so the styles keep matching the same elements. If the parent has to start with something else, such as a heading, wrap the content the styles target in a new element and put the <style> at the top of it. The scope then becomes that wrapper. Styles meant for the whole page belong in the <head>.
Invalid example
<div class="card">
<p>Card content</p>
<style>
@scope {
p {
color: darkslategray;
}
}
</style>
</div>
Valid example
<div class="card">
<style>
@scope {
p {
color: darkslategray;
}
}
</style>
<p>Card content</p>
</div>
A <style> element in the <body> contains an @import rule, which the HTML standard doesn't allow there.
Since October 2026, a <style> element can sit in the <body> as long as its styles can't reach anything earlier in the page. Its style rules have to go inside an @scope block, and it can't use @import at all. The rules in an imported file aren't scoped, so they would apply to the whole page. Every @import in the element triggers the error, including ones with layer() or a media query, even when the rest of the style sheet is valid.
Moving the @import into the @scope block doesn't work either. CSS only accepts @import at the top of a style sheet, before other rules, so the validator reports a CSS parse error instead. Older validator builds worded this message differently and mentioned @scope, but the problem and the fix are the same.
To fix the error, load the file with a <link rel="stylesheet"> element. A stylesheet link is allowed in the <body>, but the linked rules still apply to the whole page, so the <head> is the more usual place for it. If the imported rules only target this part of the page, copy them into the @scope block of the <style> element instead. A <style> in the <body> also has to stay the first child of its parent.
Invalid example
<div class="card">
<style>
@import url("card.css");
@scope {
p {
color: darkslategray;
}
}
</style>
<p>Card content</p>
</div>
Valid example
<div class="card">
<style>
@scope {
p {
color: darkslategray;
}
}
</style>
<link rel="stylesheet" href="card.css">
<p>Card content</p>
</div>
Older builds of the validator reported at-rules like @keyframes and @font-face outside an @scope block in a <style> element in the <body>, but the HTML standard allows them there.
Since October 2026, a <style> element can sit in the <body> as long as its styles can't reach anything earlier in the page. Its style rules have to go inside an @scope block, and it can't use @import. An earlier draft of that change allowed only a few at-rules at the top level of the style sheet, such as @media, @supports and @layer. Builds of the Nu HTML Checker from late September and early October 2026 followed the draft and reported every other at-rule with this message.
The final rule lets at-rules that don't select elements stay outside @scope, at the top level or inside @media. That includes @keyframes, @font-face, @page and @counter-style. Current builds of the validator don't report them, so the code doesn't need to change: validate the page again and the error goes away. @import is still not allowed, and it now has a message of its own.
The names these at-rules define, such as an animation name or a font family, apply to the whole page. The HTML standard suggests using them only in the scoped rules of the same <style> element, and picking names that nothing else on the page uses. That way they can't restyle content the browser has already drawn.
Example
Older builds reported both at-rules in this <style> element. Current builds accept it, because the only style rule is inside @scope:
<div class="card">
<style>
@font-face {
font-family: "Card Sans";
src: url("card-sans.woff2") format("woff2");
}
@keyframes card-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@scope {
p {
font-family: "Card Sans", sans-serif;
animation: card-fade-in 0.3s ease-in;
}
}
</style>
<p>Card content</p>
</div>
The "Stray start tag style" error occurs when the HTML parser encounters a <style> element somewhere it doesn't belong according to the HTML specification. The most common causes are:
- A
<style>element placed after the closing</body>tag or after the closing</html>tag. - A
<style>element accidentally placed inside an element like<p>,<span>, or<a>that only accepts phrasing content and not<style>in that context. - A
<style>element appearing in the<body>without being a valid child in that context (though the HTML living standard does allow<style>in the<body>in certain conditions).
According to the WHATWG HTML living standard, the <style> element is primarily expected inside the <head> section. While the spec does technically allow <style> in the <body> where metadata content is expected (such as within a <noscript> element that is a child of <head>), placing it in the <head> is the most reliable and universally valid approach.
When a <style> tag appears after </body> or </html>, the browser's error-recovery behavior kicks in. Browsers will still try to apply the styles, but you're relying on undefined recovery behavior rather than the specification. This can lead to inconsistent rendering, makes the document harder to maintain, and signals structural problems in your HTML.
This issue commonly arises when content management systems, JavaScript frameworks, or copy-paste mistakes inject styles at the end of a document. It can also happen when template includes or server-side rendering place <style> blocks in unexpected locations.
To fix this issue, move the <style> element into the <head> section. Alternatively, consider moving the CSS into an external stylesheet linked via a <link> element, which is generally the preferred approach for maintainability and caching.
Examples
Incorrect: <style> after the closing </body> tag
This is the most common scenario that triggers the error. The <style> element is "stray" because it appears outside the <body> and <head>:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
</head>
<body>
<p>Hello world</p>
</body>
<style>
p {
color: green;
}
</style>
</html>
Incorrect: <style> after the closing </html> tag
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
</head>
<body>
<p>Hello world</p>
</body>
</html>
<style>
p {
color: green;
}
</style>
Incorrect: <style> nested inside a <p> element
The <p> element only accepts phrasing content, so a <style> element here may trigger the stray tag error:
<p>
<style>
.highlight { color: red; }
</style>
Some highlighted text.
</p>
Correct: <style> inside the <head>
Move all <style> elements into the <head> section:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
<style>
p {
color: green;
}
.highlight {
color: red;
}
</style>
</head>
<body>
<p>Hello world</p>
<p class="highlight">Important text.</p>
</body>
</html>
Correct: using an external stylesheet instead
For better separation of concerns, caching, and maintainability, use an external stylesheet:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<p>Hello world</p>
</body>
</html>
If you have multiple <style> blocks scattered throughout your document, consolidate them all into a single <style> element in the <head>, or better yet, move them into an external .css file. This ensures valid HTML, predictable rendering across browsers, and cleaner document structure.
A <style> element sits between the closing </head> tag and the opening <body> tag, where the HTML parser doesn't expect any elements.
After </head>, the parser expects the <body> start tag, with nothing but whitespace or comments before it. When it finds a <style> there, browsers put the element back into the <head> and still apply its CSS, so the page usually looks right. The markup is still invalid. A theme or plugin that prints inline CSS right after </head> is a common cause.
The validator treats this error as fatal. Right after it, the report shows "Cannot recover after last error. Any further errors will be ignored." and nothing later in the page gets checked. Other HTML problems below this point stay hidden until you fix it.
To fix it, move the <style> element inside the <head>, before </head>. Since October 2026, the HTML standard also allows a <style> element in the <body>, but only as the first child of its parent and with its rules wrapped in @scope, which limits them to that part of the page. For styles that apply to the whole page, the <head> is the right place.
Invalid example
<!DOCTYPE html>
<html lang="en">
<head>
<title>My page</title>
</head>
<style>
p {
color: navy;
}
</style>
<body>
<p>Hello, world!</p>
</body>
</html>
Valid example
<!DOCTYPE html>
<html lang="en">
<head>
<title>My page</title>
<style>
p {
color: navy;
}
</style>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
A <style> element in the <body> contains a CSS style rule that isn't inside an @scope block.
Since October 2026, the HTML standard allows <style> in the <body> as long as its styles can't reach anything earlier in the page. That means every style rule has to sit inside an @scope block with nothing between @scope and the opening brace. Without a selector, @scope applies to the parent element of the <style> and everything inside it, so p only matches the paragraphs in that parent. A top-level rule like .card p could restyle any paragraph on the page, including ones the browser has already drawn.
Style rules nested in @media, @supports or @container count too. Put those at-rules inside the @scope block, or put an @scope block inside them. At-rules that don't select elements, like @font-face and @keyframes, can stay at the top level. Two other limits apply to a <style> in the <body>: it can't contain @import, and it must be the first child of its parent.
To fix the error, wrap the rules in @scope { } and drop the part of each selector that pointed at the parent. Inside the block, :scope matches the parent element itself. Writing a selector after the keyword, as in @scope (.card), triggers a different error unless that rule is nested in another @scope. If the styles need to apply to the whole page, move the <style> element into the <head>.
Invalid example
<div class="card">
<style>
.card p {
color: darkslategray;
}
</style>
<p>Card content</p>
</div>
Valid example
<div class="card">
<style>
@scope {
:scope {
padding: 1rem;
}
p {
color: darkslategray;
}
}
</style>
<p>Card content</p>
</div>
In earlier versions of HTML (HTML 4 and XHTML), the type attribute was required on the <style> element to declare the MIME type of the styling language being used. The value was almost always text/css, as CSS has been the dominant stylesheet language for the web since its inception.
With HTML5, the specification changed. The type attribute on <style> now defaults to text/css, and since no browser supports any other styling language, the attribute serves no practical purpose. The WHATWG HTML Living Standard explicitly notes that the attribute is unnecessary and can be omitted. The W3C validator flags its presence as a warning to encourage cleaner, more modern markup.
Why This Matters
- Cleaner code: Removing unnecessary attributes reduces file size (even if marginally) and improves readability. Every attribute should earn its place in your markup.
- Standards compliance: Modern HTML encourages omitting default values when they add no information. Including
type="text/css"signals outdated coding practices. - Consistency: The same principle applies to
<script>elements, wheretype="text/javascript"is also unnecessary. Keeping your markup consistent by omitting both makes your codebase easier to maintain.
How to Fix It
The fix is straightforward: find every <style> element in your HTML that includes a type attribute and remove it. No other changes are needed — the browser behavior will be identical.
If you're working on a large codebase, a simple search for <style type= across your files will help you find all instances.
Examples
❌ Incorrect: Redundant type attribute
<style type="text/css">
p {
color: red;
}
</style>
<p>This text will be red.</p>
The type="text/css" attribute is unnecessary and triggers the W3C validator warning.
✅ Correct: type attribute omitted
<style>
p {
color: red;
}
</style>
<p>This text will be red.</p>
Without the type attribute, the browser still interprets the contents as CSS — the behavior is exactly the same.
❌ Incorrect: Other variations that also trigger the warning
The warning is triggered regardless of how the type value is formatted:
<style type="text/css" media="screen">
body {
font-family: sans-serif;
}
</style>
✅ Correct: Other attributes are fine, just remove type
<style media="screen">
body {
font-family: sans-serif;
}
</style>
Note that other valid attributes like media or nonce should be kept — only the type attribute needs to be removed.
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