This accessibility report identifies a moderate-severity violation of WCAG 1.3.6 (Identify Purpose) where multiple informational message blocks utilize the same contentinfo landmark role without unique identifiers. When a page contains multiple landmarks of the same type, screen reader users cannot easily distinguish between them unless each is given a unique name via aria-label, aria-labelledby, or a title attribute. In this case, the .messages--info containers lack the necessary programmatic labeling to help users understand their specific purpose relative to other landmarks on the page. To resolve this, the Drupal message templates should be updated to provide unique labels for each message block—or more appropriately, the contentinfo role (reserved for the page footer) should be removed entirely in favor of ARIA live region roles like status or alert, which are better suited for system notifications.

Note: AI was used to create the scripts that created this scanner and the corresponding report.

In Messages section of Theming Tools. This is a pretty extreme example, but it isn't uncommon for a page to have multiple messages. How do we ensure that this can be done without introducing errors.

Status messages.

Pattern ID: DRU-338C31F8
Rule: axe-core - landmark-unique
Axe Rule URL: https://dequeuniversity.com/rules/axe/4.11/landmark-unique
Severity: Medium (axe impact: moderate)
WCAG SC: 1.3.6 - Identify Purpose (Level A)
Frequency: 2 of 452 pages (0%)
Selector: .messages--info
XPath: //[contains(@class,"messages--info")]
*Parent Context:
N/A

Affected URLs (full list):

Conditions:

  • admin (dark desktop, dark mobile, light desktop, light mobile), claro (dark desktop, dark mobile, light desktop, light mobile)

HTML Snippet

<div class="messages__wrapper">
                      
      <div role="contentinfo" aria-labelledby="message-status-title" class="messages-list__item messages messages--status">
                  <div class="messages__header">
                          <h2 id="message-status-title" class="messages__title">
                Status message
              </h2>
                      </div>
                <div class="messages__content">
                      A status message
                  </div>
        <button type="button" class="button button--dismiss js-message-button-hide" title="Hide" data-once="gin-messages-dismiss">
          <span class="icon-close"></span>
          Hide
        </button>
      </div>
                                  
...
                                  
      <div role="contentinfo" aria-labelledby="message-warning-title" class="messages-list__item messages messages--warning">
                  <div class="messages__header">
                          <h2 id="message-warning-title" class="messages__title">
                Warning message
              </h2>
                      </div>
                <div class="messages__content">
                      A warning message
                  </div>
        <button type="button" class="button button--dismiss js-message-button-hide" title="Hide" data-once="gin-messages-dismiss">
          <span class="icon-close"></span>
          Hide
        </button>
      </div>

Description

Fix any of the following:
The landmark must have a unique aria-label, aria-labelledby, or title to make landmarks distinguishable

Steps to Reproduce

  1. Go to https://drupal-core.ddev.site/message
  2. Install the theming_tools and form_style modules
  3. Use the matching context from Conditions: admin (dark desktop, dark mobile, light desktop, light mobile), claro (dark desktop, dark mobile, light desktop, light mobile)
  4. Open browser DevTools and run axe.run() in the Console.
  5. Confirm rule landmark-unique on selector .messages--info.

Expected Behaviour

Element and interaction meet the mapped WCAG success criterion.

Actual Behaviour

Fix any of the following:
The landmark must have a unique aria-label, aria-labelledby, or title to make landmarks distinguishable

Impact

users with disabilities

Suggested Fix

See axe documentation.

Additional References

Testing Environment

Tracking IDs

  • Pattern ID: DRU-338C31F8
  • Instance IDs: INS-72820C3E, INS-C1AF88AE

Issue fork drupal-3587678

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mgifford created an issue. See original summary.

mgifford’s picture

Results of playwright axe scan from https://github.com/mgifford/drupal-core/blob/main/reports/PATTERN-REPORT...

Theme and style elements explicitly exposed using:
https://www.drupal.org/project/theming_tools
https://www.drupal.org/project/form_style

There are efforts to put in unique identifiers that will hopefully make this easier to search for and to find if there are issues that re-emerge.

It is an attempt to follow the best practices defined here:
https://mgifford.github.io/ACCESSIBILITY.md/examples/ACCESSIBILITY_BUG_R...

mgifford’s picture

Issue summary: View changes
mgifford’s picture

Title: Ensure landmarks are unique » Ensure landmarks are unique - Status Messages
Issue summary: View changes
Issue tags: +theming tools
mgifford’s picture

Issue summary: View changes
StatusFileSize
new57.5 KB
mgifford’s picture

A real-world location where duplicate messages show up.
https://www.drupal.org/project/drupal/issues/3587676

jurgenhaas made their first commit to this issue’s fork.

jurgenhaas’s picture

Status: Active » Needs review

Status messages currently render with role="contentinfo" on each message-type wrapper. The contentinfo landmark is reserved for the page footer, so emitting it on every message block produces two violations that share the same root cause:

The proposed resolution in the issue summary already points at the correct fix: drop contentinfo and use ARIA live region roles instead. Status messages are notifications, not page-level landmarks. The existing Drupal.theme.message() function in core/misc/message.js already uses this mapping for messages added by JavaScript, so the Twig output now matches the JS output.

So, here is what I changed, but not just for default_admin: replace role="contentinfo" on the outer message wrapper with:

  • role="alert" for error and warning types
  • role="status" for all other types

The inner <div role="alert"> wrapper that previously surrounded only the content of error messages is removed. The outer element is now itself a live region, so the duplicate inner role is no longer needed. The Olivero template loses the inner role="alert" on .messages__container for the same reason.

The Umami template already used the correct pattern. Its docstring is refreshed to match.

Some extra notes:

  • BC consideration: themes or tests that select status messages via [role="contentinfo"] will need to update to [role="status"] or [role="alert"]
  • The Olivero template previously wrapped error content in role="alert". Errors now expose role="alert" on the outer wrapper. The visually-hidden heading is now inside the live region, so screen readers announce "Error message" before the content
  • The change aligns Twig-rendered messages with Drupal.theme.message() for messages added via Drupal.Message().add()
mgifford’s picture

Issue summary: View changes
mgifford’s picture

Issue summary: View changes
mgifford’s picture

Issue summary: View changes
dharizza’s picture

Status: Needs review » Reviewed & tested by the community

I ran some tests with Axe in Chrome and the issues "Contentinfo landmark should not be contained in another landmark" and "Document should not have more than one contentinfo landmark" are now gone. I also tested this manually and things look fine.