Skip to main content

Documentation

No results found.
Features

Accessible Navigation

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 t...

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-expanded that flips between true and false as the menu opens. Screen readers announce "expanded" or "collapsed" so users know whether to expect submenu items.
  • aria-controls linking — 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:

  1. 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.
  2. 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.
  3. 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.

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", and x-ref="hamburger". Use x-bind: (not the : shorthand) — Blade components like <x-dl.group> PHP-evaluate the :attr shorthand 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-panel in resources/css/public.css bounds the panel to the viewport space below the header and gives it its own scroll area, and bootMobileNavPanel() in resources/js/public.js keeps the --dl-mobile-panel-top measurement exact per header. Don't bake a max-h-* or overflow-* 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).