HTML Guides for tab
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 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.
URLs must conform to the URL Living Standard, which forbids tab characters (U+0009) within the host/domain portion of a URL. While some browsers may silently strip tabs and still navigate to the intended destination, this behavior is not guaranteed and should not be relied upon.
This issue typically arises from one of the following scenarios:
- Copy-paste errors: Copying a URL from a document, email, or spreadsheet that inadvertently includes tab characters.
- Template or build tool formatting: A templating engine or code generator inserting whitespace (including tabs) into a URL string, especially when the URL is constructed across multiple lines.
- Manual typos: Accidentally pressing the Tab key while editing an href value, particularly in editors that don’t visualize whitespace.
Tab characters are invisible in most code editors by default, which makes this error frustrating to track down. Enabling “show whitespace” or “show invisible characters” in your editor can help you spot the offending character.
Why this matters
- Standards compliance: The HTML specification requires that href values contain valid URLs. A tab in the domain makes the URL syntactically invalid.
- Accessibility: Screen readers and assistive technologies parse href values to announce link destinations. An invalid URL can lead to confusing or broken announcements.
- Reliability: While major browsers tend to be forgiving and strip tabs before resolving URLs, some HTTP clients, crawlers, or older browsers may not. This can cause broken links in unexpected contexts like RSS readers, email clients, or web scrapers.
How to fix it
- Enable visible whitespace in your editor to locate tab characters.
- Search for tab characters in your href values. In many editors, you can use a regex search for \t within attribute values.
- Remove the tab characters so the URL is a clean, continuous string with no embedded whitespace.
- If URLs are dynamically generated, inspect the code that builds them to ensure no tabs or other whitespace are concatenated into the domain.
Examples
❌ Incorrect: Tab character in the domain
In the example below, a tab character is embedded within the domain name (represented here as 	 for visibility, though in source code it would be an actual invisible tab):
<!-- The tab between "example" and ".com" causes the error -->
<a href="https://example .com/page">Visit Example</a>
Note: The tab character between example and .com is invisible in most editors but triggers the validation error.
✅ Correct: Clean URL with no whitespace
<a href="https://example.com/page">Visit Example</a>
❌ Incorrect: Tab introduced by multi-line URL construction in a template
This can happen when a URL is broken across lines in a template and tabs are used for indentation:
<a href="https://
example.com/page">Visit Example</a>
✅ Correct: URL on a single line with no embedded whitespace
<a href="https://example.com/page">Visit Example</a>
Tip: Finding hidden tabs
If you’re having trouble locating the tab character, try pasting your href value into a tool that reveals character codes, or run a quick check in your browser’s developer console:
// Check for tabs in a URL string
const url = document.querySelector('a').getAttribute('href');
console.log(url.includes('\t')); // true if a tab is present
console.log(JSON.stringify(url)); // shows \t explicitly in the output
The HTML specification requires that URL attributes like src contain valid URLs. While browsers are generally forgiving and may strip or ignore whitespace characters in URLs, including tabs (\t), newlines (\n), or carriage returns (\r) inside a src value is technically invalid. These characters are not legal within a URL according to both the HTML and URL standards.
Beyond standards compliance, there are practical reasons to fix this:
- Unpredictable behavior: Different browsers may handle embedded whitespace differently. Some might silently strip it, while others could encode it as %0A or %0D, leading to broken image requests.
- Debugging difficulty: Invisible characters make URLs fail silently. The path looks correct in your editor, but the actual HTTP request goes to a different (malformed) URL.
- Accessibility and tooling: Screen readers, crawlers, and other tools that parse your HTML may not handle malformed URLs gracefully, potentially causing images to be skipped or reported as broken.
Common Causes
This issue typically appears in a few scenarios:
- Multi-line attributes in templates: When a URL is built dynamically in a template engine (e.g., PHP, Jinja, Handlebars), line breaks in the template code can leak into the rendered attribute value.
- Copy-paste errors: Copying a URL from a document, email, or another source may include hidden line breaks or tab characters.
- Code formatting: Some developers or auto-formatters may break long attribute values across multiple lines, inadvertently injecting newlines into the value.
- String concatenation: Building URLs through string concatenation in server-side code can introduce whitespace if variables contain trailing newlines.
Examples
Incorrect: Newline inside src value
The newline before the closing quote makes this an invalid URL:
<img src="images/photo.jpg
" alt="A scenic photo">
Incorrect: Tab character in src value
A tab character embedded in the middle of the URL path:
<img src="images/ photo.jpg" alt="A scenic photo">
Incorrect: Multi-line URL from template output
This can happen when a template engine outputs a URL with leading or trailing whitespace:
<img src="
https://example.com/images/photo.jpg
" alt="A scenic photo">
Correct: Clean, single-line src value
The URL is a single continuous string with no tabs, newlines, or carriage returns:
<img src="images/photo.jpg" alt="A scenic photo">
Correct: Long URL kept on one line
Even if the URL is long, it must remain on a single line without breaks:
<img src="https://cdn.example.com/assets/images/2024/photos/scenic-landscape-full-resolution.jpg" alt="A scenic photo">
How to Fix It
- Inspect the attribute value: Open the raw HTML source (not the rendered page) and look at the src value character by character. Use your editor’s “show whitespace” or “show invisible characters” feature to reveal hidden tabs and line breaks.
- Remove all whitespace inside the URL: Ensure the entire URL is on a single line with no tabs, newlines, or carriage returns anywhere between the opening and closing quotes.
- Check your templates: If the URL is generated dynamically, trim the output. Most languages offer a trim() or strip() function that removes leading and trailing whitespace including newlines.
- Lint your HTML: Use the W3C validator or an HTML linter in your build pipeline to catch these issues automatically before deployment.
Ready to validate your sites?
Start your free trial today.