HTML Guides
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.
The <textarea> element represents a multi-line plain-text editing control, commonly used for comments, feedback forms, and other free-form text input. The rows attribute specifies the number of visible text lines the textarea should display. According to the HTML specification, when the rows attribute is present, its value must be a valid positive integer — meaning a string of one or more digits representing a number greater than zero (e.g., 1, 5, 20). An empty string ("") does not meet this requirement.
This issue typically occurs when a template engine, CMS, or JavaScript framework dynamically sets the rows attribute but outputs an empty value instead of a number, or when a developer adds the attribute as a placeholder intending to fill it in later.
Why this matters
- Standards compliance: The HTML specification explicitly requires
rowsto be a positive integer when present. An empty string violates this rule. - Unpredictable rendering: Browsers fall back to a default value (typically
2) when they encounter an invalidrowsvalue, but this behavior isn't guaranteed to be consistent across all browsers and versions. - Maintainability: Invalid attributes can mask bugs in dynamic code that was supposed to provide a real value.
How to fix it
You have two options:
- Set a valid positive integer: Replace the empty string with a number like
4,5, or whatever suits your design. - Remove the attribute: If you don't need to control the number of visible rows (or prefer to handle sizing with CSS), simply omit the
rowsattribute. The browser will use its default.
If the value is generated dynamically, ensure your code has a fallback so it never outputs an empty string. You can also control the textarea's height using CSS (height or min-height) instead of relying on the rows attribute.
Examples
❌ Invalid: empty string for rows
<textarea name="comments" rows="" cols="25">
</textarea>
This triggers the error because "" is not a valid positive integer.
✅ Fixed: providing a valid positive integer
<textarea name="comments" rows="5" cols="25">
</textarea>
Setting rows="5" tells the browser to display five visible lines of text.
✅ Fixed: removing the attribute entirely
<textarea name="comments" cols="25">
</textarea>
When rows is omitted, the browser uses its default (typically 2 rows). This is perfectly valid.
✅ Alternative: using CSS for sizing
<textarea name="comments" style="height: 10em; width: 25ch;">
</textarea>
If you need precise control over the textarea's dimensions, CSS properties like height, min-height, and width give you more flexibility than the rows and cols attributes. In this case, you can safely leave both attributes off.
The HTML living standard defines that scripts with type="module" are always fetched in parallel and evaluated after the document has been parsed, which is the same behavior that the defer attribute provides for classic scripts. Because this deferred execution is an inherent characteristic of module scripts, the spec explicitly forbids combining the two. Including both doesn't change how the browser handles the script, but it signals a misunderstanding of how modules work and produces invalid HTML.
This validation error commonly arises when developers migrate classic scripts to ES modules. A classic script like <script defer src="app.js"></script> relies on the defer attribute to avoid blocking the parser. When converting to a module by adding type="module", it's natural to leave defer in place — but it's no longer needed or allowed.
It's worth noting that the async attribute is valid on module scripts and does change their behavior. While defer is redundant because modules are already deferred, async overrides that default and causes the module to execute as soon as it and its dependencies have finished loading, rather than waiting for HTML parsing to complete.
How to Fix
Remove the defer attribute from any <script> element that has type="module". No other changes are needed — the loading and execution behavior will remain identical.
If you intentionally want the script to run as soon as possible (before parsing completes), use async instead of defer. But if you want the standard deferred behavior, simply omit both attributes and let the module default take effect.
Examples
❌ Incorrect: defer combined with type="module"
<script type="module" defer src="app.js"></script>
The defer attribute is redundant here and causes a validation error.
✅ Correct: module script without defer
<script type="module" src="app.js"></script>
Module scripts are deferred automatically, so this behaves exactly the same as the incorrect example above but is valid HTML.
✅ Correct: using async with a module (when needed)
<script type="module" async src="analytics.js"></script>
Unlike defer, the async attribute is permitted on module scripts. It causes the module to execute as soon as it's ready, without waiting for HTML parsing to finish.
✅ Correct: classic script with defer
<script defer src="app.js"></script>
For classic (non-module) scripts, the defer attribute is valid and necessary if you want deferred execution.
An img element without an alt attribute cannot carry any aria-* attributes except aria-hidden.
When an img lacks an alt attribute, its role and purpose are undefined from an accessibility standpoint. The HTML specification treats this as an image whose text alternative has not been provided, and assistive technologies handle it in a special fallback way. In this state, adding ARIA attributes like aria-label, aria-labelledby, or aria-describedby creates a contradiction: the markup simultaneously says "this image has no known alternative" and "here is semantic information about this image."
The only ARIA attribute allowed on an img without alt is aria-hidden="true", because hiding the image from the accessibility tree is consistent with having no text alternative.
There are two ways to fix this. Either add an alt attribute to the img (which you should almost always do), or remove the ARIA attributes and optionally add aria-hidden="true" if the image is purely decorative.
HTML examples
Invalid: img with no alt but with aria-label
<img src="photo.jpg" aria-label="A sunset over the ocean">
Fixed: add an alt attribute
If the image conveys meaning, provide an alt attribute. Once alt is present, ARIA attributes are allowed.
<img src="photo.jpg" alt="A sunset over the ocean">
Fixed: decorative image hidden from assistive technology
If the image is decorative and carries no meaning, use an empty alt or drop ARIA attributes in favor of aria-hidden:
<img src="decoration.jpg" alt="">
<img src="decoration.jpg" aria-hidden="true">
Hidden inputs are designed to carry data between the client and server without any user interaction or visual presence. The browser does not render them, screen readers do not announce them, and they are entirely excluded from the accessibility tree. Because aria-* attributes exist solely to convey information to assistive technologies, adding them to an element that assistive technologies cannot perceive is contradictory and meaningless.
The HTML specification explicitly prohibits aria-* attributes on input elements with type="hidden". This restriction exists because WAI-ARIA attributes — such as aria-label, aria-invalid, aria-describedby, aria-required, and all others in the aria-* family — are meant to enhance the accessible representation of interactive or visible elements. A hidden input has no such representation, so these attributes have nowhere to apply.
This issue commonly arises when:
- JavaScript frameworks or templating engines apply
aria-*attributes indiscriminately to all form inputs, regardless of type. - A developer changes an input's type from
"text"to"hidden"but forgets to remove the accessibility attributes that were relevant for the visible version. - Form libraries or validation plugins automatically inject attributes like
aria-invalidonto every input in a form.
To fix the issue, simply remove all aria-* attributes from any input element that has type="hidden". If the aria-* attribute was meaningful on a previously visible input, no replacement is needed — the hidden input doesn't participate in the user experience at all.
Examples
Incorrect: hidden input with aria-invalid
<form action="/submit" method="post">
<input type="hidden" name="referer" value="https://example.com" aria-invalid="false">
<button type="submit">Submit</button>
</form>
Correct: hidden input without aria-* attributes
<form action="/submit" method="post">
<input type="hidden" name="referer" value="https://example.com">
<button type="submit">Submit</button>
</form>
Incorrect: hidden input with multiple aria-* attributes
<form action="/save" method="post">
<input
type="hidden"
name="session_token"
value="abc123"
aria-label="Session token"
aria-required="true"
aria-describedby="token-help">
<button type="submit">Save</button>
</form>
Correct: all aria-* attributes removed
<form action="/save" method="post">
<input type="hidden" name="session_token" value="abc123">
<button type="submit">Save</button>
</form>
Correct: aria-* attributes on a visible input (where they belong)
If the input is meant to be visible and accessible, use an appropriate type value instead of "hidden":
<form action="/login" method="post">
<label for="username">Username</label>
<input
type="text"
id="username"
name="username"
aria-required="true"
aria-invalid="false"
aria-describedby="username-help">
<p id="username-help">Enter your registered email or username.</p>
<button type="submit">Log in</button>
</form>
According to the HTML specification, the content model for <ul> is strictly limited to zero or more <li> elements. Any text node placed directly inside the <ul> violates this rule, even if it seems harmless or invisible. Browsers may still render the page, but the resulting DOM structure is technically invalid and can lead to unpredictable behavior across different browsers and assistive technologies.
This matters for accessibility because screen readers rely on proper list structure to announce the number of items and allow users to navigate between them. Stray text nodes inside a <ul> can confuse these tools, causing list items to be miscounted or the text to be read in an unexpected context.
There are several common scenarios that trigger this error:
Loose text used as a list title. Developers sometimes place a heading or label directly inside the <ul> to describe the list. This text must be moved outside the list element.
Stray or other entities between list items. This often happens in templating systems or when code is concatenated, where characters or other text nodes end up between <li> elements. These should be removed entirely, since spacing between list items should be controlled with CSS.
Accidentally placing inline content without wrapping it in <li>. Sometimes content that should be a list item is simply missing its <li> wrapper.
Examples
❌ Text used as a list title inside <ul>
<ul>
Fruits
<li>Apple</li>
<li>Orange</li>
<li>Banana</li>
</ul>
The word "Fruits" is a text node directly inside the <ul>, which is not allowed.
✅ Move the title outside the list
<h3>Fruits</h3>
<ul>
<li>Apple</li>
<li>Orange</li>
<li>Banana</li>
</ul>
Using a heading before the list is semantically clear. You can also use a <p> or <span> if a heading isn't appropriate.
❌ entities between list items
<ul>
<li>First item</li>
<li>Second item</li>
<li>Third item</li>
</ul>
Each is a text node sitting directly inside the <ul>, triggering the error.
✅ Remove the entities and use CSS for spacing
<ul>
<li>First item</li>
<li>Second item</li>
<li>Third item</li>
</ul>
ul li {
margin-bottom: 0.5em;
}
Any visual spacing between list items should be handled with CSS margin or padding, not with HTML entities.
❌ Unwrapped content that should be a list item
<ul>
<li>Milk</li>
Eggs
<li>Bread</li>
</ul>
✅ Wrap the content in an <li> element
<ul>
<li>Milk</li>
<li>Eggs</li>
<li>Bread</li>
</ul>
The same rules apply to <ol> (ordered lists) and <menu> elements — their direct children must be <li> elements, and text nodes are not permitted. If your list is generated dynamically by a templating engine or JavaScript, check the output carefully for stray whitespace or text that may have been injected between list items.
The CSS fill property does not accept linear-gradient() as a valid value.
The fill property is used primarily with SVG elements to define the color that fills the interior of a shape. It accepts simple color values like named colors, hex codes, rgb(), hsl(), currentColor, none, or a reference to an SVG paint server using url().
CSS gradients such as linear-gradient() are classified as <image> values, not <color> values, so they cannot be used directly with fill. This is different from properties like background or background-image, which do accept gradient values.
If you need a gradient fill on an SVG element, you should define a <linearGradient> inside an SVG <defs> block and reference it with url().
How to fix it
Invalid: Using linear-gradient() with fill:
<svg width="100" height="100">
<rect width="100" height="100" style="fill: linear-gradient(to right, red, blue);" />
</svg>
Valid: Using an SVG <linearGradient> definition:
<svg width="100" height="100">
<defs>
<linearGradient id="myGradient" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="red" />
<stop offset="100%" stop-color="blue" />
</linearGradient>
</defs>
<rect width="100" height="100" fill="url(#myGradient)" />
</svg>
The x1, y1, x2, and y2 attributes on <linearGradient> control the direction of the gradient. In this example, x1="0" x2="1" creates a left-to-right gradient, equivalent to to right in CSS.
Direct text nodes inside select are not permitted content. Browsers typically ignore or mangle that text, leading to inconsistent rendering and confusing experiences for screen reader users. It also breaks conformance, which can impact maintainability and automated tooling. The right approach is to keep instructional text in a corresponding label, or provide a non-selectable prompt using a disabled, selected option. Group labels must be provided with optgroup elements, not free text.
To fix it, remove any raw text inside the select. If you need a prompt, add a first option with value="" and disabled selected hidden for a placeholder-like experience, or rely on a visible label. Ensure all selectable items are wrapped in option, and any grouping uses optgroup with its label attribute. Always associate the select with a label via for/id for accessibility.
Examples
Triggers the error (text node inside select)
<select>
Please select an option:
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
Correct: move instructions to a label
<label for="flavor">Please select a flavor:</label>
<select id="flavor" name="flavor">
<option value="vanilla">Vanilla</option>
<option value="chocolate">Chocolate</option>
</select>
Correct: provide a non-selectable prompt inside select
<label for="country">Country</label>
<select id="country" name="country" required>
<option value="" disabled selected hidden>Select a country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
Correct: use optgroup for grouping, not free text
<label for="city">City</label>
<select id="city" name="city">
<optgroup label="USA">
<option value="nyc">New York</option>
<option value="la">Los Angeles</option>
</optgroup>
<optgroup label="Canada">
<option value="toronto">Toronto</option>
<option value="vancouver">Vancouver</option>
</optgroup>
</select>
Correct: full document (for context)
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Select without stray text</title>
</head>
<body>
<form>
<label for="size">Choose a size:</label>
<select id="size" name="size">
<option value="" disabled selected hidden>Select a size</option>
<option value="s">Small</option>
<option value="m">Medium</option>
<option value="l">Large</option>
</select>
</form>
</body>
</html>
Tips:
- Put instructions in a
labelor surrounding text, not insideselect. - Every choice must be an
option; useoptgroupwithlabelto name groups. - For placeholders, prefer a disabled, selected first
option; avoid raw text nodes.
The background CSS property is a shorthand that can accept values for background-color, background-image, background-position, background-size, background-repeat, background-origin, background-clip, and background-attachment. When the validator encounters an unrecognized value, it tries to match it against individual sub-properties like background-color. If the value doesn't match any of them, you'll see this error.
Common causes include typos in color names (e.g., bleu instead of blue), malformed hex codes (e.g., #gggggg or a missing #), incorrect function syntax (e.g., rgb(255 0 0 with a missing parenthesis), or using values that simply don't exist in CSS. This error can also appear when a CSS custom property (variable) is used in inline styles and the validator can't resolve it, or when a browser-specific value is used that isn't part of the CSS specification.
Fixing this issue ensures your styles render predictably across browsers. While browsers are often forgiving and may ignore invalid declarations silently, relying on that behavior can lead to inconsistent rendering. Standards-compliant CSS is easier to maintain and debug.
How to Fix
- Check for typos in color names, hex codes, or function syntax.
- Verify the value format — hex colors need a
#prefix,rgb()andrgba()need proper comma-separated or space-separated values with closing parentheses. - Use
background-colorinstead of the shorthandbackgroundif you only intend to set a color. This makes your intent clearer and reduces the chance of conflicting shorthand values. - Remove vendor-prefixed or non-standard values that the validator doesn't recognize.
Examples
Incorrect — Typo in color name
<div style="background: aquaa;">Content</div>
aquaa is not a valid CSS color name, so the validator rejects it.
Correct — Valid color name
<div style="background: aqua;">Content</div>
Incorrect — Malformed hex code
<div style="background: #xyz123;">Content</div>
Hex color codes only allow characters 0–9 and a–f.
Correct — Valid hex code
<div style="background: #00a123;">Content</div>
Incorrect — Missing hash symbol
<div style="background: ff0000;">Content</div>
Without the #, the validator interprets ff0000 as an unknown keyword.
Correct — Hex code with hash
<div style="background: #ff0000;">Content</div>
Incorrect — Broken rgb() syntax
<div style="background: rgb(255, 0, 300);">Content</div>
RGB channel values must be between 0 and 255 (or 0% to 100%).
Correct — Valid rgb() value
<div style="background: rgb(255, 0, 128);">Content</div>
Correct — Using background-color for clarity
When you only need to set a color, prefer the specific background-color property over the shorthand:
<div style="background-color: rgba(255, 0, 0, 0.5);">Semi-transparent red</div>
Correct — Valid shorthand with image and other properties
<div style="background: url('image.jpg') no-repeat center / cover;">Content</div>
Note the / between background-position (center) and background-size (cover) — this is required syntax in the shorthand.
The padding-right property defines the space between an element's content and its right border. According to the CSS Box Model specification, padding represents internal space within an element, and conceptually, negative internal space doesn't make sense — you can't have less than zero space between content and its border. This rule applies equally to all padding properties: padding-top, padding-right, padding-bottom, padding-left, and the padding shorthand.
Browsers will typically ignore or discard a negative padding value, meaning your intended layout adjustment won't take effect. Beyond simply being invalid CSS, this can lead to inconsistent rendering across browsers and unexpected layout behavior. Relying on invalid values makes your stylesheets fragile and harder to maintain.
If your goal is to pull an element closer to its neighbor or create an overlapping effect, margin-right is the appropriate property to use. Unlike padding, margins are explicitly allowed to have negative values. Negative margins reduce the space between elements or even cause them to overlap, which is often the actual intent behind a negative padding attempt.
How to Fix
- Set the value to
0or a positive number. If you simply want no padding, use0. If you need some spacing, use a positive value. - Use
margin-rightfor negative spacing. If you need to reduce external space or create overlap, switch to a negative margin instead. - Re-evaluate your layout approach. In some cases, using
transform: translateX(), Flexboxgap, or Grid layout may achieve the desired result more cleanly than negative values on any property.
Examples
Incorrect: negative padding value
<style>
.sidebar {
padding-right: -10px;
}
</style>
<div class="sidebar">
<p>Sidebar content</p>
</div>
This triggers the validator error because -10px is not a valid value for padding-right.
Fixed: using zero or a positive value
<style>
.sidebar {
padding-right: 0;
}
</style>
<div class="sidebar">
<p>Sidebar content</p>
</div>
Fixed: using a negative margin instead
If the intent was to reduce external spacing on the right side, use margin-right:
<style>
.sidebar {
padding-right: 0;
margin-right: -10px;
}
</style>
<div class="sidebar">
<p>Sidebar content</p>
</div>
Fixed: using transform for visual offset
If the goal is to visually shift the element without affecting document flow, transform is another option:
<style>
.sidebar {
padding-right: 0;
transform: translateX(10px);
}
</style>
<div class="sidebar">
<p>Sidebar content</p>
</div>
Quick reference: padding vs. margin
| Property | Negative values allowed? | Purpose |
|---|---|---|
padding-right | No | Space between content and border |
margin-right | Yes | Space between the element's border and surrounding elements |
Choose the property that matches your layout intent, and remember that all four padding directions — padding-top, padding-right, padding-bottom, and padding-left — follow the same non-negative rule.
The <iframe> element is designed to embed another browsing context (essentially a nested document) within the current page. According to the HTML specification, the content model of <iframe> is nothing — it must not contain any text nodes or child elements. Any text placed directly inside the <iframe> tags is invalid HTML.
Historically, some older HTML versions allowed fallback text inside <iframe> for browsers that didn't support iframes. This is no longer the case in modern HTML. All current browsers support <iframe>, so there is no need for fallback content. The W3C validator flags this as an error because text or elements between the <iframe> tags violate the current HTML specification.
There are two valid ways to specify the content an iframe should display:
srcattribute — Provide a URL pointing to the document you want to embed.srcdocattribute — Provide inline HTML content directly as the attribute's value. The HTML is written as a string with appropriate character escaping (e.g.,<for<and"for"if using double-quoted attributes).
If you were using text inside <iframe> as a description or fallback, consider using the title attribute instead to provide an accessible label for the embedded content.
Examples
❌ Invalid: text inside the <iframe> element
<iframe>Some content here</iframe>
<iframe><p>This is my embedded content.</p></iframe>
Both of these will trigger the "Text not allowed in element iframe in this context" error.
✅ Valid: using the src attribute
<iframe src="https://example.com" title="Example website"></iframe>
This loads the document at the specified URL inside the iframe.
✅ Valid: using the srcdoc attribute
<iframe srcdoc="<p>This is my embedded content.</p>" title="Inline content"></iframe>
This renders the inline HTML provided in the srcdoc attribute. Note that HTML special characters must be escaped when the attribute value is enclosed in double quotes.
✅ Valid: self-closing style (empty element)
<iframe src="https://example.com" title="Example website"></iframe>
The <iframe> element requires a closing tag, but nothing should appear between the opening and closing tags. Keeping the element empty is the correct approach.
✅ Valid: adding an accessible label with title
If your original intent was to describe the iframe's content for users or assistive technologies, use the title attribute:
<iframe src="/embedded-report.html" title="Monthly sales report"></iframe>
The title attribute provides a label that screen readers can announce, improving the accessibility of the embedded content.
An invalid value was passed to the drop-shadow() function inside the CSS filter property.
The drop-shadow() function accepts a shadow definition similar to box-shadow, but with some restrictions. It takes two or three length values (offset-x, offset-y, and an optional blur-radius), plus an optional color. Unlike box-shadow, it does not accept a spread-radius value or the inset keyword.
The correct syntax is:
filter: drop-shadow(offset-x offset-y blur-radius color);
Each parameter has specific requirements:
offset-xandoffset-yare required length values (e.g.,2px,0.5em).blur-radiusis optional and must be a non-negative length. Negative values are not allowed.coloris optional. If omitted, the browser uses the element'scolorproperty value.
Common mistakes that trigger this error include adding a spread-radius (a fourth length value), using the inset keyword, or passing a malformed color value.
Examples
This is invalid because drop-shadow() does not support a spread-radius (the 5px fourth length value):
<div style="filter: drop-shadow(4px 4px 10px 5px red);">
Shadow content
</div>
Remove the spread-radius to fix it:
<div style="filter: drop-shadow(4px 4px 10px red);">
Shadow content
</div>
The box-shadow property applies one or more shadow effects to an element. Its syntax accepts several values in a flexible order:
box-shadow: <offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>? inset?;
The <color> component is optional — if omitted, it defaults to currentcolor. However, when a color is provided, it must be a valid CSS color value. The validator raises this error when it encounters something in the color position that doesn't match any recognized color format.
Common causes of this error include:
- Misspelled color names — e.g.,
greyyinstead ofgrey, orbalckinstead ofblack. - Invalid hex codes — e.g.,
#GGGor#12345(hex codes must be 3, 4, 6, or 8 hex digits). - Fabricated color names — e.g.,
bananaordarkwhite, which are not part of the CSS named colors specification. - Malformed function syntax — e.g.,
rgb(0,0,0,0.3)using the legacy comma syntax with four values instead ofrgba(), or missing parentheses. - Incorrect value order — placing values in an unexpected position can cause the validator to misinterpret a non-color value as a color attempt.
This matters for standards compliance because browsers may silently ignore an invalid box-shadow declaration entirely, meaning your intended shadow effect won't render. Using valid CSS ensures consistent behavior across browsers and passes validation checks.
Recognized CSS color formats
The following formats are valid for the color component:
- Named colors:
red,blue,transparent,currentcolor, etc. - Hex codes:
#000,#0000,#000000,#00000080 - RGB/RGBA:
rgb(0, 0, 0),rgba(0, 0, 0, 0.5), or the modernrgb(0 0 0 / 50%) - HSL/HSLA:
hsl(0, 0%, 0%),hsla(0, 0%, 0%, 0.5), orhsl(0 0% 0% / 50%)
Examples
Incorrect — misspelled or invalid color values
<!-- "balck" is not a valid color name -->
<div style="box-shadow: 2px 4px 8px balck;">Typo in color</div>
<!-- "banana" is not a recognized CSS color -->
<div style="box-shadow: 2px 4px 8px banana;">Invalid color name</div>
<!-- "#12345" is not a valid hex code (5 digits) -->
<div style="box-shadow: 2px 4px 8px #12345;">Bad hex code</div>
Correct — valid color values
<!-- Named color -->
<div style="box-shadow: 2px 4px 8px black;">Named color</div>
<!-- Hex color -->
<div style="box-shadow: 2px 4px 8px #333;">Hex color</div>
<!-- RGBA for semi-transparency -->
<div style="box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.3);">Semi-transparent</div>
<!-- HSL color -->
<div style="box-shadow: 2px 4px 8px hsl(210, 50%, 40%);">HSL color</div>
Correct — omitting the color entirely
If you want the shadow to inherit the element's text color, you can omit the color value altogether. This is valid and avoids the error:
<!-- Defaults to currentcolor -->
<div style="box-shadow: 2px 4px 8px;">Uses currentcolor</div>
Correct — multiple shadows with valid colors
<div style="box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2), inset 0 0 6px #ccc;">
Multiple shadows
</div>
To resolve this validation error, check the exact value the validator flags (the "X" in the error message), verify its spelling against the list of CSS named colors, and ensure any hex or function-based color uses correct syntax. If the color isn't needed, simply remove it and let the browser default to currentcolor.
Unicode allows some characters to be represented in multiple ways. For example, the accented letter "é" can be stored as a single precomposed character (U+00E9) or as two separate code points: the base letter "e" (U+0065) followed by a combining acute accent (U+0301). While these look identical when rendered, they are fundamentally different at the byte level. Unicode Normalization Form C (NFC) is the canonical form that prefers the single precomposed representation whenever one exists.
The HTML specification and the W3C Character Model for the World Wide Web require that all text in HTML documents be in NFC. This matters for several reasons:
- String matching and search: Non-NFC text can cause failures when browsers or scripts try to match strings, compare attribute values, or process CSS selectors. Two visually identical strings in different normalization forms won't match with simple byte comparison.
- Accessibility: Screen readers and assistive technologies may behave inconsistently when encountering decomposed character sequences.
- Interoperability: Different browsers, search engines, and tools may handle non-NFC text differently, leading to unpredictable behavior.
- Fragment identifiers and IDs: If an
idattribute contains non-NFC characters, fragment links (#id) may fail to work correctly.
This issue most commonly appears when text is copied from word processors, PDFs, or other applications that use decomposed Unicode forms (NFD), or when content is generated by software that doesn't normalize its output.
How to Fix It
- Identify the affected text: The validator will point to the specific line containing non-NFC characters. The characters will often look normal visually, so you'll need to inspect them at the code-point level.
- Convert to NFC: Use a text editor or command-line tool that supports Unicode normalization. Many programming languages provide built-in normalization functions.
- Prevent future issues: Configure your text editor or build pipeline to save files in NFC. When accepting user input, normalize it server-side before storing or embedding in HTML.
In Python, you can normalize a string:
import unicodedata
normalized = unicodedata.normalize('NFC', original_string)
In JavaScript (Node.js or browser):
const normalized = originalString.normalize('NFC');
On the command line (using uconv from ICU):
uconv -x NFC input.html > output.html
Examples
Incorrect (decomposed form — NFD)
In this example, the letter "é" is represented as two code points (e + combining acute accent), which triggers the validation warning. The source may look identical to the correct version, but the underlying bytes differ:
<!-- "é" here is stored as U+0065 U+0301 (decomposed) -->
<p>Résumé available upon request.</p>
Correct (precomposed form — NFC)
Here, the same text uses the single precomposed character é (U+00E9):
<!-- "é" here is stored as U+00E9 (precomposed) -->
<p>Résumé available upon request.</p>
Incorrect in attributes
Non-NFC text in attribute values also triggers this issue:
<!-- The id contains a decomposed character -->
<h2 id="resumé">Résumé</h2>
Correct in attributes
<!-- The id uses the precomposed NFC character -->
<h2 id="resumé">Résumé</h2>
While these examples look the same in rendered text, the difference is in how the characters are encoded. To verify your text is in NFC, you can paste it into a Unicode inspector tool or use the normalization functions mentioned above. For further reading, the W3C provides an excellent guide on Normalization in HTML and CSS.
A CSS color value (like a hex code or color name) was used where a background-image value is expected, or vice versa — the background-image property only accepts image functions such as url() or gradient functions, not plain color values.
The background-image property is specifically designed for setting images or gradients as backgrounds. If you want to set a solid background color, use the background-color property instead. Alternatively, you can use the shorthand background property, which accepts both colors and images.
This error often occurs when using the background shorthand incorrectly or when accidentally assigning a color value directly to background-image in inline styles.
Incorrect Example
<div style="background-image: #ff0000;">
This will trigger a validation error.
</div>
Correct Examples
Use background-color for solid colors:
<div style="background-color: #ff0000;">
Using background-color for a solid color.
</div>
Use background-image only for images or gradients:
<div style="background-image: url('banner.jpg');">
Using background-image with a URL.
</div>
<div style="background-image: linear-gradient(to right, #ff0000, #0000ff);">
Using background-image with a gradient.
</div>
Or use the background shorthand, which accepts both:
<div style="background: #ff0000 url('banner.jpg') no-repeat center;">
Using the background shorthand.
</div>
The CSS font shorthand property has a specific syntax defined in the CSS specification. At minimum, it requires both a font-size and a font-family value. Optionally, you can prepend font-style, font-variant, and font-weight, and you can append a line-height value after the font-size using a slash separator. The full syntax looks like this:
font: [font-style] [font-variant] [font-weight] font-size[/line-height] font-family;
When the W3C validator reports that a value "is not a font-family value," it means the parser reached a point in the font declaration where it expected to find a font family name but instead found something it couldn't interpret as one. This often happens in two scenarios:
- Using
fontwhen you meant a specific property — For example, writingfont: 300when you only intended to set the font weight. The validator tries to parse300as a completefontvalue, and since there's nofont-sizeorfont-family, it fails. - Incomplete
fontshorthand — Providing some values but forgetting the mandatoryfont-familyat the end, such asfont: 300 16pxwithout a family name.
This matters because browsers may ignore an invalid font declaration entirely, causing your text to render with default or inherited styles instead of what you intended. Keeping your CSS valid also ensures consistent behavior across different browsers and helps maintain clean, predictable stylesheets.
How to fix it:
- If you only need to set a single font-related property, use the specific property (
font-weight,font-size,font-style,font-variant, orfont-family) instead of thefontshorthand. - If you want to use the
fontshorthand, make sure you include at leastfont-sizeandfont-family, and that all values appear in the correct order. - Remember that the
fontshorthand resets any omitted font sub-properties to their initial values, so use it deliberately.
Examples
Incorrect: Using font to set only the weight
<p style="font: 300;">This text has an invalid font declaration.</p>
The validator reports that 300 is not a valid font-family value because the font shorthand expects at least a font-size and font-family.
Correct: Using font-weight directly
<p style="font-weight: 300;">This text has a light font weight.</p>
Incorrect: Missing font-family in the shorthand
<p style="font: italic 300 16px;">This is also invalid.</p>
Even though font-style, font-weight, and font-size are all present, the required font-family is missing.
Correct: Complete font shorthand
<p style="font: italic 300 16px/1.5 'Helvetica', sans-serif;">This is valid.</p>
This includes all components in the correct order: font-style, font-weight, font-size/line-height, and font-family.
Correct: Minimal valid font shorthand
<p style="font: 16px sans-serif;">Only size and family — the minimum required.</p>
Correct: Using individual properties instead of the shorthand
<p style="font-style: italic; font-weight: 300; font-size: 16px; line-height: 1.5; font-family: 'Helvetica', sans-serif;">
Each property set individually.
</p>
Using individual properties avoids the pitfalls of the shorthand and gives you explicit control without accidentally resetting other font sub-properties.
Negative values for the CSS line-height property are invalid and browsers reject them.
The line-height property sets the height of each line box, so a negative value has no meaning and violates the CSS specification. Valid values are normal, a unitless multiplier like 1.5, a length like 24px or 1.5em, or a percentage like 150%. A unitless number is usually the best choice, since it scales with the element's font size and avoids the inheritance quirks that fixed lengths can cause.
This error usually appears when line-height is set inline through the style attribute, where the W3C validator catches it during HTML validation. A common cause is a calculation in a template or script that produces a negative number and writes it into the markup.
To fix it, replace the negative value with a positive one, or use normal for the browser's default spacing. Tighter lines are fine as long as the value stays positive, such as line-height: 0.9.
HTML examples
Invalid: negative line-height value
<p style="line-height: -0.5em;">
Paragraph text
</p>
Valid: positive value or normal
<!-- Unitless multiplier, scales with font size -->
<p style="line-height: 1.5;">
Paragraph text
</p>
<!-- Browser default spacing -->
<p style="line-height: normal;">
Paragraph text
</p>
The role attribute is not allowed on a label element when that label is associated with a form control (a labelable element) through the for attribute or by nesting.
When a label is associated with a form control, the browser already understands its purpose — it's a label. Adding a role attribute overrides this native semantics, which is redundant at best and confusing for assistive technologies at worst.
A label becomes "associated" with a labelable element in two ways: explicitly via the for attribute pointing to the control's id, or implicitly by wrapping the control inside the label. Labelable elements include input (except type="hidden"), select, textarea, button, meter, output, and progress.
If the label is associated, simply remove the role attribute. The native semantics are already correct and sufficient.
If you truly need a custom role for some reason and the label is not functionally labeling a control, you can disassociate it by removing the for attribute or unnesting the control — but this is rarely the right approach.
Invalid Example
<label for="email" role="presentation">Email</label>
<input type="email" id="email">
Valid Example
<label for="email">Email</label>
<input type="email" id="email">
When the HTML parser encounters a < character inside an opening tag, it doesn't treat it as the start of a new tag — instead, it tries to interpret it as an attribute name. Since < is not a valid attribute name, the W3C validator raises this error. The browser may still render the page, but the behavior is undefined and can vary across different browsers, potentially leading to broken markup or elements that don't display correctly.
This issue most commonly occurs in a few scenarios:
- Accidental keystrokes — a stray
<typed while editing attributes. - Copy-paste artifacts — fragments of other tags getting pasted into the middle of an element.
- Misplaced angle brackets — attempting to nest or close tags incorrectly, such as adding
<before/>in a self-closing tag. - Template or code generation errors — dynamic HTML output that incorrectly injects
<into attribute positions.
Because this is a syntax-level problem, it can cause cascading parse errors. The parser may misinterpret everything after the stray < until it finds a matching >, which can swallow subsequent elements or attributes and produce unexpected rendering results.
How to Fix It
- Open the file referenced by the validator error and go to the indicated line number.
- Look inside the opening tag of the flagged element for a
<character that doesn't belong. - Remove the stray
<character. - If the
<was meant to represent a literal less-than sign in an attribute value, replace it with the HTML entity<.
Examples
Stray < before the closing slash
<!-- ❌ Stray "<" before the self-closing slash -->
<img src="photo.jpg" alt="smiling cat" < />
<!-- ✅ Fixed: removed the stray "<" -->
<img src="photo.jpg" alt="smiling cat" />
Stray < between attributes
<!-- ❌ Accidental "<" between attributes -->
<a href="/about" < class="nav-link">About</a>
<!-- ✅ Fixed: removed the stray "<" -->
<a href="/about" class="nav-link">About</a>
Fragment of another tag pasted inside an element
<!-- ❌ Leftover "<span" pasted inside the div's opening tag -->
<div class="card" <span>
<p>Hello world</p>
</div>
<!-- ✅ Fixed: removed the pasted fragment -->
<div class="card">
<p>Hello world</p>
</div>
Literal < intended in an attribute value
If you actually need a less-than sign inside an attribute value — for example, in a title or data-* attribute — use the < entity instead of a raw <.
<!-- ❌ Raw "<" in an attribute value can cause parsing issues -->
<span title="x < 10">Threshold</span>
<!-- ✅ Fixed: use the HTML entity -->
<span title="x < 10">Threshold</span>
The stroke-width property controls the thickness of the outline (stroke) drawn around shapes and text, primarily used in SVG but also applicable to HTML elements via CSS. According to both the SVG specification and the CSS standard, stroke-width accepts only non-negative values — that is, zero or any positive number, optionally with a CSS length unit like px, em, or rem. A unitless number is also valid and is interpreted in the current coordinate system's user units.
Negative values are logically meaningless for stroke width because you cannot draw an outline with negative thickness. Browsers will typically ignore or discard the invalid declaration, meaning the stroke may render with an unexpected default width or not at all. Beyond rendering issues, using invalid CSS values causes W3C validation errors, which can indicate broader quality problems in your code and may lead to unpredictable behavior across different browsers.
A common cause of this error is dynamic value generation — for example, a CSS calc() expression or a preprocessor variable that inadvertently produces a negative result. If your stroke width is computed, make sure to clamp the value so it never goes below 0.
How to fix it
- Replace negative values with
0or a positive number. If you intended no visible stroke, use0. If you wanted a visible stroke, use the appropriate positive thickness. - Guard computed values. If the value comes from a
calc()expression or CSS custom property, usemax()to ensure the result is never negative — for example,stroke-width: max(0px, calc(10px - 15px)). - Check inline styles and stylesheets. The error can appear in both inline
styleattributes and external/internal CSS. Search your codebase for anystroke-widthdeclaration with a negative number.
Examples
❌ Invalid: negative stroke-width on an HTML element
<p style="stroke-width: -1">Some content</p>
This triggers the validator error because -1 is not an allowed value.
✅ Fixed: non-negative stroke-width
<p style="stroke-width: 0">Some content</p>
Using 0 removes the stroke entirely and is valid.
❌ Invalid: negative stroke-width on an SVG element
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="-3" fill="none"/>
</svg>
✅ Fixed: positive stroke-width on an SVG element
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="none"/>
</svg>
✅ Using max() to clamp a computed value
<div style="stroke-width: max(0px, calc(5px - 10px))">Some content</div>
Here, calc(5px - 10px) would produce -5px, but max(0px, ...) ensures the final value is 0px, keeping the CSS valid.
Text content cannot be placed directly inside an ol element. All text within an ordered list must be wrapped in li elements.
The ol element accepts only li, script, and template elements as children. Any bare text node placed directly between <ol> and </ol> (or between </li> and <li>) is invalid. This includes whitespace-only text nodes in some edge cases, but the validator typically flags visible text content.
This error often appears when list items are missing their <li> tags, or when extra text (like a heading or description) is placed inside the ol instead of before or after it.
Invalid example
<ol>
Some introductory text
<li>First item</li>
<li>Second item</li>
</ol>
Valid example
Move the text outside the ol, or wrap each piece of text in an li element:
<p>Some introductory text</p>
<ol>
<li>First item</li>
<li>Second item</li>
</ol>
Unlike margin properties, which accept negative values to pull elements closer together or overlap them, all padding properties (padding-top, padding-right, padding-bottom, padding-left, and the padding shorthand) are defined in the CSS specification to only accept zero or positive lengths. This is because padding represents the space inside an element between its content and its border — a negative internal space is not a meaningful concept.
When you use a negative padding value, browsers will typically ignore the declaration entirely, meaning your layout may not look the way you intended. The W3C validator catches this to help you identify code that won't behave consistently across browsers and doesn't conform to the CSS specification.
If your goal is to reduce the space between elements, negative margin values are the correct tool. If you're trying to shift content upward within a container, consider using position: relative with a negative top offset, or adjust the layout with other techniques like transform: translateY().
Examples
❌ Invalid: negative padding value
<div style="padding-top: -20px;">
This element has invalid negative padding.
</div>
The validator will report: CSS: "padding-top": "-20" negative values are not allowed.
✅ Fixed: using zero or positive padding
<div style="padding-top: 0;">
This element has no top padding.
</div>
<div style="padding-top: 10px;">
This element has valid positive top padding.
</div>
✅ Alternative: using negative margin instead
If you need to reduce the space above an element, use a negative margin-top:
<div style="margin-top: -20px;">
This element is pulled upward with a negative margin.
</div>
❌ Invalid: negative values in the padding shorthand
The same rule applies to the padding shorthand property. Any negative component value is invalid:
<div style="padding: -10px 20px 15px 20px;">
Invalid shorthand padding.
</div>
✅ Fixed: all-positive shorthand values
<div style="padding: 0 20px 15px 20px;">
Valid shorthand padding with zero top padding.
</div>
✅ Alternative: using transform for visual offset
If you need to visually shift an element's content upward without affecting layout flow, transform is a clean option:
<div style="transform: translateY(-20px);">
This element appears shifted upward.
</div>
An img element with a role attribute cannot have an empty alt attribute because the role overrides the default semantics, and an empty alt signals that the image is decorative — two intentions that contradict each other.
When an img has alt="", it tells assistive technologies to skip the image entirely. The image is treated as presentational, equivalent to role="presentation" or role="none". Assigning a different role (such as button, link, or tab) tells assistive technologies the element has a specific function and should be announced. These two signals conflict: one says "ignore this," the other says "this is interactive" or "this has meaning."
To fix this, decide what the image actually does:
- If the image is decorative and should be ignored, remove the
roleattribute and keepalt="". - If the image has a functional role, provide a descriptive
altvalue that explains the image's purpose.
HTML examples
Image with conflicting role and empty alt
<img src="search.png" alt="" role="button">
Fixed: decorative image without a role
If the image is purely decorative, remove the role:
<img src="search.png" alt="">
Fixed: functional image with descriptive alt
If the image acts as a button, give it a meaningful alt:
<img src="search.png" alt="Search" role="button">
An <input type="hidden"> element is inherently invisible to all users. It is not rendered on the page, it cannot receive focus, and browsers automatically exclude it from the accessibility tree. The aria-hidden attribute is designed to hide visible content from assistive technologies like screen readers, but applying it to an element that is already fully hidden serves no purpose.
The HTML specification explicitly forbids the use of aria-hidden on hidden inputs. This restriction exists because combining the two is semantically meaningless — you cannot "hide from assistive technologies" something that is already invisible to everyone. Validators flag this as an error to encourage clean, standards-compliant markup and to help developers avoid misunderstandings about how ARIA attributes interact with native HTML semantics.
This issue commonly arises when aria-hidden="true" is applied broadly to a group of elements (for example, via a script or template) without checking whether specific children already handle their own visibility. It can also happen when developers add ARIA attributes as a precaution, not realizing that the native behavior of type="hidden" already covers accessibility concerns.
To fix this, remove the aria-hidden attribute from any <input> element whose type is hidden. No replacement is needed — the browser already handles everything correctly.
Examples
Incorrect
Adding aria-hidden to a hidden input triggers a validation error:
<form action="/submit" method="post">
<input type="hidden" aria-hidden="true" name="month" value="10">
<input type="hidden" aria-hidden="true" name="csrf_token" value="abc123">
<button type="submit">Submit</button>
</form>
Correct
Remove the aria-hidden attribute entirely. The hidden inputs are already inaccessible to all users by default:
<form action="/submit" method="post">
<input type="hidden" name="month" value="10">
<input type="hidden" name="csrf_token" value="abc123">
<button type="submit">Submit</button>
</form>
When aria-hidden is appropriate
The aria-hidden attribute is intended for elements that are visually present but should be hidden from assistive technologies, such as decorative icons:
<button type="button">
<span aria-hidden="true">★</span>
Favorite
</button>
In this case, the decorative star is visible on screen but irrelevant to screen reader users, so aria-hidden="true" correctly prevents it from being announced. This is the proper use case — hiding visible content from the accessibility tree, not redundantly marking already-hidden elements.
The aria-label attribute is not allowed on a label element that is associated with a form control through the for attribute.
A <label> element already provides an accessible name for the form control it's associated with. Adding aria-label to the <label> itself creates a conflict: the aria-label would attempt to override the label's own accessible name, but the label's visible text is what gets passed to the associated form control. This redundancy is not only unnecessary but explicitly prohibited by the HTML specification.
The <label> element's purpose is to be the accessible label for another element. If you want the form control to have an accessible name, simply put that text inside the <label> element as visible content. If you need to provide a different accessible name directly to the form control, place the aria-label on the input element instead.
Incorrect Example
<label for="input_email" id="label_input_email" aria-label="Email">
</label>
<input type="email" id="input_email">
Correct Example
The simplest fix is to remove the aria-label from the <label>, since the label's text content already serves as the accessible name for the input:
<label for="input_email" id="label_input_email">
</label>
<input type="email" id="input_email">
If you need the accessible name to differ from the visible label text, place aria-label on the input instead:
<label for="input_email" id="label_input_email">
</label>
<input type="email" id="input_email" aria-label="Your email address">
An aria-hidden attribute on a <label> element that is associated with a form control hides the label from assistive technologies while the control still expects an accessible name from it. This creates a broken association where the form field appears unlabeled to screen readers.
The aria-hidden="true" attribute removes an element and all its descendants from the accessibility tree. When a <label> is linked to an input (either by wrapping it or through the for attribute), the browser uses that label to compute the input's accessible name. Setting aria-hidden="true" on the label strips away that name, leaving the input without a label for assistive technology users.
The HTML spec explicitly forbids this combination because it guarantees an accessibility failure. A label that exists visually but is hidden from the accessibility tree defeats its own purpose.
To fix this, remove aria-hidden from the <label>. If the label text should not be visible on screen, use a CSS technique to visually hide it while keeping it accessible. If the label itself is truly decorative and another mechanism provides the accessible name (like aria-label on the input), remove the for/id association so the <label> is no longer linked to the control.
Incorrect example
<label for="email" aria-hidden="true">Email</label>
<input type="email" id="email">
Correct examples
Remove aria-hidden from the label:
<label for="email">Email</label>
<input type="email" id="email">
If the goal is to visually hide the label while keeping it accessible, use CSS instead:
<label for="email" class="visually-hidden">Email</label>
<input type="email" id="email">
<style>
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
</style>
If the label is decorative and the input gets its accessible name from another source, break the association:
<label aria-hidden="true">Email</label>
<input type="email" aria-label="Email">
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.
Pro Trial
Full Pro access. Cancel anytime.
Start Pro Trial →Join teams across 40+ countries