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
composer create-project statamic/statamic, create a collection with an entry
- Go to the collection listing, open a row's action menu, click Delete
- 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.
Bug description
ui/Modal/Modal.vuerenders 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
No
role="dialog", norole="alertdialog", noaria-modal, noaria-labelledbypointing 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.vuecollections/DeleteEntryConfirmation.vuecollections/DeleteLocalizationConfirmation.vuenavigation/RemovePageConfirmation.vueui/Listing/Presets.vue(delete view)actions/ConfirmableAction.vueMeasured 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'sDialogContentdirectly 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
composer create-project statamic/statamic, create a collection with an entrySuggested fix
Give
Modal.vuethe same treatment the command palette already has:role="dialog"(oralertdialogfor confirmations),aria-modal="true",aria-labelledbybound to the existingdata-ui-modal-titleelement, and the page behind markedinert.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
Installation
Fresh statamic/statamic site via CLI
Additional details
Found during a WCAG 2.1 AA audit of a vanilla
composer create-project statamic/statamicinstall 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 atv6.31.0.