Bug description
The licensing alert that opens on first load of a trial or unlicensed site covers the page and swallows every click until dismissed, but it has none of the semantics of a dialog.
Measured on first load of /cp/dashboard:
document.querySelector('[role=dialog],[role=alertdialog]') -> null
containing chain -> div.portal-targets -> div -> body
no role, no aria-modal, no aria-label/labelledby
elements with [inert] -> 0
Tab cycle -> Snooze <-> Manage Licenses only
Escape -> does not dismiss
Focus is moved into the alert, so a screen reader user is dropped into an unnamed two-button loop with no announcement of what happened or that the page behind is blocked. The page behind is not marked inert, so the accessibility tree still exposes it as available while every click is being swallowed. Escape does nothing, which breaks the convention every other overlay in the CP follows.
The command palette in the same codebase gets this right: role="dialog", focus moved inside, rest of the page inert, Escape closes. The licensing alert should use the same primitive.
Source: https://github.com/statamic/cms/blob/v6.31.0/resources/js/components/LicensingAlert.vue, mounted from https://github.com/statamic/cms/blob/v6.31.0/resources/js/pages/layout/Layout.vue
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
- Set
STATAMIC_PRO_ENABLED=true with no license key, so the site is in trial mode
- Create a super user and log in to
/cp
- With the "Licensing Alert" showing, run in the console:
document.querySelector('[role=dialog],[role=alertdialog]') // null
document.querySelectorAll('[inert]').length // 0
- Press Escape. The alert stays open.
Suggested fix
Render it through the same dialog primitive the command palette uses:
role="alertdialog" and aria-modal="true"
aria-labelledby pointing at the "Licensing Alert" heading
- mark the page behind it
inert
- map Escape to Snooze
Additional note, worth flagging separately
Because this overlay is up on first load and is not marked as a dialog, any automated accessibility tooling pointed at a trial Statamic install silently measures the alert instead of the page behind it. The first pass of the audit that found this returned near-identical results for 48 different CP screens before we realised what was being scanned. Anyone benchmarking Statamic's CP accessibility will get wrong numbers until this is fixed or they know to dismiss it first.
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
The licensing alert that opens on first load of a trial or unlicensed site covers the page and swallows every click until dismissed, but it has none of the semantics of a dialog.
Measured on first load of
/cp/dashboard:Focus is moved into the alert, so a screen reader user is dropped into an unnamed two-button loop with no announcement of what happened or that the page behind is blocked. The page behind is not marked
inert, so the accessibility tree still exposes it as available while every click is being swallowed. Escape does nothing, which breaks the convention every other overlay in the CP follows.The command palette in the same codebase gets this right:
role="dialog", focus moved inside, rest of the page inert, Escape closes. The licensing alert should use the same primitive.Source: https://github.com/statamic/cms/blob/v6.31.0/resources/js/components/LicensingAlert.vue, mounted from https://github.com/statamic/cms/blob/v6.31.0/resources/js/pages/layout/Layout.vue
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/statamicSTATAMIC_PRO_ENABLED=truewith no license key, so the site is in trial mode/cpSuggested fix
Render it through the same dialog primitive the command palette uses:
role="alertdialog"andaria-modal="true"aria-labelledbypointing at the "Licensing Alert" headinginertAdditional note, worth flagging separately
Because this overlay is up on first load and is not marked as a dialog, any automated accessibility tooling pointed at a trial Statamic install silently measures the alert instead of the page behind it. The first pass of the audit that found this returned near-identical results for 48 different CP screens before we realised what was being scanned. Anyone benchmarking Statamic's CP accessibility will get wrong numbers until this is fixed or they know to dismiss it first.
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.