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.
MIME types (also called media types) follow a strict type/subtype structure as defined by IETF standards. For example, image/png has the type image and the subtype png. The value "favicon" doesn't follow this format — it has no slash and no subtype — so the validator reports "Subtype missing." This is a common mistake that happens when developers confuse the purpose of the icon (a favicon) with the format of the file (an image in a specific encoding).
While most browsers are forgiving and will still display the favicon even with an invalid type value, using incorrect MIME types can cause issues. Some browsers or tools may ignore the <link> element entirely if the type doesn't match a recognized format. It also hurts standards compliance and makes your HTML less predictable across different environments.
The correct MIME type depends on the file format of your favicon:
.icofiles:image/x-icon(orimage/vnd.microsoft.icon).pngfiles:image/png.svgfiles:image/svg+xml.giffiles:image/gif
It's also worth noting that the type attribute on <link rel="icon"> is entirely optional. If omitted, the browser will determine the file type from the server's Content-Type header or by inspecting the file itself. Removing it is a perfectly valid fix.
Examples
❌ Invalid: using "favicon" as the type
<link rel="icon" href="/favicon.png" type="favicon">
The value "favicon" is not a valid MIME type, triggering the validation error.
✅ Fixed: using the correct MIME type for a PNG favicon
<link rel="icon" href="/favicon.png" type="image/png">
✅ Fixed: using the correct MIME type for an ICO favicon
<link rel="icon" href="/favicon.ico" type="image/x-icon">
✅ Fixed: using the correct MIME type for an SVG favicon
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
✅ Fixed: omitting the type attribute entirely
<link rel="icon" href="/favicon.png">
Since the type attribute is optional for <link rel="icon">, removing it avoids the error and lets the browser detect the format automatically. This is often the simplest and most maintainable approach.
The action attribute tells the browser where to send form data when the form is submitted. According to the WHATWG HTML living standard, if the action attribute is specified, its value must be a valid non-empty URL potentially surrounded by spaces. An empty string ("") does not satisfy this requirement, which is why the W3C validator flags it.
While some developers use action="" intending the form to submit to the current page's URL, this approach is non-conforming HTML. Browsers do typically interpret an empty action as "submit to the current URL," but relying on this behavior is unnecessary since simply omitting the action attribute achieves the same result in a standards-compliant way. When no action attribute is present, the form submits to the URL of the page containing the form — this is the defined default behavior per the HTML specification.
This issue matters for several reasons:
- Standards compliance: Non-conforming HTML can lead to unexpected behavior across different browsers or future browser versions.
- Maintainability: Using the correct approach makes your intent clearer to other developers. Omitting
actionexplicitly signals "submit to the current page," whileaction=""looks like a mistake or a placeholder that was never filled in. - Tooling: Build tools, linters, and automated testing pipelines that rely on valid HTML may flag or break on this error.
How to Fix
You have two options:
- Remove the
actionattribute if you want the form to submit to the current page URL. - Provide a valid URL if the form should submit to a specific endpoint.
Examples
❌ Invalid: Empty action attribute
<form action="" method="post">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
This triggers the error: Bad value "" for attribute "action" on element "form": Must be non-empty.
✅ Fixed: Remove the action attribute
If you want the form to submit to the current page, simply omit action:
<form method="post">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
✅ Fixed: Provide a valid URL
If the form should submit to a specific endpoint, supply the URL:
<form action="/subscribe" method="post">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
✅ Fixed: Use a hash as a placeholder for JavaScript-handled forms
If your form is entirely handled by JavaScript and should not navigate anywhere, you can use a # as the action or, preferably, omit action and prevent submission in your script:
<form method="post" id="js-form">
<label for="query">Search:</label>
<input type="text" id="query" name="query">
<button type="submit">Search</button>
</form>
document.getElementById('js-form').addEventListener('submit', function(e) {
e.preventDefault();
// Handle form data with JavaScript
});
In this case, omitting action is the cleanest solution. The JavaScript preventDefault() call stops the browser from actually submitting the form, so the action value is never used.
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 aria-activedescendant attribute tells assistive technologies which child element within a composite widget — such as a combobox, listbox, or autocomplete dropdown — is currently "active" or focused. Instead of moving actual DOM focus to each option, the parent element (like an input) retains focus while aria-activedescendant points to the visually highlighted option by referencing its id. This allows screen readers to announce the active option without disrupting keyboard interaction on the input.
When aria-activedescendant is set to an empty string (""), it creates an invalid state. The HTML and ARIA specifications require that any ID reference attribute either contains a valid, non-empty ID token or is omitted altogether. An empty string is not a valid ID, so the W3C validator flags this as an error: Bad value "" for attribute "aria-activedescendant" on element "input": An ID must not be the empty string.
This problem commonly occurs in JavaScript-driven widgets where aria-activedescendant is cleared by setting it to "" when no option is highlighted — for example, when a dropdown closes or the user clears their selection. While the developer's intent is correct (indicating that nothing is active), the implementation is wrong.
Why this matters
- Accessibility: Screen readers may behave unpredictably when encountering an empty ID reference. Some may silently ignore it, while others may announce errors or fail to convey widget state correctly.
- Standards compliance: The ARIA specification explicitly requires ID reference values to be non-empty strings that match an existing element's
id. - Browser consistency: Browsers handle invalid ARIA attributes inconsistently, which can lead to different experiences across platforms and assistive technologies.
How to fix it
- Remove the attribute when no descendant is active. Use
removeAttribute('aria-activedescendant')in JavaScript instead of setting it to an empty string. - Set a valid ID when a descendant becomes active, pointing to the
idof the currently highlighted or selected option. - Never render the attribute in HTML with an empty value. If your framework or templating engine conditionally renders attributes, ensure it omits the attribute entirely rather than outputting
aria-activedescendant="".
Examples
Incorrect: empty string value
This triggers the W3C validation error because the attribute value is an empty string.
<input type="text" role="combobox" aria-activedescendant="" />
Correct: attribute omitted when no option is active
When nothing is active, simply leave the attribute off.
<input type="text" role="combobox" aria-expanded="false" />
Correct: valid ID reference when an option is active
When a user highlights an option, set aria-activedescendant to that option's id.
<div role="combobox">
<input
type="text"
role="combobox"
aria-expanded="true"
aria-controls="suggestions"
aria-activedescendant="option2" />
<ul id="suggestions" role="listbox">
<li id="option1" role="option">Apple</li>
<li id="option2" role="option" aria-selected="true">Banana</li>
<li id="option3" role="option">Cherry</li>
</ul>
</div>
Correct: managing the attribute dynamically with JavaScript
The key fix in JavaScript is using removeAttribute instead of setting the value to an empty string.
<div role="combobox">
<input
id="search"
type="text"
role="combobox"
aria-expanded="true"
aria-controls="results" />
<ul id="results" role="listbox">
<li id="result1" role="option">First result</li>
<li id="result2" role="option">Second result</li>
</ul>
</div>
<script>
const input = document.getElementById('search');
function setActiveOption(optionId) {
if (optionId) {
input.setAttribute('aria-activedescendant', optionId);
} else {
// Remove the attribute instead of setting it to ""
input.removeAttribute('aria-activedescendant');
}
}
</script>
In summary, always ensure aria-activedescendant either points to a real, non-empty id or is removed from the element. Never set it to an empty string.
The aria-controls attribute establishes a programmatic relationship between an interactive element (like a button or link) and the content it controls. Assistive technologies such as screen readers use this relationship to help users navigate between a control and the region it affects—for example, a button that toggles a panel or a tab that switches visible content. When the attribute is present but empty (aria-controls=""or aria-controls=" "), it signals a broken relationship: the browser and assistive technology expect a target element but find nothing.
This issue commonly occurs when aria-controls is added by a JavaScript framework or template before the target element's id is known, leaving behind an empty placeholder. It can also happen when a CMS or component library outputs the attribute unconditionally, even when no controlled region exists.
Why this matters
- Accessibility: Screen readers may announce that a control is associated with another element, but an empty reference leads nowhere. This creates a confusing or misleading experience for users who rely on assistive technology.
- Standards compliance: The HTML specification defines
aria-controlsas an IDREFS attribute, meaning its value must contain one or more space-separated tokens, each matching theidof an element in the same document. An empty value is invalid per both the WAI-ARIA specification and the HTML standard. - Maintainability: Empty or placeholder ARIA attributes are a sign of incomplete implementation. They can mask bugs in dynamic UIs where the controlled element was supposed to be rendered but wasn't, or where an
idwas accidentally removed during refactoring.
How to fix it
- If the element controls another region, set
aria-controlsto theidof that region. Make sure the target element exists in the DOM and has a uniqueid. - If the element doesn't control anything, remove the
aria-controlsattribute entirely. An absent attribute is always better than an empty one. - For dynamically rendered content, only add
aria-controlsafter the target element and itsidare present in the DOM. If using a framework, conditionally render the attribute. - Keep references in sync. If you rename or remove an
id, update everyaria-controlsthat references it.
Examples
Invalid: empty aria-controls
This triggers the validator error because the attribute value contains no id reference.
<a href="#" aria-controls="">Toggle details</a>
Invalid: whitespace-only value
A value with only spaces is also invalid—IDREFS requires at least one non-whitespace token.
<button type="button" aria-controls=" ">Open menu</button>
Fixed: provide a valid id reference
Point aria-controls to the id of the element being controlled.
<button type="button" aria-controls="details-panel" aria-expanded="false">
Toggle details
</button>
<div id="details-panel" hidden>
<p>Here are some additional details.</p>
</div>
Fixed: controlling multiple regions
You can reference multiple id values separated by spaces. Each id must correspond to an element in the document.
<button type="button" aria-controls="filters results">
Show filters and results
</button>
<section id="filters" hidden>
<p>Filter options...</p>
</section>
<section id="results" hidden>
<p>Search results...</p>
</section>
Fixed: remove the attribute when not needed
If the element doesn't actually control another region, simply omit aria-controls.
<a href="/details">View details</a>
Complete document with proper toggle behavior
This example shows a working toggle pattern combining aria-controls with aria-expanded for full accessibility.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>aria-controls Toggle Example</title>
</head>
<body>
<button type="button" aria-controls="info-panel" aria-expanded="false">
Toggle info
</button>
<div id="info-panel" hidden>
<p>Extra information goes here.</p>
</div>
<script>
const btn = document.querySelector('button');
const panel = document.getElementById('info-panel');
btn.addEventListener('click', () => {
const expanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!expanded));
panel.hidden = expanded;
});
</script>
</body>
</html>
Conditional rendering in a framework
When using a templating system or JavaScript framework, only render aria-controls when the target id is available. Here's a conceptual example:
<!-- Good: only add the attribute when targetId has a value -->
<button type="button" aria-controls="sidebar-nav" aria-expanded="false">
Menu
</button>
<nav id="sidebar-nav" hidden>
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
<!-- Bad: template outputs an empty value when targetId is undefined -->
<!-- <button type="button" aria-controls="">Menu</button> -->
In frameworks like React, you can conditionally spread the attribute: use aria-controls={targetId || undefined} so that when the value is empty, the attribute is omitted from the rendered HTML entirely rather than being set to an empty string.
The aria-controls attribute on a button element has an empty value, but it requires at least one valid ID reference.
The aria-controls attribute accepts a space-separated list of id values that point to the elements controlled by the current element. When a screen reader encounters a button with aria-controls, it can offer the user a way to navigate to the controlled element. An empty string ("") is not a valid IDREF, so the W3C validator rejects it.
This often happens when a template or JavaScript framework sets aria-controls before the target element's id is known, or when the value is conditionally generated and falls through to an empty string.
There are two ways to fix this: either set aria-controls to a valid id that exists in the document, or remove the attribute entirely when no target is available. An absent aria-controls attribute is perfectly fine and preferable to an empty one.
HTML examples
Invalid: empty aria-controls
<button aria-controls="" aria-expanded="false">
Toggle menu
</button>
<nav id="main-menu" hidden>
<a href="/about">About</a>
</nav>
Fixed: aria-controls references a valid ID
<button aria-controls="main-menu" aria-expanded="false">
Toggle menu
</button>
<nav id="main-menu" hidden>
<a href="/about">About</a>
</nav>
If the controlled element does not exist yet or the ID is not available, remove the attribute:
<button aria-expanded="false">
Toggle menu
</button>
JavaScript can add aria-controls later, once the target element and its id are present in the DOM.
The aria-controls attribute must contain at least one valid ID reference and cannot be an empty string.
The aria-controls attribute accepts a space-separated list of IDs pointing to elements that are controlled by the current element. When a screen reader encounters aria-controls, it can offer navigation to those referenced elements. An empty string is not a valid IDREF value per the WAI-ARIA specification, so the W3C validator rejects it.
If the element does not currently control anything, remove the aria-controls attribute entirely rather than leaving it empty. Setting it to an empty string does not mean "no value" — it means "invalid value," which can confuse assistive technologies.
If the controlled element is added dynamically (for example, a dropdown that appears after user interaction), add aria-controls through JavaScript at the same time the controlled element appears in the DOM, and remove it when the element is gone.
Examples
Invalid: empty aria-controls
<div aria-controls="" aria-expanded="false">
Toggle panel
</div>
Fixed: remove the attribute when unused
<div aria-expanded="false">
Toggle panel
</div>
Fixed: provide a valid ID when a controlled element exists
<div aria-controls="panel-1" aria-expanded="true">
Toggle panel
</div>
<div id="panel-1">
Panel content here.
</div>
The aria-describedby attribute cannot be an empty string. It must contain at least one valid ID reference, or be removed entirely.
The aria-describedby attribute accepts a space-separated list of id values that point to elements providing a description for the current element. When a screen reader encounters an element with aria-describedby, it reads the text content of each referenced element as additional context.
An empty string ("") is not a valid IDREF value. The W3C validator expects at least one non-whitespace character. This often happens when a template engine or JavaScript outputs an empty variable into the attribute, or when a CMS generates the attribute without a corresponding value.
If no description exists, remove the attribute altogether. An absent aria-describedby is perfectly fine — it simply means the element has no supplementary description. Assistive technologies handle missing attributes gracefully, but an empty one can cause unpredictable behavior.
Examples
Invalid: empty aria-describedby
<a href="/settings" aria-describedby="">Account settings</a>
Fixed: attribute removed
<a href="/settings">Account settings</a>
Fixed: attribute references a valid ID
<a href="/settings" aria-describedby="settings-note">Account settings</a>
<p id="settings-note">Opens your profile and notification preferences.</p>
The aria-describedby attribute cannot be an empty string — it must either contain valid ID references or be removed entirely.
The aria-describedby attribute accepts one or more ID values (separated by spaces) that point to elements providing additional descriptive text for the current element. When a screen reader focuses on the element, it reads the content of the referenced elements to give the user more context.
Setting aria-describedby="" is invalid because the attribute expects at least one valid IDREF — a non-empty string that matches the id of another element in the page. An empty value doesn't reference anything and creates a validation error. If no description is needed, simply omit the attribute altogether.
This commonly happens when a template or JavaScript dynamically sets the attribute but provides an empty fallback value instead of removing the attribute entirely.
Invalid Example
<label for="email">Email</label>
<input type="email" id="email" aria-describedby="">
Fixed Examples
If there is no description to reference, remove the attribute:
<label for="email">Email</label>
<input type="email" id="email">
If a description exists, point to its id:
<label for="email">Email</label>
<input type="email" id="email" aria-describedby="email-hint">
<p id="email-hint">We'll never share your email with anyone.</p>
If you're generating the attribute dynamically, make sure your code removes aria-describedby entirely rather than setting it to an empty string when no hint is available.
The aria-labelledby attribute accepts an IDREFS value — a space-separated list of one or more id values that reference other elements in the document. The validator expects each ID in the list to be non-empty and contain at least one non-whitespace character. When the attribute is set to an empty string (aria-labelledby=""), it violates this constraint and triggers the validation error.
This issue commonly arises in templating systems and JavaScript frameworks where a variable intended to hold an ID reference resolves to an empty string. For example, a template like aria-labelledby="{{ labelId }}" will produce an empty attribute if labelId is undefined or blank.
Why this matters
The aria-labelledby attribute is one of the highest-priority methods for computing an element's accessible name. According to the accessible name computation algorithm, aria-labelledby overrides all other naming sources — including visible text content, aria-label, and the title attribute. When aria-labelledby is present but empty or broken, screen readers may calculate the link's accessible name as empty, effectively making the link invisible or meaningless to assistive technology users. A link with no accessible name is a significant accessibility barrier: users cannot determine where the link goes or what it does.
Beyond accessibility, an empty aria-labelledby also signals invalid HTML according to both the WHATWG HTML living standard and the WAI-ARIA specification, which define the IDREFS type as requiring at least one valid token.
How to fix it
You have several options depending on your situation:
- Reference a valid ID — Point
aria-labelledbyto theidof an existing element whose text content should serve as the link's accessible name. - Remove the attribute and use visible link text — If the link already contains descriptive text,
aria-labelledbyis unnecessary. - Use
aria-labelinstead — For icon-only links where no visible label element exists,aria-labelprovides a concise accessible name directly on the element. - Conditionally render the attribute — In templates, use conditional logic to omit
aria-labelledbyentirely when there's no valid ID to reference, rather than rendering an empty value.
Examples
Invalid: empty aria-labelledby
This triggers the validation error because the attribute value contains no non-whitespace characters.
<a href="/report" aria-labelledby=""></a>
Invalid: whitespace-only aria-labelledby
A value containing only spaces is equally invalid — IDREFS requires at least one actual token.
<a href="/report" aria-labelledby=" "></a>
Fixed: referencing an existing element by id
The aria-labelledby attribute points to a <span> whose text content becomes the link's accessible name.
<a href="/report" aria-labelledby="report-link-text">
<svg aria-hidden="true" viewBox="0 0 16 16"></svg>
</a>
<span id="report-link-text">View report</span>
Fixed: referencing multiple IDs
You can concatenate text from multiple elements by listing their IDs separated by spaces. The accessible name is built by joining the referenced text in order.
<span id="action">Learn more:</span>
<span id="subject">Apples</span>
<a href="/apples" aria-labelledby="action subject">
<svg aria-hidden="true" viewBox="0 0 16 16"></svg>
</a>
In this case, the computed accessible name is "Learn more: Apples".
Fixed: using visible link text instead
When the link already contains descriptive text, no ARIA attribute is needed. This is the simplest and most robust approach.
<a href="/report">View report</a>
Fixed: using aria-label for an icon-only link
When there is no separate visible label element to reference, aria-label provides the accessible name directly.
<a href="/search" aria-label="Search">
<svg aria-hidden="true" viewBox="0 0 16 16"></svg>
</a>
Fixed: conditional rendering in a template
If you're using a templating engine, conditionally include the attribute only when a value exists. The exact syntax varies by framework, but here's the general idea:
<!-- Instead of always rendering the attribute: -->
<!-- <a href="/report" aria-labelledby="{{ labelId }}"> -->
<!-- Only render it when labelId has a value: -->
<!-- <a href="/report" {{#if labelId}}aria-labelledby="{{labelId}}"{{/if}}> -->
This prevents the empty-attribute problem at its source rather than patching it after the fact.
The aria-labelledby attribute is an IDREFS attribute, meaning its value must be a space-separated list of one or more id values that exist in the document. These referenced elements collectively provide the accessible name for the element. When the value is an empty string ("") or contains only whitespace, there are no valid ID references, which violates the IDREFS requirement defined in the WAI-ARIA and HTML specifications.
This issue commonly appears when templating systems or JavaScript frameworks conditionally set aria-labelledby but output an empty string when no label ID is available. It also occurs when developers add the attribute as a placeholder with the intention of filling it in later but forget to do so.
Why this matters
An empty aria-labelledby is problematic for several reasons:
- Accessibility: Screen readers rely on
aria-labelledbyto announce the accessible name of an element. An empty value can cause unpredictable behavior — some screen readers may ignore the SVG entirely, while others may fall back to reading unhelpful content or nothing at all. This leaves users who depend on assistive technology without a meaningful description of the graphic. - Standards compliance: The W3C validator flags this as an error because the HTML specification requires IDREFS attributes to contain at least one non-whitespace character. Shipping invalid HTML can signal broader quality issues and may cause problems in strict parsing environments.
- Maintainability: An empty
aria-labelledbyis ambiguous. It's unclear whether the developer intended the SVG to be decorative, forgot to add a reference, or encountered a bug in their templating logic.
How to fix it
Choose the approach that matches your intent:
- Reference a labeling element by ID: If the SVG conveys meaning, add a
<title>element (or another visible text element) inside or near the SVG with a uniqueid, then setaria-labelledbyto thatid. IDs are case-sensitive, so ensure an exact match. - Use
aria-labelinstead: If you want to provide an accessible name directly as a text string without needing a separate element, replacearia-labelledbywitharia-label. - Remove the attribute: If the SVG already has an accessible name through other means (such as visible adjacent text or a
<title>child that doesn't need explicit referencing), simply remove the emptyaria-labelledby. - Mark as decorative: If the SVG is purely decorative and adds no information, remove
aria-labelledbyand addaria-hidden="true"so assistive technology skips it entirely.
When generating aria-labelledby dynamically, ensure your code omits the attribute entirely rather than outputting an empty value when no label ID is available.
Examples
❌ Empty aria-labelledby (triggers the error)
<svg role="img" aria-labelledby="">
<use href="#icon-star"></use>
</svg>
The empty string is not a valid IDREFS value, so the validator reports an error.
✅ Reference a <title> element by ID
<svg role="img" aria-labelledby="star-title">
<title id="star-title">Favorite</title>
<use href="#icon-star"></use>
</svg>
The aria-labelledby points to the <title> element's id, giving the SVG a clear accessible name of "Favorite."
✅ Use aria-label with a text string
<svg role="img" aria-label="Favorite">
<use href="#icon-star"></use>
</svg>
When you don't need to reference another element, aria-label provides the accessible name directly as an attribute value.
✅ Reference multiple labeling elements
<svg role="img" aria-labelledby="star-title star-desc">
<title id="star-title">Favorite</title>
<desc id="star-desc">A five-pointed star icon</desc>
<use href="#icon-star"></use>
</svg>
The aria-labelledby value can include multiple space-separated IDs. The accessible name is constructed by concatenating the text content of the referenced elements in order.
✅ Decorative SVG (no accessible name needed)
<svg aria-hidden="true" focusable="false">
<use href="#icon-decorative-divider"></use>
</svg>
For purely decorative graphics, aria-hidden="true" removes the element from the accessibility tree. Adding focusable="false" prevents the SVG from receiving keyboard focus in older versions of Internet Explorer and Edge.
The aria-valuenow attribute requires a valid numeric value and cannot be an empty string.
The aria-valuenow attribute is used on range-type widgets like progress bars, sliders, and spinbuttons to indicate the current value. It must be a valid floating point number — for example, 0, 50, or 3.5. An empty string ("") is not a number, so the validator rejects it.
If the current value is unknown or indeterminate, you should remove the aria-valuenow attribute entirely rather than setting it to an empty string. This is common with indeterminate progress bars where the completion percentage isn't known yet.
When present, aria-valuenow should fall between aria-valuemin and aria-valuemax. All three attributes work together to communicate the state of a range widget to assistive technologies.
Bad Example
<div role="progressbar"
aria-valuenow=""
aria-valuemin="0"
aria-valuemax="100">
Loading...
</div>
Good Example — Known Value
<div role="progressbar"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100">
50% complete
</div>
Good Example — Indeterminate State
<div role="progressbar"
aria-valuemin="0"
aria-valuemax="100">
Loading...
</div>
In the indeterminate example, omitting aria-valuenow tells assistive technologies that the progress is ongoing but the exact value is not known.
The autocomplete attribute tells the browser whether and how it should autofill a form field. The HTML living standard defines a specific set of allowed values — an empty string is not among them. When the attribute is present but empty, the browser receives ambiguous instructions: you've explicitly declared the attribute, signaling intent, but provided no actual directive. Different browsers may interpret this inconsistently, some treating it as on, others ignoring it, and others falling back to default behavior.
This matters for several reasons:
- Standards compliance: The WHATWG HTML specification requires
autocompleteto contain either the keywordsonoroff, or one or more valid autofill detail tokens. An empty string satisfies none of these. - Accessibility: Autofill helps users with motor impairments or cognitive disabilities complete forms more quickly and accurately. An ambiguous
autocompletevalue can interfere with assistive technologies that rely on these hints. - User experience: Specific autofill tokens like
email,tel,street-address, andcurrent-passwordallow browsers and password managers to suggest the right data for the right field. Using them correctly makes forms faster and easier to complete.
This issue commonly arises when a framework or template engine outputs autocomplete="" as a default, or when a developer intends to disable autocomplete but leaves the value blank instead of using off.
How to fix it
Choose one of these approaches depending on your intent:
- Remove the attribute if you want default browser behavior (the browser decides whether to autofill).
- Use
onto explicitly allow autofill. - Use
offto explicitly discourage autofill (note: browsers may still autofill for login fields regardless). - Use a specific autofill token to tell the browser exactly what kind of data the field expects. This is the most helpful option for users.
Common autofill tokens include: name, given-name, family-name, email, username, new-password, current-password, tel, street-address, postal-code, country, and cc-number. You can find the full list in the WHATWG autofill specification.
Examples
Incorrect: empty autocomplete value
<input type="text" name="username" autocomplete="">
This triggers the validation error because the attribute is present but contains no valid token.
Correct: remove the attribute entirely
If you have no specific autofill preference, simply omit the attribute:
<input type="text" name="username">
Correct: use on or off
Explicitly enable or disable autofill:
<input type="text" name="username" autocomplete="on">
<input type="text" name="search-query" autocomplete="off">
Correct: use specific autofill tokens
Specific tokens give browsers the best hints for filling in the right data. This is the recommended approach for forms that collect personal information:
<form>
<label for="name">Full name</label>
<input type="text" id="name" name="name" autocomplete="name">
<label for="useremail">Email</label>
<input type="email" id="useremail" name="useremail" autocomplete="email">
<label for="phone">Phone</label>
<input type="tel" id="phone" name="phone" autocomplete="tel">
<label for="pwd">Password</label>
<input type="password" id="pwd" name="pwd" autocomplete="current-password">
<button type="submit">Sign in</button>
</form>
Using precise tokens like current-password and email helps password managers and mobile keyboards provide the most relevant suggestions, improving the experience for all users.
The dir attribute on the <div> is set to an empty string. dir is an enumerated attribute that only accepts ltr, rtl, or auto, so an empty value is invalid.
The attribute sets the base text direction of an element's content: ltr for left-to-right scripts such as English, rtl for right-to-left scripts such as Arabic or Hebrew, and auto to let the browser decide from the first strongly typed character. An empty dir="" names none of these, so the browser has nothing to apply.
This normally comes from a template that prints dir="" when the variable feeding it is blank. If the element does not need an explicit direction, remove the attribute and it will inherit the direction of its parent. Otherwise, set one of the three valid values.
Invalid example
<div dir="">Welcome</div>
Valid example
Remove the attribute to inherit the direction from the parent:
<div>Welcome</div>
Or set a valid value. Use auto when the text comes from users and its direction is not known in advance:
<div dir="auto">مرحبا</div>
The dir attribute cannot be an empty string — it must be one of the allowed values: ltr, rtl, or auto.
The dir attribute specifies the text directionality of an element's content. It is a global attribute, meaning it can be used on any HTML element. When you set dir="", the validator rejects it because an empty string doesn't convey any meaningful direction.
If you don't need to specify a direction, simply remove the dir attribute entirely. The element will naturally inherit the directionality from its parent. If you want the browser to determine the direction based on the text content, use dir="auto".
Invalid Example
<span dir="">Some text</span>
Valid Examples
<!-- Remove the attribute to inherit direction from the parent -->
<span>Some text</span>
<!-- Or explicitly set a valid direction -->
<span dir="ltr">Some text</span>
<!-- Or let the browser decide based on content -->
<span dir="auto">Some text</span>
The for attribute on a <label> element tells the browser which form control the label describes. When a user clicks or taps the label, the browser transfers focus to the associated control. For this to work, the value of for must exactly match the id of a form element such as an <input>, <textarea>, <select>, or <button>.
An empty string ("") is not a valid ID according to the HTML specification. The WHATWG HTML standard requires that an id attribute value must contain at least one character and must not contain ASCII whitespace. Since no element can have an empty-string id, a <label> with for="" can never successfully reference anything, making it both invalid markup and a broken association.
Why this matters
Accessibility: Screen readers rely on the for/id pairing to announce what a form control is for. A label with an empty for attribute creates no programmatic association, meaning assistive technology users may not know what a field is asking for. This directly impacts WCAG compliance.
Usability: A properly associated label expands the clickable area of its form control. For example, clicking a label associated with a checkbox will toggle the checkbox. An empty for attribute breaks this behavior.
Standards compliance: The W3C validator flags this because it violates the HTML specification. Keeping markup valid helps ensure consistent behavior across browsers and future-proofs your code.
How to fix
You have three options:
- Set
forto a validid: Give the associated form control a uniqueidand reference it in the label'sforattribute. - Remove
forand use implicit association: Wrap the form control inside the<label>element. This creates an implicit association without needingfororidat all. - Remove the
forattribute: If the label is purely decorative or not meant to be associated with a control, simply remove the emptyforattribute.
Examples
❌ Empty for attribute (triggers the error)
<label for="">Username:</label>
<input type="text" name="username">
The label has no meaningful association with the input because for="" is not a valid reference.
✅ Fix: Use a valid for/id pair
<label for="username">Username:</label>
<input type="text" id="username" name="username">
The for="username" now matches id="username" on the input, creating an explicit association.
✅ Fix: Use implicit association by nesting
<label>
Username:
<input type="text" name="username">
</label>
Wrapping the input inside the <label> creates an implicit association. No for or id attributes are needed.
❌ Multiple labels with empty for attributes
<form>
<label for="">Email:</label>
<input type="email" name="email">
<label for="">Subscribe to newsletter</label>
<input type="checkbox" name="subscribe">
</form>
✅ Fixed with proper associations
<form>
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<label for="subscribe">Subscribe to newsletter</label>
<input type="checkbox" id="subscribe" name="subscribe">
</form>
Each id must be unique within the document, and each for attribute must reference exactly one id. If your labels are generated by a framework or CMS with empty for values, check the template or component configuration to ensure proper id values are being output.
In a URL, the # character has a special role: it acts as the delimiter that separates the main URL from the fragment identifier. The fragment typically points to a specific section or element within the target document, often corresponding to an element's id attribute. Because # serves this reserved purpose, it cannot appear more than once in its raw form within a URL. When the validator encounters something like ##pricing or section#one#two, it flags the extra # characters as illegal.
This issue usually arises from one of these common scenarios:
- Typos — accidentally typing
##instead of#. - String concatenation bugs — building URLs programmatically where a
#is included both in the base URL and prepended to the fragment value. - Copy-paste errors — duplicating the
#when copying URLs from browser address bars or other sources. - Literal
#intended in fragment — if you genuinely need a#symbol within the fragment text, it must be percent-encoded as%23.
This matters because browsers may handle malformed URLs inconsistently. Some browsers silently strip the extra #, while others may fail to navigate to the intended fragment. Malformed URLs also cause problems for assistive technologies, web crawlers, and any tooling that parses links. Keeping your URLs well-formed ensures predictable behavior across all user agents and complies with the URL Standard and HTML specification.
Examples
Incorrect: duplicate # in the URL
The double ## makes the fragment identifier invalid:
<a href="https://example.com/faqs##pricing">Pricing</a>
Correct: single # delimiter
Remove the extra # so that pricing is the fragment:
<a href="https://example.com/faqs#pricing">Pricing</a>
Incorrect: extra # inside the fragment
Here, the fragment portion overview#details contains a raw #, which is not allowed:
<a href="/docs#overview#details">Details</a>
Correct: percent-encode the literal #
If you truly need a # as part of the fragment text, encode it as %23:
<a href="/docs#overview%23details">Details</a>
In most cases though, this pattern suggests the URL structure should be rethought. A cleaner approach is to link directly to the intended fragment:
<a href="/docs#details">Details</a>
Incorrect: programmatic concatenation error
A common bug in templates or JavaScript is prepending # when the variable already includes it:
<!-- If defined as defined as fragment = "#pricing", this produces a double ## -->
<a href="https://example.com/faqs#pricing">Pricing</a>
Correct: ensure only one # is present
Make sure either the base URL or the fragment variable includes the #, but not both:
<a href="https://example.com/faqs#pricing">Pricing</a>
Fragment-only links
Fragment-only links (links to sections within the same page) follow the same rule — only one #:
<!-- Incorrect -->
<a href="##contact">Contact Us</a>
<!-- Correct -->
<a href="#contact">Contact Us</a>
A | (pipe) character in the fragment portion of a URL must be percent-encoded as %7C to be valid.
The href attribute on an <a> element must contain a valid URL. According to the URL specification, certain characters — including the pipe | — are not permitted as literal characters in a URL fragment (the part after #). The W3C validator flags this because the browser may handle it inconsistently, and it violates the URL standard.
To fix it, replace every | in the URL with its percent-encoded equivalent: %7C.
HTML Examples
❌ Invalid: literal pipe in fragment
<a href="https://example.com/page#section|one">Link</a>
✅ Valid: percent-encoded pipe in fragment
<a href="https://example.com/page#section%7Cone">Link</a>
If you control the target page, consider redesigning the fragment identifiers to avoid special characters altogether — for example, using hyphens or underscores like #section-one instead.
A URL fragment identifier is the part of a URL that follows the # character. It typically points to an element on the page that has a matching id attribute. According to the URL specification, certain characters — including spaces — are not allowed to appear literally in a URL. When the W3C HTML Validator encounters a raw space in a fragment, it reports this as an illegal character.
This issue matters for several reasons. Browsers may handle unescaped spaces in fragments inconsistently, leading to broken in-page navigation. Screen readers and other assistive technologies rely on well-formed URLs to navigate users to the correct section of a page. Additionally, spaces in id attributes are themselves invalid in HTML — the id attribute must not contain any ASCII whitespace characters. So the root cause often involves two separate violations: an invalid id and an invalid fragment URL.
The best approach is to use hyphens (-) or underscores (_) instead of spaces in your id values, then match the fragment accordingly. This produces clean, readable, and shareable URLs (e.g., page.html#contact-info instead of page.html#contact%20info). If you're working with a CMS or build tool that auto-generates id values with spaces, configure it to produce hyphen-separated, lowercase identifiers instead.
If you absolutely cannot change the id values (e.g., they're generated by a third-party system), you can percent-encode the spaces as %20 in the href. This satisfies URL syntax rules, but note that an id containing spaces is still invalid HTML on its own. Fixing the id is always the preferred solution.
Examples
Invalid: space in fragment and id
This triggers the validator error because href="#My Section" contains an unescaped space. The id="My Section" is also invalid HTML since id values cannot contain spaces.
<a href="#My Section">Go to section</a>
<h2 id="My Section">My Section</h2>
Fixed: hyphen-separated fragment and id
Replace spaces with hyphens in both the id and the fragment. This is the cleanest and most widely recommended approach.
<a href="#my-section">Go to section</a>
<h2 id="my-section">My Section</h2>
Fixed: underscore-separated fragment and id
Underscores work equally well if you prefer that convention.
<a href="#my_section">Go to section</a>
<h2 id="my_section">My Section</h2>
Alternative: percent-encoding the space
Encoding the space as %20 resolves the fragment URL error, but the id with a space is still invalid HTML. Use this only as a last resort when you cannot control the id values.
<a href="#My%20Section">Go to section</a>
<!-- Note: this id is still invalid HTML due to the space -->
<h2 id="My Section">My Section</h2>
Full valid document
A complete example demonstrating multiple in-page links with properly formatted fragments and id values:
<!doctype html>
<html lang="en">
<head>
<title>Page sections</title>
</head>
<body>
<nav>
<ul>
<li><a href="#getting-started">Getting Started</a></li>
<li><a href="#advanced-usage">Advanced Usage</a></li>
<li><a href="#frequently-asked-questions">FAQ</a></li>
</ul>
</nav>
<h2 id="getting-started">Getting Started</h2>
<p>Introduction content here.</p>
<h2 id="advanced-usage">Advanced Usage</h2>
<p>Advanced content here.</p>
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<p>FAQ content here.</p>
</body>
</html>
URLs follow strict syntax rules defined by RFC 3986. Only a specific set of characters are allowed directly in a URL path segment — these include letters, digits, hyphens (-), periods (.), underscores (_), and tildes (~), along with a handful of sub-delimiters like !, $, &, ', (, ), *, +, ,, ;, and =. Any character outside this set — including spaces, angle brackets (< >), curly braces ({ }), pipe characters (|), backslashes (\), carets (^), and backticks (`) — must be percent-encoded.
Percent-encoding replaces the character with a % sign followed by its two-digit hexadecimal ASCII code. For example:
| Character | Percent-encoded |
|---|---|
| (space) | %20 |
{ | %7B |
} | %7D |
| ` | ` |
< | %3C |
> | %3E |
^ | %5E |
This validation error matters for several reasons. First, browsers may handle illegal characters inconsistently — some may silently encode them, while others may break the link or navigate to an unexpected destination. Second, tools that parse HTML (screen readers, search engine crawlers, link checkers) rely on well-formed URLs and may fail or behave unpredictably when they encounter illegal characters. Third, standards compliance ensures your HTML works reliably across all environments.
Common causes of this error include:
- Copying and pasting URLs from documents or emails that contain unencoded spaces or special characters.
- Template variables or placeholders left in
hrefvalues (e.g.,{{url}}). - File paths with spaces used directly as URLs without encoding.
- Non-ASCII characters in URLs that haven't been properly encoded.
Examples
❌ Space in the URL path
<a href="/my page/about us.html">About Us</a>
✅ Spaces percent-encoded as %20
<a href="/my%20page/about%20us.html">About Us</a>
❌ Curly braces from a template placeholder left in the markup
<a href="/products/{{product-id}}/details">View Details</a>
✅ Curly braces replaced with an actual value
<a href="/products/42/details">View Details</a>
❌ Pipe character in the path
<a href="/search/color|size">Filter Results</a>
✅ Pipe character percent-encoded as %7C
<a href="/search/color%7Csize">Filter Results</a>
❌ Angle brackets in the URL
<a href="/page/<section>">Go to Section</a>
✅ Angle brackets percent-encoded
<a href="/page/%3Csection%3E">Go to Section</a>
How to Fix
- Identify the illegal character from the validator's error message — it typically tells you exactly which character is problematic.
- Replace it with the correct percent-encoded equivalent using the table above or a URL encoder tool.
- If the URL contains template syntax (like
{{...}}), make sure your templating engine processes it before the HTML is served to the browser. The raw template syntax should never appear in the final rendered HTML. - Consider renaming files and directories to avoid spaces and special characters altogether — this is the cleanest long-term solution.
If you're generating URLs programmatically, use built-in encoding functions like JavaScript's encodeURIComponent() or PHP's rawurlencode() to ensure all special characters are properly escaped before inserting them into href attributes.
The href attribute on an <a> element contains a literal pipe character (|), which is not a valid character in a URL according to RFC 3986.
URLs have a defined set of allowed characters. The pipe character (|) is not among them. While most browsers will silently encode the pipe and follow the link anyway, the HTML specification requires that href values contain valid URLs. Characters outside the allowed set must be percent-encoded before being placed in the markup.
The percent-encoded form of | is %7C. Replace every literal | in the URL with %7C to produce a valid href value.
HTML examples
Invalid
<a href="https://example.com/search?fields=name|date|status">Search</a>
Valid
<a href="https://example.com/search?fields=name%7Cdate%7Cstatus">Search</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.
The W3C HTML Validator checks that URLs in href attributes conform to the URL standard (defined by WHATWG). While square brackets are permitted in the host component of a URL (to support IPv6 addresses like [::1]), they are not valid unescaped characters in the query string — the part of the URL that comes after the ?. When the validator encounters a literal [ or ] in the query portion, it flags it as an illegal character.
This issue commonly arises when working with APIs or server-side frameworks that use square brackets in query parameters to represent arrays or nested data structures. For example, PHP-style query strings like ?filter[name]=foo or ?ids[]=1&ids[]=2 contain brackets that must be encoded for valid HTML.
Why this matters
- Standards compliance: The WHATWG URL Standard explicitly lists square brackets among the characters that must be percent-encoded in query strings. Invalid URLs cause W3C validation failures.
- Browser behavior: While most modern browsers are forgiving and will often handle unescaped brackets correctly, relying on this lenient parsing is fragile. Some HTTP clients, proxies, or intermediary servers may reject or mangle URLs with illegal characters.
- Interoperability: Encoded URLs are safer when copied, shared, or processed by tools like link checkers, web scrapers, or email clients that may perform strict URL parsing.
How to fix it
Replace every literal square bracket in the query string with its percent-encoded form:
| Character | Percent-encoded |
|---|---|
[ | %5B |
] | %5D |
If you're generating URLs dynamically in a server-side language or JavaScript, use the appropriate encoding function (e.g., encodeURIComponent() in JavaScript, urlencode() in PHP, or urllib.parse.quote() in Python) to handle this automatically.
Examples
Incorrect: literal brackets in the query string
<a href="https://example.com/search?filter[status]=active">Active items</a>
This triggers the validation error because [ and ] appear unescaped in the query.
Correct: percent-encoded brackets
<a href="https://example.com/search?filter%5Bstatus%5D=active">Active items</a>
Replacing [ with %5B and ] with %5D resolves the error. The server receiving this request will decode the values back to filter[status]=active.
Incorrect: array-style parameters with brackets
<a href="/api/items?ids[]=1&ids[]=2&ids[]=3">Load items</a>
Correct: array-style parameters encoded
<a href="/api/items?ids%5B%5D=1&ids%5B%5D=2&ids%5B%5D=3">Load items</a>
Note that in addition to encoding the brackets, the & characters in HTML attributes should be written as & for fully valid markup.
Incorrect: brackets in a simple value
<a href="search.html?q=[value]">Search</a>
Correct: encoded brackets in a simple value
<a href="search.html?q=%5Bvalue%5D">Search</a>
Note on brackets in the host (valid use)
Square brackets are valid in the host portion of a URL for IPv6 addresses. The following does not trigger the error:
<a href="http://[::1]:8080/page">IPv6 localhost</a>
The validator only flags brackets that appear in the query string or other parts of the URL where they are not permitted.
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 yourhrefvalues. - 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 strict syntax rules defined by RFC 3986. Within the path segment of a URL, only a specific set of characters is allowed: unreserved characters (letters, digits, -, ., _, ~), percent-encoded characters (like %20), and certain reserved sub-delimiters. When the W3C validator encounters a character outside this allowed set in a <link> element's href attribute, it flags the error.
Common causes of this issue include:
- Template placeholders left in the URL, such as
{{variable}}or${path}, where curly braces and dollar signs haven't been resolved or encoded. - Spaces in file paths, such as
href="styles/my file.css"instead of using%20or renaming the file. - Copy-paste errors that introduce invisible or special Unicode characters.
- Backslashes (
\) used instead of forward slashes (/), which is a common mistake on Windows systems. - Unencoded query-like characters placed in the path portion of the URL.
This matters because browsers may interpret malformed URLs inconsistently. A URL that works in one browser might fail in another. Additionally, invalid URLs can break resource loading, cause accessibility issues when assistive technologies try to process the document, and lead to unexpected behavior with proxies, CDNs, or other intermediaries that strictly parse URLs.
To fix the issue, inspect the href value reported in the error and either:
- Remove the illegal character if it was included by mistake.
- Percent-encode the character if it must be part of the URL (e.g., a space becomes
%20, a pipe|becomes%7C). - Rename the referenced file or directory to avoid special characters altogether (the simplest and most reliable approach).
Examples
Incorrect: Space in the path
<link rel="stylesheet" href="styles/my styles.css">
The space character is not allowed in a URL path segment. The validator will flag this as an illegal character.
Fixed: Percent-encode the space
<link rel="stylesheet" href="styles/my%20styles.css">
Better fix: Rename the file to avoid spaces
<link rel="stylesheet" href="styles/my-styles.css">
Incorrect: Template placeholder left unresolved
<link rel="stylesheet" href="styles/{{theme}}/main.css">
Curly braces { and } are not valid in URL path segments. This commonly happens with server-side or client-side templating syntax that wasn't processed before the HTML was served.
Fixed: Use a valid resolved path
<link rel="stylesheet" href="styles/dark/main.css">
Incorrect: Backslash used as path separator
<link rel="stylesheet" href="styles\main.css">
Backslashes are not valid URL characters. URLs always use forward slashes.
Fixed: Use forward slashes
<link rel="stylesheet" href="styles/main.css">
Incorrect: Pipe character in the URL
<link rel="stylesheet" href="styles/font|icon.css">
Fixed: Percent-encode the pipe character
<link rel="stylesheet" href="styles/font%7Cicon.css">
Full valid document example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Webpage</title>
<link rel="stylesheet" href="styles/main.css">
<link rel="icon" href="images/favicon.ico">
</head>
<body>
<h1>Welcome to my webpage!</h1>
<p>Here is some content.</p>
</body>
</html>
When in doubt, run your URL through a URL encoder or validator separately to confirm all characters are legal. As a general best practice, stick to lowercase letters, digits, hyphens, and forward slashes in your file and directory names—this avoids encoding issues entirely and makes your URLs clean and predictable.
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