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 W3C HTML Validator raises this error when it encounters a backslash character (\) inside the href attribute of an anchor element. According to the WHATWG URL Standard, backslashes are not valid characters in URL scheme data. URLs are defined with forward slashes (/) as delimiters — this applies to all parts of a URL, including the scheme, authority, path, query, and fragment.
This issue most commonly occurs when developers copy file paths from Windows operating systems, where backslashes are the default path separator (e.g., C:\Users\Documents\file.html), and paste them directly into HTML markup. It can also happen when server-side code generates URLs using OS-level path functions that produce backslashes on Windows.
Why this matters
- Standards compliance: The WHATWG URL Standard explicitly forbids backslashes in scheme data. Validators flag this as an error because the resulting URL is malformed.
- Cross-browser inconsistency: While some browsers may silently correct backslashes to forward slashes, this behavior is not guaranteed across all browsers or versions. Relying on browser error correction leads to fragile code.
- Broken links: Certain browsers, HTTP clients, or intermediary servers may not auto-correct the backslash, causing the link to fail entirely — resulting in 404 errors or unexpected navigation.
- Security concerns: Backslashes in URLs can be exploited in certain attack vectors like open redirects or path traversal attacks. Using well-formed URLs reduces the attack surface.
How to fix it
- Replace all backslashes (\) with forward slashes (/) in your href values.
- Check for URL generation in server-side code. If your application builds URLs programmatically, ensure it uses forward slashes regardless of the host operating system.
- Use relative or absolute URLs consistently. Whether the URL is relative (images/photo.jpg) or absolute (https://example.com/images/photo.jpg), always use forward slashes.
Examples
Incorrect: backslashes in a relative path
<a href="pages\about\team.html">Meet the Team</a>
Correct: forward slashes in a relative path
<a href="pages/about/team.html">Meet the Team</a>
Incorrect: backslashes in an absolute URL
<a href="https://example.com\blog\2024\post.html">Read the Post</a>
Correct: forward slashes in an absolute URL
<a href="https://example.com/blog/2024/post.html">Read the Post</a>
Incorrect: Windows file path pasted directly
<a href="assets\downloads\report.pdf">Download Report</a>
Correct: converted to a proper relative URL
<a href="assets/downloads/report.pdf">Download Report</a>
Incorrect: mixed slashes
Sometimes a URL contains a mix of forward and backslashes, which also triggers this error:
<a href="https://example.com/images\photos\sunset.jpg">View Photo</a>
Correct: all forward slashes
<a href="https://example.com/images/photos/sunset.jpg">View Photo</a>
A quick way to audit your HTML files is to search for \ within any href (or src, action, etc.) attribute values and replace them with /. In most code editors, you can use find-and-replace scoped to attribute values to handle this efficiently.
URLs follow a strict syntax defined by RFC 3986 and the URL Living Standard. Only a specific set of characters are allowed to appear unencoded in a URL. Curly braces ({ and }) are among the characters that fall outside this permitted set. When the W3C validator encounters a raw { or } in an href value, it reports the error: Bad value for attribute “href” on element “a”: Illegal character in fragment.
This issue commonly arises in a few scenarios:
- Server-side or client-side template placeholders left unresolved in the rendered HTML (e.g., {id}, {{slug}}).
- URLs copied from API documentation that use curly braces to indicate variable segments (e.g., /users/{userId}/posts).
- Malformed or auto-generated URLs where curly braces were included by mistake.
Why This Matters
Standards compliance: The HTML specification requires that href values conform to valid URL syntax. Curly braces violate this requirement, producing invalid HTML.
Browser inconsistency: While most modern browsers will attempt to handle URLs with illegal characters by silently encoding them, this behavior is not guaranteed across all browsers or versions. Relying on browser error correction can lead to unpredictable results.
Accessibility and interoperability: Assistive technologies, web crawlers, and other tools that parse HTML may not handle illegal URL characters gracefully. Invalid URLs can break link extraction, bookmarking, and sharing functionality.
Debugging difficulty: If curly braces appear in your rendered HTML, it often signals that a template variable was not properly resolved, which may point to a deeper bug in your application logic.
How to Fix It
The fix depends on why the curly braces are there:
-
If the curly braces are literal characters that should be part of the URL, replace them with their percent-encoded equivalents: { becomes %7B and } becomes %7D.
-
If the curly braces are template placeholders (e.g., {userId}), ensure your server-side or client-side code resolves them to actual values before the HTML is sent to the browser. The rendered HTML should never contain unresolved template variables.
-
If the curly braces were included by mistake, simply remove them.
Examples
Incorrect: Raw curly braces in href
<a href="https://example.com/api/users/{userId}/profile">View Profile</a>
This triggers the validation error because { and } are illegal URL characters.
Correct: Percent-encoded curly braces
If the curly braces are meant to be literal parts of the URL:
<a href="https://example.com/api/users/%7BuserId%7D/profile">View Profile</a>
Correct: Resolved template variable
If the curly braces were template placeholders, ensure your templating engine resolves them before rendering. The final HTML should look like:
<a href="https://example.com/api/users/42/profile">View Profile</a>
Incorrect: Curly braces in a fragment identifier
<a href="https://example.com/docs#section-{name}">Jump to Section</a>
Correct: Percent-encoded fragment
<a href="https://example.com/docs#section-%7Bname%7D">Jump to Section</a>
Incorrect: Curly braces in query parameters
<a href="https://example.com/search?filter={active}">Active Items</a>
Correct: Percent-encoded query parameter
<a href="https://example.com/search?filter=%7Bactive%7D">Active Items</a>
Using JavaScript for dynamic URLs
If you need to build URLs dynamically with values that might contain special characters, use encodeURIComponent() in JavaScript rather than inserting raw values into href attributes:
<a id="dynamic-link" href="https://example.com">Dynamic Link</a>
<script>
var value = "{some-value}";
var link = document.getElementById("dynamic-link");
link.href = "https://example.com/path?param=" + encodeURIComponent(value);
</script>
This ensures that any special characters, including curly braces, are automatically percent-encoded in the resulting URL.
A URL can have only one fragment identifier — the portion that comes after the single # symbol. When the browser encounters a # in a URL, it treats everything after it as the fragment. If a second # appears, it becomes an illegal character within that fragment because the fragment portion of a URL does not allow unencoded # characters. This violates the URL specification (WHATWG) and the HTML standard’s requirements for valid URLs in the href attribute.
This matters for several reasons:
- Standards compliance: Browsers may handle malformed URLs inconsistently, leading to unpredictable navigation behavior across different environments.
- Accessibility: Screen readers and assistive technologies rely on well-formed URLs to correctly announce link destinations and allow users to navigate.
- SEO and crawling: Search engine crawlers may fail to follow or index pages with invalid URLs.
The # character is perfectly valid when used exactly once to introduce a fragment. For example, #pricing or /faqs#pricing are fine. The issue arises when a second # appears, such as /faqs#guarantee#pricing, or when # is used in a part of the URL where it isn’t expected.
If you genuinely need a literal # character as part of a path or query string (not as a fragment delimiter), you must percent-encode it as %23.
Examples
❌ Invalid: Multiple # characters in the URL
This triggers the validator error because the fragment (guarantee#pricing) contains an illegal #:
<a href="/faqs#guarantee#pricing">Guarantee and Pricing</a>
✅ Fixed: Use a single fragment identifier
Link to one section at a time with a single #:
<a href="/faqs#guarantee">Guarantee</a>
<a href="/faqs#pricing">Pricing</a>
✅ Fixed: Encode the # if it’s meant as a literal character
If the # is part of the actual path or data (not a fragment delimiter), encode it as %23:
<a href="/faqs%23guarantee#pricing">Pricing</a>
In this case, /faqs%23guarantee is the path (containing a literal #), and pricing is the fragment.
Using fragments correctly in a table of contents
Fragments work well for in-page navigation. Each target element needs a matching id:
<nav>
<ul>
<li><a href="#pricing">Pricing</a></li>
<li><a href="#terms">Terms</a></li>
<li><a href="#guarantee">Guarantee</a></li>
</ul>
</nav>
<h2 id="pricing">Pricing</h2>
<p>All about pricing...</p>
<h2 id="terms">Terms</h2>
<p>You can find our terms at...</p>
<h2 id="guarantee">Guarantee</h2>
<p>We offer a guarantee...</p>
❌ Invalid: # in a query string or path without encoding
Sometimes this error appears when a URL accidentally includes an unencoded # in the wrong place:
<a href="/search?color=#ff0000">Red items</a>
✅ Fixed: Encode the # in the query value
<a href="/search?color=%23ff0000">Red items</a>
Here, %23ff0000 correctly represents the literal string #ff0000 as a query parameter value, rather than being misinterpreted as the start of a fragment.
Common causes
- Copy-pasting URLs from other sources that contain multiple # characters.
- Dynamically generated links where fragment values aren’t properly sanitized.
- Color codes in URLs like #ff0000 that need to be percent-encoded as %23ff0000.
- Concatenation errors in templates where a # is added both in the base URL and the appended fragment.
The fix is always the same: make sure your href contains at most one # used as the fragment delimiter, and percent-encode (%23) any # that is meant as a literal character.
When the browser’s HTML parser encounters a src attribute, it expects the value between the opening and closing quotes to be a valid URL. URLs have strict rules about which characters are permitted — a literal double quote (") is not one of them. If a " character appears inside the URL, the validator flags it as an illegal character in the query string (or other parts of the URL).
This error often doesn’t stem from an intentionally embedded quote in the URL itself. Instead, it’s usually a symptom of malformed HTML around the attribute. Common causes include:
- Stray quotes after the attribute value, such as writing src="https://example.com/script.js"async"" instead of properly separating attributes.
- Accidentally doubled closing quotes, like src="https://example.com/script.js"".
- Boolean attributes given empty-string values incorrectly, such as async"" instead of just async, which causes the parser to interpret the extra quotes as part of the preceding src value.
- Copy-paste errors that introduce smart quotes (" ") or extra quotation marks into the URL.
This matters for several reasons. Malformed src attributes can cause the script to fail to load entirely, breaking functionality on your page. It also violates the HTML specification, which can lead to unpredictable behavior across different browsers as each parser tries to recover from the error differently.
To fix the issue, carefully inspect the <script> tag and ensure that:
- The src attribute value contains only a valid URL with no unescaped " characters.
- The opening and closing quotes around the attribute value are properly balanced.
- Attributes are separated by whitespace — not jammed together.
- Boolean attributes like async and defer are written as bare keywords without values.
If you genuinely need a double quote character in a query string (which is rare), encode it as %22.
Examples
Incorrect — stray quotes between attributes
In this example, the async attribute is malformed as async"", which causes the parser to interpret the extra quotes as part of the src URL:
<script src="https://example.com/js/app.js?ver=3.1" async""></script>
Incorrect — doubled closing quote
Here, an extra " at the end of the src value creates an illegal character in the URL:
<script src="https://example.com/js/app.js?ver=3.1""></script>
Incorrect — smart quotes from copy-paste
Curly/smart quotes copied from a word processor are not valid HTML attribute delimiters and get treated as part of the URL:
<script src="https://example.com/js/app.js?ver=3.1"></script>
Correct — clean attributes with proper quoting
<script src="https://example.com/js/app.js?ver=3.1" async></script>
Correct — boolean attribute written as a bare keyword
Boolean attributes like async and defer don’t need a value. Simply include the attribute name:
<script src="https://example.com/js/app.js?ver=3.1" defer></script>
While async="async" is technically valid per the spec, the cleanest and most common form is the bare attribute. Avoid empty-string patterns like async="" placed without proper spacing, as they can lead to the exact quoting errors described here.
Correct — full valid document
<!DOCTYPE html>
<html lang="en">
<head>
<title>Valid Script Example</title>
<script src="https://example.com/js/app.js?ver=3.1" async></script>
</head>
<body>
<p>Page content here.</p>
</body>
</html>
If you’re generating <script> tags dynamically (through a CMS, template engine, or build tool), check the template source rather than just the rendered output. The stray quotes are often introduced by incorrect string concatenation or escaping logic in the server-side code.
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 src value.
- 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 src value 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
<p>Status: </p>
The character ` (U+E001) is a PUA code point. Without the specific icon font loaded, this renders as a missing glyph. ### Fixed: Using inline SVG with accessible label ```html <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 ```html <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: ```html <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 ```html <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 html <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 html <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 href can 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 the href value. 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 href attributes.
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.
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 /track or track.php, prepend the full origin (e.g., https://example.com/track).
- Remove non-HTTP schemes. Values like mailto:someone@example.com or ftp://example.com/log are not valid for ping.
- 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 http or https URL.
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 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 imagesrcset with one or more image candidate strings.
- Remove the attribute — if you don’t have multiple image sources to preload, remove imagesrcset (and imagesizes) from the <link> element entirely. You can still preload a single image using just the href attribute.
- 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 %G5 or %2 that 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 %2 or %GZ exists, determine the intended character and encode it correctly, or remove the stray %.
- Use proper encoding tools. In JavaScript, use encodeURIComponent() or encodeURI() 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 with tabindex="0" or no tabindex, 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 tabindex attribute 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 the URL / URLSearchParams APIs
- PHP: urlencode() or http_build_query()
- Python: urllib.parse.urlencode() or urllib.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.
The href attribute expects a valid URL, and URLs follow strict syntax rules defined by RFC 3986. Under these rules, spaces are not permitted anywhere in a URL — not in the path, the query string, the fragment, or any other component. When a browser encounters a space in an href, it may attempt to fix the URL by encoding the space automatically, but this behavior is not guaranteed to be consistent across all browsers and contexts. Relying on browsers to silently correct invalid URLs is fragile and can lead to broken links.
This matters for several reasons. First, standards compliance: the W3C validator flags this because the HTML specification requires href values to be valid URLs. Second, interoperability: while most modern browsers handle spaces gracefully on navigation, other consumers of your HTML — such as web crawlers, screen readers, link checkers, and APIs that parse HTML — may not. Third, accessibility: assistive technologies rely on well-formed URLs to correctly announce and follow links. A malformed URL could lead to unexpected behavior for users depending on these tools.
The fix is straightforward: replace every literal space character with %20. This is called percent-encoding (sometimes called URL encoding). The sequence %20 is the hexadecimal representation of the space character (ASCII code 32). In the query string portion of a URL specifically, you may also see + used to represent spaces (as defined by the application/x-www-form-urlencoded format), but %20 is universally valid across all parts of a URL and is the safer choice.
Be aware that spaces can sometimes be hard to spot, especially trailing spaces or spaces introduced by template engines and CMS platforms that concatenate URL parts. If you’re generating URLs dynamically (e.g., in a server-side template or JavaScript), use built-in encoding functions like encodeURIComponent() in JavaScript or urlencode() in PHP rather than manually replacing spaces.
Examples
Incorrect: space in the query string
<a href="search.html?q=my search">Search for 'my search'</a>
The literal space between my and search makes this an invalid URL.
Correct: space replaced with %20
<a href="search.html?q=my%20search">Search for 'my search'</a>
Incorrect: spaces in the path
<a href="/files/my document.pdf">Download the document</a>
Spaces in the path segment are equally invalid.
Correct: spaces in the path encoded
<a href="/files/my%20document.pdf">Download the document</a>
Incorrect: multiple spaces across path and query
<a href="/product catalog/items?name=red shoes&category=on sale">Red Shoes</a>
Correct: all spaces encoded
<a href="/product%20catalog/items?name=red%20shoes&category=on%20sale">Red Shoes</a>
Note that in addition to encoding the spaces, the & in the query string should be written as & in HTML to avoid being interpreted as the start of an HTML entity.
Using JavaScript to encode URLs dynamically
If you’re building URLs in JavaScript, use encodeURIComponent() for individual parameter values or encodeURI() for full URLs:
<script>
const query = "my search";
const url = "search.html?q=" + encodeURIComponent(query);
// Result: "search.html?q=my%20search"
</script>
This approach prevents encoding issues by handling all special characters automatically — not just spaces, but also characters like #, &, =, and others that have special meaning in URLs.
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 href to 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-label if 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 span that supports naming, such as role="img", role="group", role="status", or any other role that allows aria-label.
- Use a different element that already has a semantic role supporting aria-label, such as a button, a, section, or nav.
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>
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 rel attribute defines the relationship between the current document and a linked resource. The HTML specification maintains a set of recognized keyword values for this attribute, and the allowed keywords vary depending on which element the attribute appears on. For example, stylesheet is valid on <link> but not on <a>, while nofollow is valid on <a> and <form> but not on <link>.
When the validator encounters a rel value that isn’t a recognized keyword, it checks whether the value is a valid absolute URL. This is because the HTML specification allows custom link types to be defined using absolute URLs as identifiers (similar to how XML namespaces work). If the value is neither a recognized keyword nor a valid absolute URL, the validator raises this error.
Common causes of this error include:
- Typos in standard keywords — for example, rel="styelsheet" or rel="no-follow" instead of the correct rel="stylesheet" or rel="nofollow".
- Using non-standard or invented values — such as rel="custom" or rel="external", which aren’t part of the HTML specification’s recognized set.
- Using relative URLs as custom link types — for example, rel="my-custom-type" instead of a full URL like rel="https://example.com/my-custom-type".
This matters because browsers and other user agents rely on recognized rel values to determine how to handle linked resources. An unrecognized value will simply be ignored, which could mean your stylesheet doesn’t load, your prefetch hint doesn’t work, or search engines don’t respect your intended link relationship. Using correct values ensures predictable behavior across all browsers and tools.
Examples
Incorrect: Misspelled keyword
<link rel="styleshet" href="main.css">
The validator reports that styleshet is not a recognized keyword and is not an absolute URL.
Correct: Fixed spelling
<link rel="stylesheet" href="main.css">
Incorrect: Non-standard keyword on an anchor
<a href="https://example.com" rel="external">Visit Example</a>
The value external is not a standard rel keyword in the HTML specification, so the validator flags it.
Correct: Using a recognized keyword
<a href="https://example.com" rel="noopener">Visit Example</a>
Incorrect: Relative URL as a custom link type
<link rel="my-custom-rel" href="data.json">
Correct: Absolute URL as a custom link type
If you genuinely need a custom relationship type, provide a full absolute URL:
<link rel="https://example.com/rels/my-custom-rel" href="data.json">
Correct: Common valid rel values
Here are some frequently used standard rel keywords with their appropriate elements:
<!-- Linking a stylesheet -->
<link rel="stylesheet" href="styles.css">
<!-- Linking a favicon -->
<link rel="icon" href="favicon.ico">
<!-- Preloading a resource -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<!-- Telling search engines not to follow a link -->
<a href="https://example.com" rel="nofollow">Sponsored link</a>
<!-- Opening a link safely in a new tab -->
<a href="https://example.com" target="_blank" rel="noopener noreferrer">External site</a>
Multiple rel values
You can specify multiple space-separated rel values. Each one must individually be either a recognized keyword or a valid absolute URL:
<!-- Correct: both values are recognized keywords -->
<a href="https://example.com" target="_blank" rel="noopener noreferrer">External</a>
<!-- Incorrect: "popup" is not a recognized keyword or absolute URL -->
<a href="https://example.com" target="_blank" rel="noopener popup">External</a>
To resolve this error, consult the MDN rel attribute reference for the full list of recognized keywords and which elements support them. If your value isn’t on the list, either replace it with the correct standard keyword or use a complete absolute URL to define your custom link type.
The WAI-ARIA specification defines a strict ownership model for tab-related roles. An element with role="tab" controls the visibility of an associated role="tabpanel" element, and tabs are expected to be grouped within a tablist. This relationship is how assistive technologies like screen readers understand and communicate the tab interface pattern to users — for example, announcing “tab 2 of 4” when focus moves between tabs.
When a tab is not contained in or owned by a tablist, screen readers cannot determine how many tabs exist in the group, which tab is currently selected, or how to navigate between them. This fundamentally breaks the accessibility of the tab interface, making it confusing or unusable for people who rely on assistive technologies.
There are two ways to establish the required relationship:
- Direct containment: Place the role="tab" elements as direct children of the role="tablist" element. This is the most common and straightforward approach.
- Using aria-owns: If the DOM structure prevents direct nesting, add aria-owns to the tablist element with a space-separated list of id values referencing each tab. This tells assistive technologies that the tablist owns those tabs even though they aren’t direct children in the DOM.
Examples
Incorrect: tab outside of a tablist
In this example, the role="tab" buttons are siblings of the tablist rather than children of it, which triggers the validation error.
<div class="tabs">
<div role="tablist" aria-label="Sample Tabs"></div>
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
First Tab
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
Second Tab
</button>
</div>
Correct: tabs as direct children of tablist
The simplest fix is to place the role="tab" elements directly inside the role="tablist" element.
<div class="tabs">
<div role="tablist" aria-label="Sample Tabs">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">
First Tab
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">
Second Tab
</button>
</div>
<div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
<p>Content for the first panel</p>
</div>
<div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden>
<p>Content for the second panel</p>
</div>
</div>
Correct: using aria-owns when DOM nesting isn’t possible
If your layout or framework makes it difficult to nest the tabs directly inside the tablist, you can use aria-owns to establish the relationship programmatically.
<div class="tabs">
<div role="tablist" aria-label="Sample Tabs" aria-owns="tab-1 tab-2"></div>
<div class="tab-wrapper">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">
First Tab
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">
Second Tab
</button>
</div>
<div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
<p>Content for the first panel</p>
</div>
<div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden>
<p>Content for the second panel</p>
</div>
</div>
Additional notes
- Each role="tab" element should use aria-selected to indicate which tab is active ("true") and which are not ("false").
- Use aria-controls on each tab to reference the id of its associated tabpanel.
- Use aria-labelledby on each tabpanel to point back to its controlling tab.
- Set tabindex="0" on the active tab and tabindex="-1" on inactive tabs to support keyboard navigation with arrow keys within the tablist.
- Always include an aria-label or aria-labelledby on the tablist to give it an accessible name.
The preload value of the <link> element’s rel attribute lets you declare fetch requests in the HTML <head>, telling the browser to start loading critical resources early in the page lifecycle—before the main rendering machinery kicks in. This can significantly improve performance by ensuring key assets are available sooner and are less likely to block rendering.
However, a preload hint is incomplete without the as attribute. The as attribute tells the browser what kind of resource is being fetched. This matters for several important reasons:
- Request prioritization: Browsers assign different priorities to different resource types. A stylesheet is typically higher priority than an image. Without as, the browser cannot apply the correct priority, and the preloaded resource may be fetched with a low default priority, undermining the purpose of preloading.
- Content Security Policy (CSP): CSP rules are applied based on resource type (e.g., script-src, style-src). Without knowing the type, the browser cannot enforce the appropriate policy.
- Correct Accept header: The as value determines which Accept header the browser sends with the request. For example, an image request sends a different Accept header than a script request. An incorrect or missing header could lead to unexpected responses from the server.
- Cache matching: When the resource is later requested by a <script>, <link rel="stylesheet">, or other element, the browser needs to match it against the preloaded resource in its cache. Without as, the browser may not recognize the preloaded resource and could fetch it again, resulting in a duplicate request—the opposite of what you intended.
The HTML specification explicitly requires the as attribute when rel="preload" is used, making this a conformance error.
Common as Values
The as attribute accepts a specific set of values. Here are the most commonly used ones:
| Value | Resource Type |
|---|---|
| script | JavaScript files |
| style | CSS stylesheets |
| font | Web fonts |
| image | Images |
| fetch | Resources fetched via fetch() or XMLHttpRequest |
| document | HTML documents (for <iframe>) |
| audio | Audio files |
| video | Video files |
| track | WebVTT subtitle tracks |
| worker | Web workers or shared workers |
Examples
Incorrect: Missing as attribute
This will trigger the validation error because the browser doesn’t know what type of resource is being preloaded:
<link rel="preload" href="/fonts/roboto.woff2">
<link rel="preload" href="/js/app.js">
Correct: as attribute included
Adding the appropriate as value tells the browser exactly what kind of resource to expect:
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/js/app.js" as="script">
<link rel="preload" href="/css/main.css" as="style">
<link rel="preload" href="/images/hero.webp" as="image">
Note on fonts and crossorigin
When preloading fonts, you must also include the crossorigin attribute, even if the font is hosted on the same origin. This is because font fetches are CORS requests by default. Without crossorigin, the preloaded font won’t match the later font request and will be fetched twice:
<!-- Correct: includes both as and crossorigin for fonts -->
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin>
Full document example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Preload Example</title>
<link rel="preload" href="/css/main.css" as="style">
<link rel="preload" href="/js/app.js" as="script">
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
<h1>Hello, world!</h1>
<script src="/js/app.js"></script>
</body>
</html>
Choosing the correct as value is straightforward—just match it to how the resource will ultimately be used on the page. If you’re preloading a stylesheet, use as="style"; if it’s a script, use as="script", and so on.
The ARIA specification defines a strict ownership hierarchy for table-related roles. A columnheader represents a header cell for a column, analogous to a <th> element in native HTML. For assistive technologies like screen readers to correctly announce column headers and associate them with the data cells beneath them, the columnheader must exist within a row context. The required structure is:
- An element with role="table", role="grid", or role="treegrid" serves as the container.
- Inside it, elements with role="rowgroup" (optional) or role="row" organize the rows.
- Each role="row" element contains one or more elements with role="columnheader", role="rowheader", or role="cell".
When a role="columnheader" element is placed directly inside a role="table" or role="grid" container — or any other element that is not role="row" — the validator raises this error. Without the row wrapper, screen readers cannot navigate the table structure properly. Users who rely on assistive technology may hear disjointed content or miss the column headers entirely, making the data table unusable.
The best practice, whenever feasible, is to use native HTML table elements (<table>, <thead>, <tr>, <th>, <td>). These carry implicit ARIA roles and establish the correct ownership relationships automatically, eliminating this entire category of errors. Only use ARIA table roles when you genuinely cannot use native table markup — for example, when building a custom grid widget with non-table elements for layout reasons.
Examples
Incorrect: columnheader not inside a row
In this example, the columnheader elements are direct children of the table container, with no role="row" wrapper:
<div role="table" aria-label="Employees">
<div role="columnheader">Name</div>
<div role="columnheader">Department</div>
<div role="row">
<div role="cell">Alice</div>
<div role="cell">Engineering</div>
</div>
</div>
Correct: columnheader inside a row
Wrapping the column headers in an element with role="row" fixes the issue:
<div role="table" aria-label="Employees">
<div role="row">
<div role="columnheader">Name</div>
<div role="columnheader">Department</div>
</div>
<div role="row">
<div role="cell">Alice</div>
<div role="cell">Engineering</div>
</div>
</div>
Correct: Using rowgroup for additional structure
You can optionally use role="rowgroup" to separate headers from body rows, similar to <thead> and <tbody>. The columnheader elements must still be inside a row:
<div role="table" aria-label="Employees">
<div role="rowgroup">
<div role="row">
<div role="columnheader">Name</div>
<div role="columnheader">Department</div>
</div>
</div>
<div role="rowgroup">
<div role="row">
<div role="cell">Alice</div>
<div role="cell">Engineering</div>
</div>
</div>
</div>
Best practice: Use native table elements
Native HTML tables have built-in semantics that make ARIA roles unnecessary. A <th> inside a <tr> already behaves as a columnheader inside a row:
<table>
<thead>
<tr>
<th>Name</th>
<th>Department</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>Engineering</td>
</tr>
</tbody>
</table>
This approach is simpler, more robust across browsers and assistive technologies, and avoids the risk of ARIA misuse. Reserve ARIA table roles for situations where native table markup is not an option.
In URLs, special characters that aren’t part of the standard allowed set must be represented using percent-encoding (also called URL encoding). This works by replacing the character with a % followed by two hexadecimal digits representing its byte value — for example, a space becomes %20, and an ampersand becomes %26. Because % itself serves as the escape character in this scheme, any bare % that isn’t followed by two hexadecimal digits creates an ambiguous, invalid URL.
When the W3C validator encounters a src attribute containing a % not followed by two valid hex digits, it cannot determine the intended character and reports the error. This issue typically arises in two scenarios:
- A literal percent sign in the URL — for instance, a query parameter like ?width=48% where the % is meant as an actual percent symbol. The % must be encoded as %25.
- Incomplete or corrupted percent-encoding — such as %2 instead of %20, or %GZ where the characters after % aren’t valid hexadecimal digits (only 0–9 and A–F/a–f are valid).
Why this matters
- Browser inconsistency: While many browsers try to handle malformed URLs gracefully, behavior varies. Some browsers may misinterpret the intended resource path, leading to broken images or unexpected requests.
- Standards compliance: The URL Living Standard and RFC 3986 define strict rules for percent-encoding. Invalid URLs violate these standards.
- Reliability: Proxies, CDNs, and server-side software may reject or misroute requests with malformed URLs, causing images to fail to load in certain environments even if they work in your local browser.
How to fix it
- Find every bare % in the URL that isn’t followed by two hexadecimal digits.
- If the % is meant as a literal percent sign, replace it with %25.
- If the % is part of a broken encoding sequence (e.g., %2 or %GH), correct it to the intended two-digit hex code (e.g., %20 for a space).
- Use proper URL encoding functions in your language or framework (e.g., encodeURIComponent() in JavaScript, urlencode() in PHP) when building URLs dynamically, rather than constructing them by hand.
Examples
Literal percent sign not encoded
<!-- ❌ Bad: bare "%" is not followed by two hex digits -->
<img alt="Chart" src="https://example.com/chart.png?scale=50%">
<!-- ✅ Fixed: "%" encoded as "%25" -->
<img alt="Chart" src="https://example.com/chart.png?scale=50%25">
Incomplete percent-encoding sequence
<!-- ❌ Bad: "%2" is incomplete — missing the second hex digit -->
<img alt="Photo" src="https://example.com/my%2photo.jpg">
<!-- ✅ Fixed: use "%20" for a space character -->
<img alt="Photo" src="https://example.com/my%20photo.jpg">
Non-hexadecimal characters after percent sign
<!-- ❌ Bad: "%zz" uses non-hex characters -->
<img alt="Logo" src="https://example.com/logo%zz.png">
<!-- ✅ Fixed: remove the erroneous sequence if it was unintentional -->
<img alt="Logo" src="https://example.com/logo.png">
Multiple encoding issues in one URL
<!-- ❌ Bad: bare "%" in query value and unencoded space -->
<img alt="Report" src="https://example.com/img?label=100%&name=my file.png">
<!-- ✅ Fixed: "%" → "%25", space → "%20" -->
<img alt="Report" src="https://example.com/img?label=100%25&name=my%20file.png">
Building URLs safely with JavaScript
When generating src values in code, use encodeURIComponent() to handle special characters automatically:
const label = "100%";
const name = "my file.png";
const src = `https://example.com/img?label=${encodeURIComponent(label)}&name=${encodeURIComponent(name)}`;
// Result: "https://example.com/img?label=100%25&name=my%20file.png"
This ensures every special character — including % — is correctly percent-encoded without manual effort.
The fragment portion of a URL (everything after the # symbol) follows the same encoding rules as the rest of the URL — literal space characters are not permitted. When a browser encounters a space in a URL fragment, it may try to correct it automatically, but this behavior is not guaranteed across all browsers and contexts. Relying on browser error-correction leads to fragile links that may break unpredictably.
This issue matters for several reasons. First, it produces invalid HTML that fails W3C validation. Second, fragment navigation (jumping to a specific section of a page) may not work correctly if the browser doesn’t auto-correct the space. Third, assistive technologies like screen readers rely on well-formed URLs to announce link destinations accurately. Finally, tools that parse or process HTML programmatically — such as crawlers, link checkers, and content management systems — may misinterpret or reject malformed URLs.
The most common scenario for this error is linking to an id attribute on the same page or another page where the id contains spaces. However, it’s worth noting that id values themselves cannot contain spaces in valid HTML (a space in an id would make it multiple invalid tokens). So if you’re writing both the link and the target, the best fix is often to correct the id itself by using hyphens or underscores instead of spaces.
How to Fix It
There are two main approaches:
- Percent-encode the spaces — Replace each space with %20 in the href value.
- Use space-free identifiers — Change the target id to use hyphens or camelCase, then update the fragment to match.
The second approach is strongly preferred because it fixes the root problem and produces cleaner, more readable markup.
Examples
❌ Invalid: Space in the fragment
<a href="https://example.com/page#some term">Go to section</a>
<a href="#my section">Jump to My Section</a>
✅ Fixed: Percent-encode the space
If you cannot control the target id (e.g., linking to an external page), percent-encode the space:
<a href="https://example.com/page#some%20term">Go to section</a>
✅ Better fix: Use a hyphenated id and matching fragment
When you control both the link and the target, use a space-free id:
<h2 id="my-section">My Section</h2>
<p>Some content here.</p>
<a href="#my-section">Jump to My Section</a>
❌ Invalid: Space in fragment linking to another page
<a href="/docs/getting-started#quick start guide">Quick Start Guide</a>
✅ Fixed: Matching hyphenated id
<!-- On the target page (/docs/getting-started): -->
<h2 id="quick-start-guide">Quick Start Guide</h2>
<!-- On the linking page: -->
<a href="/docs/getting-started#quick-start-guide">Quick Start Guide</a>
A note on id naming conventions
Since fragment identifiers reference id attributes, adopting a consistent id naming convention avoids this issue entirely. Common patterns include:
<!-- Hyphens (most common, used by many static site generators) -->
<section id="getting-started">
<!-- camelCase -->
<section id="gettingStarted">
<!-- Underscores -->
<section id="getting_started">
All three are valid and will never trigger a space-related validation error in your fragment links.
Ready to validate your sites?
Start your free trial today.