Skip to content

NGSTACK-1015 Improve navigation accessibility and UX#270

Open
jakubraus wants to merge 2 commits into
netgen:masterfrom
jakubraus:NGSTACK-1015-navigation-improvements
Open

NGSTACK-1015 Improve navigation accessibility and UX#270
jakubraus wants to merge 2 commits into
netgen:masterfrom
jakubraus:NGSTACK-1015-navigation-improvements

Conversation

@jakubraus
Copy link
Copy Markdown
Contributor

@jakubraus jakubraus commented Jan 23, 2026

This one is huge, sorry. The main purpose was to avoid opening menus on hover, have a consistent click/tap interaction, use proper elements for the submenu triggers, with proper ARIA attributes to comply with the best accessibility practices. Found few more issues along the way as usual.

Accessibility Improvements

  • Submenu triggers: Changed from <i> to semantic <button> elements with proper ARIA attributes (aria-haspopup, aria-expanded, aria-controls)
  • Submenu lists: Added aria-hidden state management and unique IDs for ARIA references
  • Screen reader text: Standardized to Bootstrap's .visually-hidden class (replaced deprecated .sr-only and custom .tt which was useless anyway)
  • Keyboard support: Submenu triggers are now focusable with keyboard, submenus can be opened with Space/Enter and closed with Escape key
  • Mobile nav: Added aria-hidden toggle when navigation opens/closes, on page load set to true

UX Enhancements

  • Submenu animations: Added smooth CSS transitions (opacity, transform) for submenu open/close (if not needed, transitions can be simply removed from the styles)
  • Mobile submenus: Improved expand/collapse animation using max-height (utility function setSubmenuMaxHeight in JS)
  • Mobile navigation: When toggled and body is set to position fixed, negative top offset it set to stay visually on the same scroll position, not jumping anywhere
  • Active state: Parent submenus auto-expand on mobile when child item is active

Code Refactoring

  • PageHeader component: Reorganized with better structure, single delegated click handler for submenus
  • Sticky header: Simplified class from site-header-sticky--active to site-header-sticky.scrolled
  • Footer: Added no-triggers class to disable interactive submenu triggers where not needed

Files Changed

  • PageHeader.component.js - Major refactor
  • _navigation.scss - Animation and button trigger styles
  • menu.html.twig - ARIA attributes and button triggers
  • footer.html.twig, search_box.html.twig, forms/theme.html.twig - Screen reader class updates

Notes

  • There is this no-triggers class, which effectively changes the child submenu-trigger to a plain heading for the subitems. This can be useful for the cases where the subitems should be displayed directly, and not in interactive menu (typically in the footer, based on my experience).
  • The isMobile breakpoint in JS has to be kept in sync with the SCSS breakpoint variables, as I noted in the code
  • I adjusted the code for the sticky header, so now the scrolled class is added to site header regardless its relative, fixed, or sticky position. This should be simpler and more universal solution, enabling easy styling if needed (e.g. styling fixed header differently if scrolled). And the BEM syntax wasn't use anywhere else, anyway

@jakubraus
Copy link
Copy Markdown
Contributor Author

Forgot to mention, even though I wouldn't recommend to use the split buttons (link AND submenu trigger), they are supported. But the preferred way is always to use either only links, or only submenu triggers, as the split buttons provide horrible UX.

@emodric emodric requested a review from Ljudevit January 27, 2026 12:35
<li{{ knp_menu.attributes(attributes) }}>
{%- if item.uri is not empty and (not matcher.isCurrent(item) or options.currentAsLink) %}
{{ block('linkElement') }}
{%- elseif submenuId is defined %}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using submenuId is defined test, we should define the variable before the if on line 47 ({% set submenuId = null %}) and then check if it is not empty ({% if submenuId is not empty %})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Edi, I just fixed that

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants