WebProCMS ships with WCAG 2.1 AA-compliant navigation out of the box. The header rows, dropdown menus, mobile hamburger panels, and skip link all expose the right ARIA semantics and keyboard behavior automatically — site owners don't have to know what aria-expanded is to get a screen-reader-friendly menu.
What you get automatically
Every header from the design library and every nav menu on a WebProCMS site has these accessibility features built in. No setting to flip, no opt-in:
- Skip-to-content link — the first focusable element on every public page is a "Skip to main content" link. Keyboard users can jump past the header in one Tab. It's visually hidden until focused, so it doesn't clutter the design.
- Real semantic landmarks — every page renders a
<header>,<nav>,<main id="main-content">, and<footer>. Screen readers can jump between landmarks; the skip link targets the main content directly. - Active page indicator — the current page's nav link gets
aria-current="page", so assistive tech announces "current page" when the user lands on it. Visual styling (typically a brand-color highlight) is still applied via the active-item classes. - Active section indicator on parent dropdowns — when the current page is a child link inside a dropdown (manually nested sub-items, dynamic children, or a mega-menu column), the parent dropdown's trigger button gets
aria-current="true"and the same active-item styling. Screen readers announce "current item" on the parent so users know which section they're in even before opening the dropdown. - Dropdown open/closed state — every dropdown trigger has
aria-expandedthat flips betweentrueandfalseas the menu opens. Screen readers announce "expanded" or "collapsed" so users know whether to expect submenu items. aria-controlslinking — each dropdown trigger references the panel it opens. Assistive tech can navigate from the trigger to the panel and back.aria-haspopup— triggers tell AT that a popup will open, not just that a button was pressed.- Decorative icons hidden — chevrons, hamburger/X icons, and other decorative SVGs in the header carry
aria-hidden="true"so screen readers don't read meaningless "image" labels. - Escape closes any open menu — Pressing Escape closes any open dropdown or mobile panel. Focus returns to the trigger button that opened it, so the keyboard user doesn't get stranded.
- Hamburger toggle exposes state — the mobile-menu hamburger announces "expanded" or "collapsed," labels itself "Toggle menu," and links to the panel via
aria-controls.
What this means for your users
| User | Experience |
|---|---|
| Sighted, mouse user | No visible change. The menu looks and feels identical. |
| Keyboard-only user | Tab once → skip link → Tab again → straight to page content. Or Tab through the nav, Enter to open a dropdown, Escape to close it, focus returns to where they were. |
| Screen reader user | Hears "Skip to main content link," can jump there. Nav items announce as "link, current page" for the active route. Dropdowns announce "button, has popup, collapsed" or "expanded." Mobile panel says "Toggle menu, expanded" when open. |
| User with cognitive accessibility needs | Consistent escape-to-close behavior, focus that doesn't disappear when a panel closes, no surprise focus jumps. |
How it works under the hood
Three layers cooperate to deliver this without anyone having to author it per-page:
- The shared nav component (
<x-dl.nav>) handles every dropdown variant — mega menus, dynamic child lists, manually nested sub-items, both accordion and dropdown styles. Each variant emits the right ARIA contract, including escape handling and focus return. - The design-library header rows (68, under
resources/design-library/rows/header/) carry consistent hamburger-button and mobile-panel markup. A migration tool baked the same ARIA attributes into every existing template; new headers inherit the same skeleton. - The public layout (public.blade.php) provides the skip link and
<main>landmark on every page, regardless of which header or footer the site is using.
The active-page link detection uses Laravel's request()->routeIs(...) and URL comparison — so aria-current="page" is applied wherever the menu item references either a named route or a literal URL that matches the current request.
Customizing nav links and active state
Active state still works the same way it always did — the menu builder doesn't require any new fields. If a nav item points to a route that matches the current request, the editor's "active class" styling AND the aria-current="page" ARIA attribute are both applied automatically.
The "active class" textarea on each nav menu (Dashboard → Menus) controls only the visual highlight. The ARIA attribute is always emitted regardless of whether an active class is set.
What's still on the roadmap
These are tracked but not in the box yet:
- Form accessibility audit — distinct from nav. Forms ship with sensible defaults but haven't had a formal WCAG pass.
- Color contrast audit of design-library row defaults — some default class combinations may dip below WCAG AA contrast in certain themes. Site owners can override per-row; the defaults will be audited in a separate pass.
- Public-side conditional rendering — the editor can hide fields conditionally during editing, but the public render path doesn't yet support "show this row only when X." Tracked separately as a follow-up.
For developers extending the system
If you're authoring a new design-library header row or adding a new nav variant, follow the conventions baked into the existing files:
- Hamburger button: emit
x-bind:aria-expanded="mobileOpen ? 'true' : 'false'",aria-controls="dl-mobile-panel",aria-label="Toggle menu", andx-ref="hamburger". Usex-bind:(not the:shorthand) — Blade components like<x-dl.group>PHP-evaluate the:attrshorthand and would treat Alpine variables as undefined constants. - Mobile panel: emit
id="dl-mobile-panel"and@keydown.escape.window="mobileOpen = false; $refs.hamburger?.focus()". That id is also the hook the panel's scroll containment keys off —#dl-mobile-panelin resources/css/public.css bounds the panel to the viewport space below the header and gives it its own scroll area, andbootMobileNavPanel()in resources/js/public.js keeps the--dl-mobile-panel-topmeasurement exact per header. Don't bake amax-h-*oroverflow-*class onto the panel in a row template: a menu longer than the screen would go back to being unreachable, because the header is fixed and the drag scrolls the page behind it. - Decorative SVGs inside the header: emit
aria-hidden="true".
Run tests/Feature/MenuAccessibilityTest.php after any header authoring — the test asserts the full ARIA contract against the rendered home page.
Architecture detail and full audit findings live in docs/menu-accessibility-audit.md (internal — implementation notes, gap analysis, decision rationale).