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
- 4.1.2 Name, Role, Value, level A
EN 301 549 clauses
| 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)
locales/en.default.json 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
| 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)
Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json 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
| 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
| 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)
page-contact.php 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)
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
| 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 checkOr 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