Skip to content

[6.x] ui/Modal has no dialog role or name, affecting every confirmation modal #15410

Description

@bpmore

Bug description

ui/Modal/Modal.vue renders its content as a plain <div> with no ARIA dialog semantics:

https://github.com/statamic/cms/blob/v6.31.0/resources/js/components/ui/Modal/Modal.vue#L193

<div ref="modalContent" v-if="visible" v-bind="restAttrs" :class="[…]" data-ui-modal-content>

No role="dialog", no role="alertdialog", no aria-modal, no aria-labelledby pointing at the title it already renders two lines below.

Because this is the base modal, every confirmation dialog in the CP inherits it, including the ones guarding destructive actions:

  • ResourceDeleter.vue
  • collections/DeleteEntryConfirmation.vue
  • collections/DeleteLocalizationConfirmation.vue
  • navigation/RemovePageConfirmation.vue
  • ui/Listing/Presets.vue (delete view)
  • actions/ConfirmableAction.vue

Measured on the delete-entry confirmation from /cp/collections/{handle}:

{ "role": null,
  "text": "Delete Are you sure you want to delete this? Cancel Delete",
  "buttons": ["Cancel", "Delete"] }

A screen reader user is given a Cancel/Delete choice with no announcement that a dialog opened, no name for it, and no indication that the page behind is unavailable.

The command palette does get role="dialog", because it uses reka-ui's DialogContent directly rather than this component. So the CP already has the right pattern — the shared modal just doesn't use it.

WCAG 2.1 SC 4.1.2 Name, Role, Value and SC 1.3.1 Info and Relationships (both Level A).

How to reproduce

  1. composer create-project statamic/statamic, create a collection with an entry
  2. Go to the collection listing, open a row's action menu, click Delete
  3. With the confirmation showing, run:
document.querySelector('[role=dialog],[role=alertdialog]')   // null
document.querySelector('[data-ui-modal-content]').getAttribute('role')  // null
document.querySelectorAll('[inert]').length                   // 0

Suggested fix

Give Modal.vue the same treatment the command palette already has: role="dialog" (or alertdialog for confirmations), aria-modal="true", aria-labelledby bound to the existing data-ui-modal-title element, and the page behind marked inert.

Related

#15393 reports the licensing alert as having no dialog semantics. That turns out to be a symptom of this — the licensing alert is one more consumer of the same modal. Fixing this should close both.

Good news

The confirmation itself works: deleting an entry is gated behind an "Are you sure you want to delete this?" step with Cancel and Delete, so WCAG 2.1 SC 3.3.4 Error Prevention passes. This issue is only about the missing semantics.

Environment

Environment
Laravel Version: 13.30.1
PHP Version: 8.4.23
Composer Version: 2.10.2
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: en

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED

Drivers
Broadcasting: log
Cache: file
Database: sqlite
Logs: stack / single
Mail: log
Queue: sync
Session: file

Storage
public/storage: NOT LINKED

Statamic
Addons: 0
License Key: Not set
Sites: 1
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.31.0 PRO

Installation

Fresh statamic/statamic site via CLI

Additional details

Found during a WCAG 2.1 AA audit of a vanilla composer create-project statamic/statamic install with no addons and no custom code. Tested with axe-core 4.x on Chromium 153 at 1440x1000, plus manual keyboard traversal. Source references point at v6.31.0.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions