Frames must have an accessible name

Frames embed content such as videos, maps, payment forms and chat windows. Without a name, screen readers announce only "frame", so blind customers cannot tell what is inside or where the payment form is.

Rule id
frame-title
Source
axe-core 4.13.0
Impact
Serious
Fix tier
AUTO
on Shopify, Shopware, WooCommerce and Magento
Status
runs

What the check does

Ensure <iframe> and <frame> elements have an accessible name

Every <iframe> and <frame> element is in scope unless it is hidden from screen readers or has a negative tabindex. Hidden means, for example, display: none or aria-hidden="true"; a 1 by 1 pixel or fully transparent frame is still tested. The rule does not look inside the frame, so third-party embeds are tested too, once on the page. By default Validiscan’s scanner blocks known chat, review, analytics and advertising hosts, so frames their scripts would add never appear.

A frame passes with a title, an aria-label or an aria-labelledby that yields text, or a presentational role. Whitespace alone counts as empty, and the <title> of the document inside the frame plays no part. role="none" or role="presentation" counts only when the frame has no title attribute at all, even an empty one, since screen readers handle that combination inconsistently.

The rule tests that a name exists, not what it says. Duplicate titles belong to frame-title-unique, whose failures axe-core reports as needs review; Validiscan’s scanner makes no findings from those.

How to fix it

Add a short, descriptive title attribute to every <iframe>, for example title="Product video", title="Store location map" or title="Card payment". For embeds added by apps or page builders, set the title in the embed code or the app settings.

WCAG success criteria

EN 301 549 clauses

The EN 301 549 clause of each criterion, by version of the standard
WCAG criterion Version 3.2.1 Version 4.1.1
4.1.2 9.4.1.2 9.4.1.2

Where a recipe fixes it

Shopify: recipe iframe-title

Show the before and after (1)
Before and after Example data

locales/en.default.json Changes to locales/en.default.json: 2 added, 1 removed

Changes to locales/en.default.json: 2 added, 1 removed
Line before Line after Change Code
34 34 "total_reviews": "{{ count }} reviews",
35 35 "link_messages": {
36 36 "new_window": "Opens in a new window."
37 Removed }
37 Added },
38 Added "embed_map": "Map"
38 39 },
39 40 "sections": {
40 41 "header": {

sections/contact-form.liquid Changes to sections/contact-form.liquid: 1 added, 1 removed

Changes to sections/contact-form.liquid: 1 added, 1 removed
Line before Line after Change Code
8 8 {%- endcapture -%}
9 9 <div class="contact-section page-width page-width--narrow">
10 10 <h2 class="contact-section__heading">{{ section.settings.map_heading | escape }}</h2>
11 Removed <iframe class="store-map" srcdoc="{{ map_document | escape }}" width="600" height="360"></iframe>
11 Added <iframe class="store-map" srcdoc="{{ map_document | escape }}" width="600" height="360" title="{{ 'accessibility.embed_map' | t | escape }}"></iframe>
12 12
13 13 <h2 class="contact-section__heading">{{ 'templates.contact.form.title' | t }}</h2>
14 14 {%- form 'contact', id: 'ContactForm', class: 'contact__form' %}

Shopware: recipe iframe-title

Show the before and after (1)
titles the contact map frame through the snippets; frame-title no longer fires Example data

Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 5 added, 0 removed

Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 5 added, 0 removed
Line before Line after Change Code
193 193 "notFoundText": "Die gesuchte Seite existiert nicht oder wurde verschoben.",
194 194 "backToHome": "Zurück zur Startseite",
195 195 "searchInstead": "Unsere Kaffees durchsuchen"
196 Added },
197 Added "a11y": {
198 Added "iframe": {
199 Added "map": "Karte"
200 Added }
196 201 }
197 202 }

Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 5 added, 0 removed

Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 5 added, 0 removed
Line before Line after Change Code
193 193 "notFoundText": "The page you are looking for does not exist or has moved.",
194 194 "backToHome": "Back to the homepage",
195 195 "searchInstead": "Search our coffees"
196 Added },
197 Added "a11y": {
198 Added "iframe": {
199 Added "map": "Map"
200 Added }
196 201 }
197 202 }

