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 linear-gradient() function went through several syntax revisions during CSS standardization. Early drafts and vendor-prefixed implementations (like -webkit-linear-gradient()) used bare direction keywords such as top, bottom left, etc., where the keyword indicated the starting point of the gradient. The final standard, defined in the CSS Images Module Level 3 and Level 4 specifications, changed this so that direction keywords use the to prefix and indicate the ending point of the gradient. For example, the old linear-gradient(top, #fff, #000) meant "start at the top and go to the bottom," while the correct modern equivalent is linear-gradient(to bottom, #fff, #000).
This matters because the old syntax without to is not valid CSS per the current specification. While some browsers may still interpret the legacy syntax for backward compatibility, relying on it is risky — behavior can vary across browsers, and it will trigger validation errors. Using standard-compliant CSS ensures consistent rendering and forward compatibility.
How to fix it
Replace the bare direction keyword with the correct to syntax. Note that the direction meaning is inverted: the old syntax specified where the gradient starts, while the new syntax specifies where it goes to.
Here's a quick mapping from old to new syntax:
| Old (invalid) | New (valid) | Angle equivalent |
|---|---|---|
top | to bottom | 180deg |
bottom | to top | 0deg |
left | to right | 90deg |
right | to left | 270deg |
top left | to bottom right | N/A (use to syntax) |
Important: Notice that top in the old syntax means "start at top, go to bottom." So the modern equivalent is to bottom, not to top. If the validator message says the argument should be to top, it means you wrote top — but be sure you understand which direction your gradient should actually go before blindly replacing it. If you truly want the gradient to go toward the top, use to top. If you want it to go from the top downward, use to bottom.
If you don't specify a direction at all, linear-gradient() defaults to to bottom (top-to-bottom), which is often what you want.
Examples
Invalid: bare direction keyword
<div style="background: linear-gradient(top, #ffffff, #000000);">
Content
</div>
The bare keyword top is not valid in the standard linear-gradient() syntax and will trigger the validator error.
Fixed: using the to keyword
<div style="background: linear-gradient(to bottom, #ffffff, #000000);">
Content
</div>
Since the old top meant "start at the top," the equivalent standard syntax is to bottom.
Fixed: using an angle
<div style="background: linear-gradient(180deg, #ffffff, #000000);">
Content
</div>
An angle of 180deg produces the same top-to-bottom gradient.
Full document example
<!DOCTYPE html>
<html lang="en">
<head>
<title>Gradient Example</title>
<style>
.box {
width: 200px;
height: 100px;
/* Valid: direction keyword with "to" */
background: linear-gradient(to top, #ffffff, #000000);
}
.box-angle {
width: 200px;
height: 100px;
/* Valid: angle equivalent of "to top" */
background: linear-gradient(0deg, #ffffff, #000000);
}
.box-default {
width: 200px;
height: 100px;
/* Valid: no direction specified, defaults to "to bottom" */
background: linear-gradient(#ffffff, #000000);
}
</style>
</head>
<body>
<div class="box"></div>
<div class="box-angle"></div>
<div class="box-default"></div>
</body>
</html>
All three approaches are valid. Choose whichever is clearest for your use case — the to keyword syntax is generally the most readable, while angles offer more precision for diagonal or non-cardinal directions.
The W3C HTML validator raises this error when the src attribute of an <img> element contains characters that are not permitted in a valid URI. The most common culprit is the < character, but other characters like >, {, }, |, \, ^, and backticks are also illegal in URIs according to RFC 3986.
This issue typically occurs in a few common scenarios:
- Template syntax left unresolved: Server-side or client-side template tags (e.g.,
<%= imageUrl %>,{{ image.src }}) appear literally in the HTML output instead of being processed into actual URLs. - Copy-paste errors: HTML markup or angle brackets accidentally end up inside a
srcvalue. - Malformed dynamic URLs: JavaScript or server-side code incorrectly constructs a URL that includes raw HTML or special characters.
This matters because browsers may fail to load the image or interpret the URL unpredictably. Invalid URIs can also cause issues with screen readers and assistive technologies that try to resolve the src to provide context about the image. Keeping your markup standards-compliant ensures consistent behavior across all browsers and environments.
How to fix it
- Check for unprocessed template tags. If you see template syntax like
<%,{{, or similar in the rendered HTML, ensure your templating engine is running correctly and outputting the resolved URL. - Use valid, well-formed URLs. The
srcvalue should be a properly formatted absolute or relative URL. - Percent-encode special characters. If a special character is genuinely part of the URL (which is rare for
<), encode it:<becomes%3C,>becomes%3E, and so on. - Inspect your generated HTML. View the page source in your browser to confirm the actual output, rather than relying on what your code looks like before processing.
Examples
Incorrect — template syntax in src
The template tag was not processed, leaving a < character in the src attribute:
<img src="<%= user.avatar %>" alt="User avatar">
Incorrect — HTML accidentally inside src
Angle brackets from stray markup ended up in the URL:
<img src="images/<thumbnail>/photo.jpg" alt="Photo">
Correct — a valid relative URL
<img src="images/photo.jpg" alt="Photo">
Correct — a valid absolute URL
<img src="https://example.com/images/photo.jpg" alt="Photo">
Correct — special characters percent-encoded
If the URL genuinely requires characters that are not allowed in a URI, percent-encode them:
<img src="https://example.com/search?q=a%3Cb" alt="Search result">
In this case, %3C represents the < character in the query string, making the URI valid.
Private Use Area (PUA) characters are reserved ranges in Unicode whose interpretation is not specified by any encoding standard. Their meaning is determined entirely by private agreement between cooperating parties—such as a font vendor and its users. This means that a PUA character that renders as a custom icon in one font may appear as a blank square, a question mark, or a completely different glyph when that specific font is unavailable.
This warning commonly appears when using icon fonts like older versions of Font Awesome, Material Icons, or custom symbol fonts. These fonts map their icons to PUA code points. While this approach works visually when the font loads correctly, it creates several problems:
- Accessibility: Screen readers cannot interpret PUA characters meaningfully. A visually impaired user may hear nothing, hear "private use area character," or hear an unrelated description depending on their assistive technology.
- Portability: If the associated font fails to load (due to network issues, content security policies, or user preferences), the characters become meaningless boxes or blank spaces.
- Interoperability: Copy-pasting text containing PUA characters into another application, email client, or document will likely produce garbled or missing content since the receiving system won't know how to interpret those code points.
- Standards compliance: The W3C and Unicode Consortium both recommend against using PUA characters in publicly exchanged documents for exactly these reasons.
Sometimes PUA characters sneak into your HTML unintentionally—through copy-pasting from word processors, PDFs, or design tools that use custom encodings. Other times, they are inserted deliberately via CSS content properties or HTML entities by icon font libraries.
To fix this, identify where the PUA characters appear and replace them with standard alternatives. Use inline SVG for icons, standard Unicode symbols where appropriate (e.g., ✓ U+2713 instead of a PUA checkmark), or CSS background images. If you must use an icon font, hide the PUA character from assistive technology using aria-hidden="true" and provide an accessible label separately.
Examples
Problematic: PUA character used directly in HTML
<!-- The character below (U+E001) is a PUA code point -->
<p>Status: </p>
Without the specific icon font loaded, PUA characters like U+E001 render as missing glyphs or blank spaces.
Fixed: Using inline SVG with accessible label
<p>
Status:
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
<path d="M6 10.8L2.5 7.3 1.1 8.7 6 13.6 14.9 4.7 13.5 3.3z"/>
</svg>
<span>Complete</span>
</p>
Problematic: Icon font via CSS content property
<style>
.icon-check::before {
font-family: "MyIcons";
content: "\e001"; /* PUA character */
}
</style>
<span class="icon-check"></span>
Fixed: Icon font with accessibility safeguards
If you must continue using an icon font, hide the PUA character from assistive technology and provide an accessible alternative:
<style>
.icon-check::before {
font-family: "MyIcons";
content: "\e001";
}
</style>
<span class="icon-check" aria-hidden="true"></span>
<span class="sr-only">Checkmark</span>
Note that this approach still triggers the validator warning if the PUA character is detectable in the markup. The most robust fix is to avoid PUA characters entirely.
Fixed: Using a standard Unicode character
<p>Status: ✓ Complete</p>
The character ✓ (U+2713, CHECK MARK) is a standard Unicode character that is universally understood and renders consistently across platforms.
Problematic: PUA character from copy-paste
<p>Click here to download</p>
Invisible or unexpected PUA characters sometimes hide in text pasted from external sources. Inspect your source code carefully—many code editors can highlight non-ASCII characters or reveal their code points.
Fixed: Cleaned-up text
<p>Click here to download</p>
If you've audited your document and determined that the PUA characters are intentional and rendering correctly in your target environments, you may choose to accept this warning. However, for publicly accessible web pages, replacing PUA characters with standard alternatives is always the safer and more accessible choice.
A URL fragment identifier is the part of a URL that follows the # symbol, typically used to link to a specific section within a page. The URL specification (RFC 3986 and the WHATWG URL Standard) defines a strict set of characters that are permitted in fragments. The > character (greater-than sign) is among those that are not allowed to appear literally. When the W3C HTML Validator encounters > inside an href value — whether in the fragment or elsewhere in the URL — it raises this error.
In most cases, the > character appears in a URL by accident — for example, from a copy-paste error, a typo where the closing > of the tag accidentally ends up inside the attribute value, or a malformed template expression. Less commonly, a developer may genuinely need the > character as part of the URL content.
Why This Matters
- Broken links: Browsers may interpret the
>as the end of the HTML tag or handle the URL unpredictably, leading to broken navigation. - Standards compliance: Invalid URLs violate both the HTML specification and URL syntax standards, causing validation failures.
- Accessibility: Screen readers and other assistive technologies rely on well-formed markup. A malformed
hrefcan confuse these tools and prevent users from reaching the intended destination. - Interoperability: While some browsers may silently correct or ignore the invalid character, others may not. Valid URLs guarantee consistent behavior everywhere.
How to Fix It
- If the
>is a typo or copy-paste error, simply remove it from thehrefvalue. This is the most common scenario. - If the
>is intentionally part of the fragment or URL, replace it with its percent-encoded equivalent:%3E. - Review your templates or CMS output — this error often originates from template engines or content management systems that inject malformed values into
hrefattributes.
Examples
Accidental > in the URL path
A common mistake is accidentally including the tag's closing > inside the attribute value:
<!-- ❌ Invalid: ">" is not allowed in the URL -->
<a href="/page.php>">Read more</a>
Remove the stray >:
<!-- ✅ Valid: clean URL without illegal character -->
<a href="/page.php">Read more</a>
Illegal > in a fragment identifier
The > can also appear inside the fragment portion of the URL:
<!-- ❌ Invalid: ">" in the fragment -->
<a href="/docs#section->overview">Go to overview</a>
If the > is unintentional, remove it:
<!-- ✅ Valid: fragment without illegal character -->
<a href="/docs#section-overview">Go to overview</a>
If the > is genuinely needed in the fragment, percent-encode it:
<!-- ✅ Valid: ">" encoded as %3E -->
<a href="/docs#section-%3Eoverview">Go to overview</a>
Query string or path containing >
The same rule applies outside of fragments. If > appears anywhere in the URL, encode it:
<!-- ❌ Invalid: ">" in query parameter -->
<a href="/search?filter=price>100">Expensive items</a>
<!-- ✅ Valid: ">" percent-encoded as %3E -->
<a href="/search?filter=price%3E100">Expensive items</a>
Template output errors
If you are using a server-side template or JavaScript framework, ensure that dynamic values inserted into href are properly URL-encoded. For example, in a template that generates links:
<!-- ❌ Invalid: unencoded dynamic value -->
<a href="/results#filter=>50">View results</a>
<!-- ✅ Valid: properly encoded value -->
<a href="/results#filter=%3E50">View results</a>
Most server-side languages provide built-in URL encoding functions (e.g., encodeURIComponent() in JavaScript, urlencode() in PHP, urllib.parse.quote() in Python) that will handle this automatically. Always use these functions when inserting dynamic content into URLs.
A | (pipe) character in the fragment portion of a URL is not allowed without percent-encoding it as %7C.
The fragment is the part of a URL that comes after the # symbol. According to RFC 3986, fragments can only contain unreserved characters (A-Z, a-z, 0-9, -, ., _, ~), percent-encoded characters, sub-delimiters (!, $, &, ', (, ), *, +, ,, ;, =), :, @, /, and ?.
The pipe character | is not in any of those sets, so it must be percent-encoded as %7C when used in a URL fragment.
This issue commonly appears when fragment identifiers are auto-generated from headings or content that contains |, such as table-related text or wiki-style page anchors.
Invalid example
<a href="/page#section|intro">Go to section</a>
Valid example
Replace | with its percent-encoded equivalent %7C:
<a href="/page#section%7Cintro">Go to section</a>
If you control the target element's id, a better approach is to remove the pipe character from the id altogether and use only unreserved characters:
<h2 id="section-intro">Section intro</h2>
<a href="/page#section-intro">Go to section</a>
The ping attribute specifies a space-separated list of URLs that the browser should notify (via a small POST request) when a user follows a hyperlink. This is commonly used for click tracking and analytics. According to the HTML specification, every URL in the list must be a valid, non-empty URL that uses either the http or https scheme — no other schemes or relative paths are permitted.
This restriction exists for practical and security reasons. The ping mechanism is specifically designed for web-based tracking endpoints, so only web protocols make sense. Relative URLs are disallowed because the ping is sent as a separate request independent of normal navigation, and the specification requires absolute URLs to unambiguously identify the target server. Using invalid values won't produce the intended tracking behavior and will cause the browser to silently ignore the ping attribute entirely.
From an accessibility and standards compliance standpoint, ensuring valid ping values means your analytics will work reliably in browsers that support the attribute. Note that browser support varies — some browsers (notably Firefox) disable ping by default or hide it behind a preference — so you should not rely on it as your sole tracking mechanism.
How to fix it
- Replace relative URLs with absolute URLs. If you have a value like
/trackortrack.php, prepend the full origin (e.g.,https://example.com/track). - Remove non-HTTP schemes. Values like
mailto:someone@example.comorftp://example.com/logare not valid forping. - Ensure each URL in the list is properly formatted. Multiple URLs must be separated by spaces (not commas or semicolons), and each one must be a complete
httporhttpsURL.
Examples
Incorrect: relative URL
<a href="https://example.com" ping="/track">Visit Example</a>
The value /track is a relative URL, which is not allowed in the ping attribute.
Incorrect: unsupported scheme
<a href="https://example.com" ping="ftp://example.com/log">Visit Example</a>
The ftp: scheme is not permitted — only http and https are valid.
Incorrect: comma-separated URLs
<a href="https://example.com" ping="https://example.com/track, https://analytics.example.com/log">Visit Example</a>
Multiple URLs must be space-separated, not comma-separated. The commas make each URL invalid.
Correct: single absolute URL
<a href="https://example.com" ping="https://example.com/track">Visit Example</a>
Correct: multiple space-separated absolute URLs
<a href="https://example.com" ping="https://example.com/track https://analytics.example.com/log">Visit Example</a>
Each URL is a fully qualified https URL, and they are separated by a single space. Both will receive a POST request when the link is clicked (in browsers that support the ping attribute).
The | (pipe) character is not allowed in URLs and must be percent-encoded as %7C in the href attribute of an <a> element.
URLs follow the syntax rules defined in RFC 3986. The pipe character is not among the allowed characters in the query component of a URI. When a URL contains a literal |, browsers will often handle it gracefully, but the HTML is technically invalid. The fix is to replace every | with its percent-encoded equivalent %7C.
This commonly appears when linking to external APIs, legacy systems, or content management platforms that use | as a delimiter in query strings.
HTML examples
Invalid example
<a href="https://example.com/search?filters=color|size|price">Search</a>
Valid example
<a href="https://example.com/search?filters=color%7Csize%7Cprice">Search</a>
If the URLs are generated dynamically in JavaScript, encodeURI() or encodeURIComponent() will handle the encoding automatically:
const filters = "color|size|price";
const url = `https://example.com/search?filters=${encodeURIComponent(filters)}`;
// Result: https://example.com/search?filters=color%7Csize%7Cprice
The mailto: link in this href contains a < character, which is not allowed in a URL. It usually appears when an email is pasted in Name <address> form, or when two addresses are joined without a proper separator.
A mailto: URL holds one or more bare email addresses after the colon, nothing else. Angle brackets, spaces, and the Display Name <address> notation used by email clients are not valid URL characters, so the validator rejects the link at the first illegal character. An encoded space (%20) followed by a second copy of the address, as in this case, is a typical sign that a Name <address> string or a pair of addresses was copied straight into the attribute.
To fix it, keep only the address itself. Drop the display name and the angle brackets, and if the link targets several people, separate the addresses with commas.
Invalid example
<a href="mailto:info@example.com%20<info@example.com">Email us</a>
Valid example
A single recipient needs only the bare address:
<a href="mailto:info@example.com">Email us</a>
For more than one recipient, separate the addresses with commas:
<a href="mailto:info@example.com,research@example.com">Email us</a>
The imagesrcset attribute is used exclusively on <link> elements that have rel="preload" and as="image". It mirrors the srcset attribute of the <img> element, allowing the browser to preload the most appropriate image resource based on the current viewport and display conditions. When the validator encounters imagesrcset="" (an empty value), it reports this error because an empty string is not a valid source set — it must contain at least one image candidate string.
Each image candidate string in the imagesrcset value consists of a URL followed by an optional width descriptor (e.g., 480w) or pixel density descriptor (e.g., 2x). Multiple candidates are separated by commas. This is the same syntax used by the srcset attribute on <img> elements.
This issue typically arises when a CMS, static site generator, or templating engine outputs the imagesrcset attribute with an empty value — for example, when a responsive image field has no data. Browsers may ignore the malformed attribute, but it results in invalid HTML, can cause unexpected preloading behavior, and signals that the page's resource hints are misconfigured. Fixing it ensures standards compliance and that the browser's preload scanner works as intended.
How to fix it
- Provide a valid source set — populate
imagesrcsetwith one or more image candidate strings. - Remove the attribute — if you don't have multiple image sources to preload, remove
imagesrcset(andimagesizes) from the<link>element entirely. You can still preload a single image using just thehrefattribute. - Conditionally render — if your templating system might produce an empty value, add logic to omit the attribute when no responsive sources are available.
When using imagesrcset, you should also include the imagesizes attribute (mirroring the sizes attribute on <img>) so the browser can select the correct candidate based on layout information.
Examples
❌ Empty imagesrcset triggers the error
<link rel="preload" as="image" href="hero.jpg" imagesrcset="" imagesizes="">
The empty imagesrcset value is invalid and produces the W3C validation error.
✅ Valid imagesrcset with width descriptors
<link
rel="preload"
as="image"
href="hero-800.jpg"
imagesrcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
imagesizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px">
This tells the browser to preload the most appropriate image based on the viewport width, matching the responsive behavior of the corresponding <img> element on the page.
✅ Valid imagesrcset with pixel density descriptors
<link
rel="preload"
as="image"
href="logo.png"
imagesrcset="logo.png 1x, logo@2x.png 2x">
This preloads the correct logo variant based on the device's pixel density.
✅ Removing the attribute when no responsive sources exist
<link rel="preload" as="image" href="hero.jpg">
If you only have a single image to preload, simply use href without imagesrcset. This is valid and avoids the error entirely.
✅ Conditional rendering in a template
If you're using a templating language, conditionally include the attribute:
<!-- Pseudocode example -->
<link
rel="preload"
as="image"
href="hero.jpg"
{% if responsive_sources %}
imagesrcset="{{ responsive_sources }}"
imagesizes="{{ image_sizes }}"
{% endif %}>
This prevents the attribute from being rendered with an empty value when no responsive image data is available.
In URLs, certain characters must be percent-encoded — a process where a character is replaced by % followed by exactly two hexadecimal digits representing its byte value. For example, a space becomes %20, a hash becomes %23, and an ampersand becomes %26. The % character itself is the escape prefix, so when a URL parser encounters %, it expects the next two characters to be valid hexadecimal digits (0–9, A–F, a–f). If they aren't, the URL is malformed.
This validation error typically arises in a few scenarios:
- A literal
%is used in the URL without encoding. For instance, a filename or path segment contains a%sign that wasn't converted to%25. - An incomplete or corrupted percent-encoding sequence. Someone may have partially encoded a URL, leaving behind sequences like
%G5or%2that don't form valid two-digit hex codes. - Copy-paste errors. A URL was pasted from a source that stripped characters or introduced stray
%symbols.
This matters because browsers may interpret malformed URLs inconsistently. One browser might try to "fix" the URL by encoding the stray %, while another might pass it through as-is, leading to broken form submissions or unexpected server-side behavior. Ensuring valid URLs in the action attribute guarantees predictable behavior across all browsers and complies with both the WHATWG URL Standard and the HTML specification.
How to Fix
- Locate every
%in the URL. Check whether each%is followed by exactly two hexadecimal digits (e.g.,%20,%3A,%7E). - Encode literal
%characters. If a%is meant to appear as a literal character in the URL (not as part of a percent-encoding sequence), replace it with%25. - Fix incomplete sequences. If a sequence like
%2or%GZexists, determine the intended character and encode it correctly, or remove the stray%. - Use proper encoding tools. In JavaScript, use
encodeURIComponent()orencodeURI()to safely encode URLs. Most server-side languages have equivalent functions (e.g.,urlencode()in PHP,urllib.parse.quote()in Python).
Examples
Literal % not encoded
This triggers the error because %d is not a valid percent-encoding sequence (d is only one character, and what follows may not be hex):
<!-- ❌ Bad: bare % not followed by two hex digits -->
<form action="/search?discount=20%off">
<button type="submit">Search</button>
</form>
Encode the % as %25:
<!-- ✅ Good: literal % encoded as %25 -->
<form action="/search?discount=20%25off">
<button type="submit">Search</button>
</form>
Incomplete percent-encoding sequence
Here, %2 is missing its second hex digit:
<!-- ❌ Bad: %2 is an incomplete sequence -->
<form action="/path/to%2file.html">
<button type="submit">Submit</button>
</form>
If the intent was to encode a / character (%2F), complete the sequence:
<!-- ✅ Good: complete percent-encoding for "/" -->
<form action="/path/to%2Ffile.html">
<button type="submit">Submit</button>
</form>
Invalid hex characters after %
The characters G and Z are not hexadecimal digits:
<!-- ❌ Bad: %GZ is not valid hex -->
<form action="/data%GZprocess">
<button type="submit">Go</button>
</form>
If %GZ was never intended as encoding, escape the % itself:
<!-- ✅ Good: literal % properly encoded -->
<form action="/data%25GZprocess">
<button type="submit">Go</button>
</form>
Using JavaScript to encode safely
When building URLs dynamically, use built-in encoding functions to avoid this issue entirely:
const query = "20% off";
const safeURL = "/search?q=" + encodeURIComponent(query);
// Result: "/search?q=20%25%20off"
This ensures every special character — including % — is properly percent-encoded before it's placed in the action attribute.
The HTML specification restricts what can appear inside a comment. Specifically, a comment must not contain the string -- (two consecutive hyphens) except as part of the opening and closing delimiters. This rule originates from SGML and XML 1.0, where -- is treated as a comment delimiter, and having extra occurrences inside the comment body creates ambiguity about where the comment starts and ends.
While most modern browsers are lenient and will handle comments with double hyphens without issues, the markup is technically invalid. This matters for several reasons:
- XML compatibility: If your HTML is served as XHTML or processed by XML parsers, double hyphens inside comments will cause parsing errors and potentially break the entire document.
- Standards compliance: The HTML living standard explicitly states that comments must not contain
--, so validators flag this as an error. - Predictable parsing: Different parsers may interpret malformed comments differently, leading to inconsistent behavior across tools, crawlers, and assistive technologies.
To fix the issue, look inside your comment text and replace any occurrence of -- with something else, such as a single hyphen, an equals sign, or a different separator.
Examples
Incorrect: double hyphens inside the comment body
<!-- This is a separator ---------- end of section -->
The string of hyphens inside the comment contains multiple consecutive -- pairs, making the comment invalid.
<!-- Do not use -- as a separator -->
Here, -- appears literally in the comment text, which violates the rule.
<!-- TODO -- fix this later -- maybe next sprint -->
Multiple -- sequences appear as casual text separators, each one triggering the validation error.
Correct: avoiding double hyphens
Replace double hyphens with a different character or pattern:
<!-- This is a separator ========== end of section -->
<!-- Do not use dashes as a separator -->
<!-- TODO: fix this later, maybe next sprint -->
Correct: standard comment syntax
Simple comments that don't contain -- in their body are perfectly valid:
<!-- This is a valid comment -->
<!--
Multi-line comments are fine too,
as long as they don't contain double hyphens.
-->
Common pitfall: decorative comment borders
Developers sometimes use hyphens to create visual separators in their source code:
<!-- -------------------------------- -->
<!-- Navigation Section -->
<!-- -------------------------------- -->
Replace these with a different character:
<!-- ================================ -->
<!-- Navigation Section -->
<!-- ================================ -->
Or simply remove the decorative lines:
<!-- Navigation Section -->
The tabindex attribute controls whether and in what order an element can receive keyboard focus. The W3C validator reports this error when it encounters tabindex="" because the HTML specification requires the attribute's value to be a valid integer — and an empty string cannot be parsed as one. This commonly happens when a value is accidentally omitted, when a template engine outputs a blank value, or when a CMS generates the attribute without a proper default.
Why this matters
Keyboard navigation is fundamental to web accessibility. Screen readers and assistive technologies rely on tabindex values to determine focus behavior. An empty tabindex attribute creates ambiguity: browsers may ignore it or handle it inconsistently, leading to unpredictable focus behavior for keyboard and assistive technology users. Beyond accessibility, it also means your HTML is invalid according to the WHATWG HTML living standard, which strictly defines tabindex as accepting only a valid integer.
How tabindex works
The attribute accepts an integer value with three meaningful ranges:
- Negative value (e.g.,
tabindex="-1"): The element can be focused programmatically (via JavaScript's.focus()), but it is excluded from the sequential keyboard tab order. tabindex="0": The element is added to the natural tab order based on its position in the document source. This is the most common way to make non-interactive elements (like a<div>or<span>) keyboard-focusable.- Positive value (e.g.,
tabindex="1",tabindex="5"): The element is placed in the tab order ahead of elements withtabindex="0"or notabindex, with lower numbers receiving focus first. Using positive values is generally discouraged because it overrides the natural document order and makes focus management harder to maintain.
How to fix it
- If the element should be focusable in tab order, set
tabindex="0". - If the element should only be focusable programmatically, set
tabindex="-1". - If you don't need custom focus behavior, remove the
tabindexattribute entirely. Interactive elements like<a>,<button>, and<input>are already focusable by default. - If a template or CMS generates the attribute, ensure the logic provides a valid integer or omits the attribute when no value is available.
Examples
❌ Empty tabindex value (triggers the error)
<div tabindex="">Click me</div>
<button tabindex="">Submit</button>
✅ Fixed: valid integer provided
<div tabindex="0">Click me</div>
<button tabindex="-1">Submit</button>
✅ Fixed: attribute removed when not needed
Interactive elements like <button> are focusable by default, so tabindex can simply be removed:
<button>Submit</button>
✅ Fixed: conditional rendering in a template
If you're using a templating system, ensure the attribute is only rendered when a valid value exists:
<!-- Instead of always outputting tabindex -->
<!-- Bad: <div tabindex="{{ tabindexValue }}"> -->
<!-- Only output the attribute when a value is set -->
<div tabindex="0">Interactive content</div>
A note on positive tabindex values
While positive integers like tabindex="1" or tabindex="3" are technically valid, they force a custom tab order that diverges from the visual and DOM order of the page. This creates confusion for keyboard users and is difficult to maintain as pages evolve. The WAI-ARIA Authoring Practices recommend avoiding positive tabindex values. Stick with 0 and -1 in nearly all cases.
When the W3C HTML Validator reports "Bad value X for attribute src on element iframe: Illegal character in query: [ is not allowed", it means your URL contains unencoded square brackets in the query portion (everything after the ?). While most modern browsers will silently handle these characters and load the resource anyway, the URL does not conform to the URI syntax defined in RFC 3986, which the HTML specification requires.
According to RFC 3986, square brackets are reserved characters that have a specific purpose: they are only permitted in the host component of a URI to denote IPv6 addresses (e.g., http://[::1]/). Anywhere else in the URI — including the query string — they must be percent-encoded. The HTML living standard (WHATWG) requires that URLs in attributes like src, href, and action be valid URLs, which means they must follow these encoding rules.
Why this matters
- Standards compliance: Invalid URLs cause W3C validation failures, which can indicate deeper issues in your markup.
- Interoperability: While mainstream browsers are forgiving, some HTTP clients, proxies, CDNs, or web application firewalls may reject or mangle URLs with unencoded square brackets.
- Consistent behavior: Percent-encoding reserved characters guarantees that the URL is interpreted the same way everywhere — in browsers, server logs, link checkers, and automated tools.
- Copy-paste reliability: When users or tools copy a URL from your HTML source, an already-encoded URL is less likely to break during transmission through email clients, messaging apps, or other systems.
How to fix it
Replace every occurrence of [ with %5B and ] with %5D within the query string of the URL. If you're generating URLs server-side or in JavaScript, use the language's built-in URL encoding functions rather than doing manual find-and-replace:
- JavaScript:
encodeURIComponent()or theURL/URLSearchParamsAPIs - PHP:
urlencode()orhttp_build_query() - Python:
urllib.parse.urlencode()orurllib.parse.quote()
These functions will automatically encode square brackets and any other reserved characters.
Examples
❌ Invalid — unencoded square brackets in query string
<iframe src="https://example.com/embed?filter[status]=active&filter[type]=video"></iframe>
The validator flags [ and ] as illegal characters in the query component.
✅ Valid — square brackets percent-encoded
<iframe src="https://example.com/embed?filter%5Bstatus%5D=active&filter%5Btype%5D=video"></iframe>
Replacing [ with %5B and ] with %5D resolves the error. The server receives the exact same parameter values — most server-side frameworks (PHP, Rails, etc.) automatically decode percent-encoded characters before processing them.
❌ Invalid — square brackets in a timestamp parameter
<iframe src="https://example.com/report?time=[2024-01-01]"></iframe>
✅ Valid — timestamp parameter properly encoded
<iframe src="https://example.com/report?time=%5B2024-01-01%5D"></iframe>
Generating encoded URLs in JavaScript
If you're setting the src dynamically, let the browser handle encoding for you:
<iframe id="report-frame"></iframe>
<script>
const url = new URL("https://example.com/embed");
url.searchParams.set("filter[status]", "active");
url.searchParams.set("filter[type]", "video");
document.getElementById("report-frame").src = url.toString();
</script>
The URLSearchParams API automatically percent-encodes the brackets, producing a valid URL in the src attribute.
A note on other elements
This same rule applies to any HTML attribute that accepts a URL — including href on <a> elements, action on <form> elements, and src on <script> or <img> elements. Whenever you place a URL in HTML, ensure all reserved characters in the query string are properly percent-encoded.
A space character in the fragment portion of a URL (the part after #) is not allowed in the src attribute of an img element.
URLs follow the syntax defined in RFC 3986, which does not permit literal spaces anywhere in the URI, including the fragment identifier. Browsers may silently handle spaces by percent-encoding them, but the raw HTML remains invalid. The W3C validator flags this because the src value must be a valid URL before the browser ever processes it.
To fix the issue, replace each space in the URL with %20, which is the percent-encoded form of a space character. This applies to all parts of the URL: the path, query string, and fragment.
A common cause is copying a URL from a browser address bar or a document system where filenames or anchors contain spaces. Another cause is linking to a named anchor (id) on a page where the id value itself contains a space, which is also invalid HTML — id values must not contain spaces.
HTML examples
Invalid: space in fragment
<img src="diagram.svg#my section" alt="Diagram of section layout">
Valid: percent-encoded space
<img src="diagram.svg#my%20section" alt="Diagram of section layout">
If you control the target resource, a better fix is to remove the space from the fragment identifier entirely:
<img src="diagram.svg#my-section" alt="Diagram of section layout">
Using hyphens or underscores instead of spaces in fragment identifiers and file names avoids this class of problem altogether.
A backslash (\) is not a valid character in a URL fragment and must be replaced with a forward slash (/) or percent-encoded.
The href attribute on an <a> element must contain a valid URL according to the URL Living Standard. The fragment portion of a URL — the part after the # symbol — follows the same rule: it can contain most characters, but the backslash (\) is explicitly forbidden as a bare character.
This commonly happens when copying file paths from Windows, which uses backslashes as directory separators, and pasting them into an href. Browsers may silently convert \ to /, but the markup is still invalid.
To fix this, replace every \ with / in the URL. If for some reason you actually need a literal backslash in the fragment, percent-encode it as %5C.
HTML Examples
❌ Invalid: backslash in the fragment
<a href="page.html#section\one">Link</a>
✅ Fixed: use a forward slash or percent-encoding
<!-- Option 1: Replace with forward slash -->
<a href="page.html#section/one">Link</a>
<!-- Option 2: Percent-encode the backslash -->
<a href="page.html#section%5Cone">Link</a>
A space character in the href attribute of an <a> element is not valid in a URL. Spaces must be encoded as %20 in the query string.
URLs follow the syntax defined in RFC 3986, which does not allow literal space characters anywhere in the URL. When the W3C validator encounters a space in a query string, it flags the href value as malformed. Browsers often handle this gracefully by encoding the space automatically, but the HTML itself is still invalid.
The query component of a URL (everything after the ?) follows the same rule. If a value in the query string contains a space, replace each space with %20. An alternative encoding, +, is specific to the application/x-www-form-urlencoded format used by HTML form submissions, and is also accepted in practice by most servers. However, %20 is the universally correct percent-encoding for a space in any part of a URL.
Invalid example
<a href="https://example.com/search?query=hello world&lang=en">Search</a>
The space between hello and world causes the validation error.
Valid example
<a href="https://example.com/search?query=hello%20world&lang=en">Search</a>
Every space in the URL is replaced with %20, and the markup passes validation.
A mailto: link in an href attribute contains a [ character that is not valid in a URI according to RFC 3986.
Square brackets ([ and ]) are reserved characters in URIs and must be percent-encoded when used outside their specific purpose (IPv6 address literals). If the email address or other part of the mailto: URI contains a [, it must be encoded as %5B. Similarly, ] must be encoded as %5D.
This error typically appears when a mailto: link includes square brackets in the subject, body, or display-related parts of the URL. Query parameters in mailto: links — such as ?subject= or ?body= — must have their values properly percent-encoded.
Invalid example
<a href="mailto:info@example.com?subject=[Action Required] Review">
Email us
</a>
Valid example
Percent-encode the square brackets in the query string:
<a href="mailto:info@example.com?subject=%5BAction%20Required%5D%20Review">
Email us
</a>
The [ becomes %5B, the ] becomes %5D, and spaces become %20. Most server side languages and JavaScript provide functions to handle this encoding automatically, such as encodeURIComponent() in JavaScript.
A span element has an implicit ARIA role of generic, and the aria-labelledby attribute is not allowed on elements with that role.
The span element is a generic inline container with no semantic meaning. Its default ARIA role is generic, and the ARIA specification explicitly prohibits naming generic elements with aria-labelledby (or aria-label). This restriction exists because accessible names on generic containers create confusing experiences for assistive technology users — screen readers wouldn't know what kind of thing is being labeled.
To fix this, you have two main options:
- Add a meaningful
roleto thespanthat supportsaria-labelledby, such asrole="group",role="region", or any other role that accepts a label. - Use a more semantic element that already has an appropriate role, like a
section,nav, ordivwith an explicit role.
If the span doesn't truly need a label, simply remove the aria-labelledby attribute.
HTML Examples
❌ Invalid: aria-labelledby on a plain span
<span id="label">Settings</span>
<span aria-labelledby="label">
<input type="checkbox" id="opt1">
<label for="opt1">Enable notifications</label>
</span>
✅ Fix: Add an appropriate role
<span id="label">Settings</span>
<span role="group" aria-labelledby="label">
<input type="checkbox" id="opt1">
<label for="opt1">Enable notifications</label>
</span>
✅ Fix: Use a semantic element instead
<span id="label">Settings</span>
<fieldset aria-labelledby="label">
<input type="checkbox" id="opt1">
<label for="opt1">Enable notifications</label>
</fieldset>
An aria-label attribute on an <a> element is only valid when the link has an accessible role that supports naming — which means the <a> must have an href attribute or an explicit role that accepts a label.
When an <a> element lacks an href attribute, it has the implicit role of generic. The generic role is in the list of roles that do not support naming, so applying aria-label to it is invalid. This is because a generic element has no semantic meaning, and screen readers wouldn't know how to announce the label in a meaningful way.
The most common cause of this error is using <a> as a placeholder or JavaScript-only trigger without an href. An <a> with an href has the implicit role of link, which does support aria-label, so the error won't appear.
You have a few ways to fix this:
- Add an
hrefto make it a proper link (most common fix). - Add an explicit role that supports naming, such as
role="button", if the element acts as a button. - Use a
<button>instead if the element triggers an action rather than navigation. - Remove
aria-labelif it's not needed, and use visible text content instead.
HTML Examples
❌ Invalid: aria-label on an <a> without href
<a aria-label="Close menu" onclick="closeMenu()">✕</a>
The <a> has no href, so its implicit role is generic, which does not support naming.
✅ Fix option 1: Add an href
<a href="/close" aria-label="Close menu">✕</a>
✅ Fix option 2: Use a <button> instead
<button aria-label="Close menu" onclick="closeMenu()">✕</button>
✅ Fix option 3: Add an explicit role that supports naming
<a role="button" tabindex="0" aria-label="Close menu" onclick="closeMenu()">✕</a>
Using a <button> (option 2) is generally the best choice for interactive elements that perform actions rather than navigate to a URL.
A span element has an implicit ARIA role of generic, and the aria-label attribute is not allowed on elements with that role.
The span element is a generic inline container with no semantic meaning. Its default ARIA role is generic, which is one of several roles that prohibit naming via aria-label or aria-labelledby. This restriction exists because screen readers are not expected to announce names for generic containers — adding aria-label to them creates an inconsistent and confusing experience for assistive technology users.
To fix this, you have two main options:
- Assign an explicit role to the
spanthat supports naming, such asrole="img",role="group",role="status", or any other role that allowsaria-label. - Use a different element that already has a semantic role supporting
aria-label, such as abutton,a,section, ornav.
If the span is purely decorative or used for styling, consider using aria-hidden="true" instead and placing accessible text elsewhere.
HTML Examples
❌ Invalid: aria-label on a plain span
<span aria-label="Close">✕</span>
✅ Fixed: assign an appropriate role
<span role="img" aria-label="Close">✕</span>
✅ Fixed: use a semantic element instead
<button aria-label="Close">✕</button>
✅ Fixed: hide the decorative span and provide text another way
<button>
<span aria-hidden="true">✕</span>
<span class="visually-hidden">Close</span>
</button>
A div element without an explicit role resolves to the generic role, which does not support naming — so adding aria-label to a plain div is invalid.
The aria-label attribute provides an accessible name for an element, but not every element is allowed to have one. The ARIA specification defines certain roles as "naming prohibited," meaning assistive technologies will ignore any accessible name applied to them. The generic role is one of these, and since a div without an explicit role attribute defaults to generic, the aria-label is effectively meaningless.
To fix this, you have two main options: assign an appropriate ARIA role to the div so it becomes a nameable landmark or widget, or switch to a semantic HTML element that already carries a valid role. Common roles that support naming include region, group, navigation, alert, and many others.
If the div is truly just a generic wrapper with no semantic meaning, consider whether aria-label is even needed. Perhaps the label belongs on a child element instead, or the content is already self-describing.
HTML Examples
❌ Invalid: aria-label on a plain div
<div aria-label="User profile section">
<p>Welcome, Jane!</p>
</div>
✅ Fix: Add an appropriate role
<div role="region" aria-label="User profile section">
<p>Welcome, Jane!</p>
</div>
✅ Fix: Use a semantic element instead
<section aria-label="User profile section">
<p>Welcome, Jane!</p>
</section>
The <time> element does not support the aria-label attribute when it has no explicit role or when it carries certain generic roles.
The <time> element has an implicit ARIA role of time, but this role is not listed among those that allow aria-label. According to the ARIA in HTML specification, aria-label is only permitted on elements with roles that support naming from author — and the default role of <time> (as well as roles like generic, presentation, paragraph, and others listed in the error) does not qualify.
In practice, the <time> element already conveys its meaning through its visible text content and the machine-readable datetime attribute. Screen readers use the visible text to announce the date, so aria-label is typically unnecessary.
To fix this, simply remove the aria-label attribute and ensure the visible text content is descriptive enough. If you need to provide a more accessible reading of the date, you can adjust the visible text itself or wrap the element with a <span> that has an appropriate role.
Also note the original code has a missing space before datetime — the attribute must be separated from class="Tag" by a space.
HTML Examples
❌ Invalid: aria-label on <time>
<time aria-label="Apr 2." class="Tag" datetime="2026-04-02">
Apr 2.
</time>
✅ Fixed: Remove aria-label and use clear visible text
<time class="Tag" datetime="2026-04-02">
April 2, 2026
</time>
If you truly need aria-label, you can assign an explicit role that supports naming, such as role="text", though this is rarely necessary:
<time role="text" aria-label="April 2nd, 2026" class="Tag" datetime="2026-04-02">
Apr 2.
</time>
A div element without an explicit role (or with role="generic") cannot have the aria-labelledby attribute because generic containers have no semantic meaning that benefits from a label.
The div element maps to the generic ARIA role by default. Generic elements are purely structural — they don't represent anything meaningful to assistive technologies. Labeling something that has no semantic purpose creates a confusing experience for screen reader users, since the label points to an element that doesn't convey a clear role.
The aria-labelledby attribute is designed for interactive or landmark elements — things like dialog, region, navigation, form, or group — where a label helps users understand the purpose of that section.
To fix this, you have two options: assign a meaningful ARIA role to the div, or use a more semantic HTML element that naturally supports labeling.
HTML Examples
❌ Invalid: aria-labelledby on a plain div
<h2 id="section-title">User Settings</h2>
<div aria-labelledby="section-title">
<p>Manage your account preferences here.</p>
</div>
✅ Fixed: Add a meaningful role to the div
<h2 id="section-title">User Settings</h2>
<div role="region" aria-labelledby="section-title">
<p>Manage your account preferences here.</p>
</div>
✅ Fixed: Use a semantic element instead
<h2 id="section-title">User Settings</h2>
<section aria-labelledby="section-title">
<p>Manage your account preferences here.</p>
</section>
Using a section element or adding role="region" tells assistive technologies that this is a distinct, meaningful area of the page — making the label useful and the markup valid.
The aria-label attribute cannot be used on an <i> element with its default implicit role (generic), because generic elements are not allowed to have accessible names.
The <i> element has an implicit ARIA role of generic, which is one of the roles explicitly prohibited from carrying an aria-label. This restriction exists because screen readers and other assistive technologies ignore accessible names on generic containers — so adding aria-label to a plain <i> element would silently fail to convey any meaning to users who rely on assistive technology.
This issue commonly appears when icon fonts (like Font Awesome) use <i> elements as decorative icons. If the icon is purely decorative, you should hide it from assistive technology with aria-hidden="true" and place the accessible label on a parent or sibling element instead. If the icon conveys meaning on its own, you can assign an appropriate role like role="img" so the aria-label is actually announced.
HTML Examples
❌ Invalid: aria-label on a plain <i> element
<button>
<i class="icon-search" aria-label="Search"></i>
</button>
✅ Fix 1: Decorative icon — hide it, label the parent
<button aria-label="Search">
<i class="icon-search" aria-hidden="true"></i>
</button>
✅ Fix 2: Meaningful icon — assign role="img"
<button>
<i class="icon-search" role="img" aria-label="Search"></i>
</button>
The defer attribute is invalid on an inline <script>, because it only affects scripts that are loaded from an external file through the src attribute.
defer tells the browser to download an external script in parallel with parsing and run it after the document has finished parsing. An inline script has no external file to download, so there is nothing to defer and the browser runs it as soon as it is parsed. Because the attribute has no defined effect here, the validator flags it.
This usually happens when a template or a performance plugin adds defer to every <script> on the page, including the inline ones. The same applies to async, which is likewise only valid together with src.
Invalid example
<script defer>
console.log("Hello");
</script>
Valid example
Remove the defer attribute from the inline script:
<script>
console.log("Hello");
</script>
If you need the code to run after the document is parsed, move it into an external file and defer that instead:
<script src="app.js" defer></script>
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