Navigation and icon-only controls
Name the action, expose menu state, label landmarks, and let the router own current-page state.
A visual hamburger with no programmatic purpose or state
A native button can still be inaccessible when its visual lines provide no useful name or
menu relationship. Multiple unlabeled navigation landmarks are also hard to distinguish;
React Router’s NavLink can already expose the current page.
<button className="hamburger" onClick={toggleMenu}>
<span className="hamburger-line" />
<span className="hamburger-line" />
<span className="hamburger-line" />
</button>
<nav className={`site-nav${menuAperto ? " is-open" : ""}`}>
…
</nav>
User impact
- The control may be announced only as “button,” without its menu purpose.
- Open and closed state is available visually but not to a screen reader.
- Unlabelled navigation landmarks are hard to distinguish when jumping by landmark.
Keep native controls; add only the missing name, state, and relationship
Use a real button with a contextual name, aria-expanded, and a stable
aria-controls target. Give each navigation landmark a distinct name. Do not
duplicate route state when the router already supplies it.
Implementation
React JSX + React Router
const { currentLang, t, setLang } = useLanguage();
const [menuAperto, setMenuAperto] = useState(false);
const toggleMenu = () => setMenuAperto((v) => !v);
const chiudiMenu = () => setMenuAperto(false);
<button
className="hamburger"
type="button"
onClick={toggleMenu}
aria-label={menuAperto ? t().a11y.closeMenu : t().a11y.openMenu}
aria-expanded={menuAperto}
aria-controls="primary-navigation"
>
<span className="hamburger-line" aria-hidden="true"></span>
<span className="hamburger-line" aria-hidden="true"></span>
<span className="hamburger-line" aria-hidden="true"></span>
</button>
<nav
id="primary-navigation"
className={`site-nav${menuAperto ? " is-open" : ""}`}
aria-label={t().a11y.primaryNavigation}
>
<NavLink
to="/collezione"
className={({ isActive }) => (isActive ? "is-active" : "")}
onClick={chiudiMenu}
>
{t().nav.collezione}
</NavLink>
</nav>
React Router’s NavLink supplies aria-current="page" for the active route.
Framework / library
NavLink is imported from react-router-dom. Translation access,
menu state, and handlers are project-local context omitted from this excerpt.
Angular Template + Router
<button
class="hamburger"
type="button"
aria-controls="primary-navigation"
[attr.aria-expanded]="menuAperto()"
[attr.aria-label]="
menuAperto() ? lang.t().nav.closeMenu : lang.t().nav.openMenu
"
(click)="toggleMenu()"
>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
<nav
id="primary-navigation"
[class.is-open]="menuAperto()"
[attr.aria-label]="lang.t().nav.primaryLabel"
>
<a
routerLink="/collezione"
routerLinkActive="is-active"
ariaCurrentWhenActive="page"
(click)="chiudiMenu()"
>{{ lang.t().nav.collezione }}</a>
</nav>
menuAperto = signal(false);
toggleMenu(): void {
this.menuAperto.update((v) => !v);
}
Angular adds current-page semantics through ariaCurrentWhenActive.
Framework / library
routerLink, routerLinkActive, and
ariaCurrentWhenActive are supplied by @angular/router;
ariaCurrentWhenActive is an input of RouterLinkActive.
React vs Angular
Same semantic result Both retain native buttons and links. Only the router syntax differs: React keeps NavLink behavior; Angular opts into ariaCurrentWhenActive.