Cross-origin iframe: review manually
frames/cross-origin
- WCAG success criterion
- 4.1.2 (level A)
- Outcome class
- Manual. A tool cannot decide this check. The audit only points a person at it; it is never counted as automatically verified.
- Area
- Accessibility, Technical Robustness
- Default severity
- medium
- axe-core rule
frame-tested
Why we flag this
Embedded content (maps, videos, consent widgets) may be inaccessible to screen reader users.
Iframe content is unreachable via CDP — automatic rule checking is not possible.
What the rule checks
Scope. Every perceivable <iframe>/<frame> (not hidden, aria-hidden, role none/presentation, display:none, visibility:hidden, or 1px or smaller) in the top document, its open shadow roots included.
Expectation. One UNTESTED note per perceivable frame whose document is cross-origin to the page (Rendering::frame_reachable: the frame's contentDocument cannot be read, measured by accessibility::probes), located at the frame. Same-origin and srcdoc frames, and frames that are not perceivable (hidden, aria-hidden, display:none, at most 1px in either dimension), get no note. A frame inserted by script or inside a shadow root is part of the page and is judged the same way. Never a pass or a violation. A page whose scripts break the probe records the rule as not run (CapabilityMissing).
Why a person has to decide. The rule only flags embedded content from another origin for a human: whether that content meets WCAG is outside the page's own markup and is never concluded automatically.
Source. WCAG 2.2 SC 4.1.2 (and every SC for embedded content); axe-core frame-tested; HTML §7.1 origin
Detection: shared; needs dom, geometry, frames.
Failing example
The check flags this for review. Corpus case content_frames_cross_origin, element iframe#booking. The page may contain further findings for other rules.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Room booking</title>
<style>
nav a, .skip-link { display: inline-block; min-width: 44px; min-height: 44px; }
</style>
</head>
<body>
<a class="skip-link" href="#main">Skip to main content</a>
<header>
<p>City Library</p>
<nav aria-label="Main"><a href="/" aria-current="page">Home</a> <a href="/catalogue">Catalogue</a> <a href="/visit">Visit</a></nav>
</header>
<main id="main">
<h1>Book a study room</h1>
<iframe id="booking" title="Room booking" src="{{CROSS_ORIGIN}}/booking" width="400" height="200"></iframe>
<p>Our location, as a small marker:</p>
<iframe id="small-map" title="Location map" src="{{CROSS_ORIGIN}}/map" width="2" height="2" style="border: 0"></iframe>
<p>Visits are counted anonymously.</p>
<iframe id="pixel" title="Visitor counter" src="{{CROSS_ORIGIN}}/pixel" width="1" height="1" style="border: 0"></iframe>
<iframe id="collapsed" title="Chat" src="{{CROSS_ORIGIN}}/chat" style="display: none"></iframe>
<iframe id="silent" title="Analytics" src="{{CROSS_ORIGIN}}/analytics" width="300" height="100" aria-hidden="true" tabindex="-1"></iframe>
<iframe id="local-help" title="Booking help" width="400" height="120" srcdoc="<!DOCTYPE html><html lang="en"><head><title>Help</title></head><body><p>Rooms can be booked for up to two hours.</p></body></html>"></iframe>
<booking-widget></booking-widget>
<div id="late-frame"></div>
<script>
var crossOrigin = '{{CROSS_ORIGIN}}';
customElements.define('booking-widget', class extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }).innerHTML =
'<iframe id="shadow-calendar" title="Room calendar" src="' + crossOrigin + '/calendar" width="300" height="150"></iframe>';
}
});
document.getElementById('late-frame').innerHTML =
'<iframe id="late-reviews" title="Reviews" src="' + crossOrigin + '/reviews" width="300" height="150"></iframe>';
</script>
</main>
<footer><p>City Library, 1 Market Square</p></footer>
</body>
</html>This page is generated from the rule catalogue and the rule contracts; see therule overview for how the outcome classes are derived.