Resources/views/storefront/block/cms-block-map.html.twig Changes to Resources/views/storefront/block/cms-block-map.html.twig: 2 added, 1 removed

Changes to Resources/views/storefront/block/cms-block-map.html.twig: 2 added, 1 removed
Line before Line after Change Code
11 11 <iframe class="cms-map-frame"
12 12 src="{{ url('/widgets/cms/map') }}"
13 13 width="1280"
14 Removed height="360"></iframe>
14 Added height="360"
15 Added title="{{ 'a11y.iframe.map'|trans|striptags }}"></iframe>
15 16 </div>
16 17 <p class="cms-map-address">{{ shop.company.street }}, {{ shop.company.zip }} {{ shop.company.city }}</p>
17 18 </div>

WooCommerce: recipe iframe-title

Show the before and after (1)
titles the store map iframe of the contact page from the embedded document Example data

page-contact.php Changes to page-contact.php: 1 added, 1 removed

Changes to page-contact.php: 1 added, 1 removed
Line before Line after Change Code
81 81
82 82 <div class="kb-store-map">
83 83 <?php // VALIDISCAN-SEED frame-title: the map iframe has no title. ?>
84 Removed <iframe src="<?php echo esc_url( get_stylesheet_directory_uri() . '/assets/embeds/store-map.html' ); ?>" width="600" height="320" loading="lazy"></iframe>
84 Added <iframe src="<?php echo esc_url( get_stylesheet_directory_uri() . '/assets/embeds/store-map.html' ); ?>" width="600" height="320" loading="lazy" title="<?php esc_attr_e( 'Map: Kestrel & Birch workshop, Hamburg', 'kestrel-birch' ); ?>"></iframe>
85 85 </div>
86 86 </div>
87 87 </article>

Magento: recipe iframe-title

Show the before and after (1)
titles the store map on the contact page and validates Example data

app/design/frontend/Validiscan/fixture/Magento_Contact/templates/form.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Contact/templates/form.phtml: 1 added, 1 removed

Changes to app/design/frontend/Validiscan/fixture/Magento_Contact/templates/form.phtml: 1 added, 1 removed
Line before Line after Change Code
48 48 <p>Nordwind Outfitters (fixture)<br>Musterstraße 12<br>10115 Berlin, Germany</p>
49 49 <p><?= $escaper->escapeHtml(__('Monday to Saturday, 10:00 to 19:00')) ?></p>
50 50 <!-- VALIDISCAN-SEEDED frame-title: the store map iframe has no title -->
51 Removed <iframe src="<?= $escaper->escapeUrl($block->getBaseUrl() . 'media/wysiwyg/store-map.html') ?>" width="480" height="260" loading="lazy"></iframe>
51 Added <iframe src="<?= $escaper->escapeUrl($block->getBaseUrl() . 'media/wysiwyg/store-map.html') ?>" width="480" height="260" loading="lazy" title="<?= $escaper->escapeHtmlAttr(__('Store location map')) ?>"></iframe>
52 52 </div>
53 53 </div>

Start with a free check of your shop.

Send your shop’s address. Validiscan scans it and replies with every failure the scan can prove, with the page and the code.

Request a free check

Or write to info@validiscan.com.

What the free check covers, and what it leaves out
You send
your shop’s address
Pages scanned
up to 15
You get
every failure the scan can prove, with the page and the code
On your shop
public pages only; no login, nothing bought, no forms sent
What it is not
a full audit

Write to Validiscan

For a free check, give the address of your shop and its platform.

Validiscan uses what you send to answer you. Privacy notice