Capitolo 11
Anatomia di un menu
Un menu offre una scelta, ma ogni scelta ruba un po' di contesto: copre quello che stavi guardando, sposta il fuoco, chiede di ricordare dov'eri. Il mestiere non è aprire un elenco — è offrire le strade tenendo l'utente ancorato a dove si trova.
Il capitolo scorso parlava del singolo controllo; questo allarga lo sguardo a un gesto che sembra banale e invece è tra i più delicati dell’interfaccia: offrire una scelta. Un menu è esattamente questo — un elenco di strade fra cui prendere una. Ma ogni elenco che si apre paga un prezzo nascosto: copre qualcosa, sposta il fuoco, chiede all’utente di ricordare dov’era prima di aprirlo. La domanda del capitolo è tutta qui: come si offre una scelta senza nascondere il contesto?
La risposta non è una sola, perché i menu non sono tutti lo stesso oggetto. C’è il dropdown che cresce attaccato al suo bottone, così l’occhio non perde il filo. C’è il menu del tasto destro, che nasce dove clicchi e non deve mai scappare fuori dallo schermo. C’è la listbox, la scelta che resta visibile mentre la navighi. C’è lo split button, che dà subito l’azione più probabile e nasconde le varianti dietro una freccina. E c’è la command palette, che rovescia tutto: invece di navigare gerarchie, scrivi cosa vuoi. Cinque modi di rispondere alla stessa domanda, ognuno con un compromesso diverso fra rapidità e contesto.
Cinque dimostrazioni, e come sempre niente librerie. Il web del 2026 ha finalmente gli attrezzi nativi: la Popover API che apre e chiude nel top layer, il CSS Anchor Positioning che àncora un pannello al suo trigger senza JS, il <dialog> che intrappola il focus da solo. Il nostro lavoro è cucirli insieme con l’ARIA giusta — role="menu", role="listbox", aria-expanded, aria-activedescendant — perché un menu inaccessibile è un menu che, per metà delle persone, semplicemente non si apre.
Il menu che resta attaccato al bottone
Il primo è il dropdown nella sua forma più onesta: cresce da sotto il trigger che l’ha aperto, non vola al centro e non copre la pagina. È la forma che meno disturba il contesto, perché l’occhio resta esattamente dove stava.
Si apre sotto al bottone; Esc chiude.
Dropdown ancorato · Popover API + CSS Anchor Positioning
Sotto la pelle ci sono due API native, finalmente mature, che fanno quasi tutto da sole. La Popover API (popover + popovertarget) regala apertura, chiusura con Esc, light-dismiss al click fuori, e — gratis — il salire nel top layer, sopra ogni overflow:hidden: niente guerre di z-index, niente portal a mano. Il CSS Anchor Positioning (anchor-name/position-anchor) àncora il pannello al bottone via CSS puro, senza un solo getBoundingClientRect in un loop di scroll. È il capitolo «il nativo batte le librerie» portato al suo apice.
L’onestà sta nel fallback dignitoso: dove l’anchor positioning ancora non c’è, un @supports lascia il pannello in posizione assoluta sotto il trigger — stesso esito, una riga di magia in meno. L’accessibilità non è un ripensamento: il trigger porta aria-haspopup e aria-expanded, il pannello è un role=“menu” con frecce ↑/↓ e Home/End, e alla chiusura il focus torna sul bottone. Con prefers-reduced-motion l’animazione d’entrata collassa a 1ms: il menu è semplicemente già lì, senza scivolare.
Il menu che nasce dove clicchi
Secondo modo, opposto al primo: invece di un punto fisso, il menu appare al puntatore. È il menu del tasto destro — comodissimo perché nasce dove hai già la mano, ma con un pericolo concreto: il puntatore può essere a filo del bordo, e un menu che esce dallo schermo è un menu inutilizzabile.
Menu contestuale · sempre dentro al viewport
La regola è netta: il menu non lascia mai la finestra. Parte dal punto del click, ma se non ci sta sotto-a-destra si ribalta a sinistra e/o verso l’alto, di nuovo verso il puntatore. Lo facciamo misurando il pannello reale dopo averlo reso e poi facendo clamp ai bordi del viewport: è il calcolo che ogni libreria di «floating» nasconde, ma in un menu contestuale serve a mano, perché il punto d’origine lo decide l’utente, non noi. Intercettiamo l’evento contextmenu e preveniamo il menu nativo del browser solo dentro la superficie marcata.
Il tasto destro è un gesto da mouse, e qui sta la trappola dell’accessibilità: chi usa la tastiera preme il tasto menu o Shift+F10, che emettono comunque un contextmenu — lo apriamo allora centrato sulla superficie, non a coordinate zero. Dentro, ↑/↓ scorrono, Esc chiude e il focus torna alla superficie. La voce «Elimina» è pericolosa, ma il rosso non è il solo segnale: il testo resta leggibile e l’hover la marca con uno sfondo, non con la sola tinta — perché chi non distingue il rosso deve poterla riconoscere lo stesso.
La scelta che resta sotto gli occhi
Terzo modo: a volte la scelta non va nascosta affatto. La listbox tiene l’elenco sempre visibile e ci si naviga dentro — è il controllo giusto quando vuoi che l’utente confronti le opzioni, non le scopra una alla volta.
- Bologna
- Cagliari
- Catania
- Firenze
- Genova
- Lecce
- Milano
- Napoli
- Palermo
- Torino
Frecce per muoverti, digita per cercare, Invio per scegliere.
Listbox accessibile · aria-activedescendant + typeahead
L’HTML nativo non ha un <listbox> generico e stilabile, quindi questo è il pattern più «ARIA puro» del capitolo, costruito a regola d’arte secondo le APG: role=“listbox” sul contenitore, role=“option” su ogni voce, aria-selected sulla scelta. Il fulcro è aria-activedescendant: il focus reale del DOM resta sul contenitore, e l’attributo punta all’opzione attiva. Lo screen reader la annuncia senza che il focus si sposti davvero — è la tecnica che permette di avere «l’opzione evidenziata» distinta da «l’elemento col focus», impossibile con un focus DOM solo.
La tastiera è quella vera, completa: ↑/↓ muovono l’opzione attiva, Home/End ai capi, Invio/Spazio selezionano. E il typeahead — digiti «mi» e salta a «Milano» — perché nelle liste lunghe è il gesto che la gente usa davvero, non lo scroll a freccette. La selezione non è solo colore: c’è la spunta che si disegna e il peso del testo che aumenta, doppio canale. Con prefers-reduced-motion lo scroll verso l’opzione attiva passa da fluido a istantaneo: stesso esito, nessuno scorrimento.
L’azione che porta con sé i suoi cugini
Quarto modo, il più sottile: a volte non serve un menu del tutto. Lo split button dà subito l’azione più probabile e nasconde le varianti dietro una freccina — chi sa cosa vuole non apre nulla, chi cerca un’alternativa la trova a un click.
Split button · azione primaria + menu di varianti
Sono due bottoni veri, affiancati, che sembrano un oggetto solo: a sinistra l’azione primaria, a destra la freccia che apre le varianti. Il valore UX è il dosaggio del contesto — l’utente non deve scegliere nulla per fare la cosa più comune («Salva»), e le alternative restano a portata senza ingombrare. È l’opposto del menu che costringe a decidere prima ancora di sapere cosa vuoi: qui il default è la decisione, finché non chiedi di più.
L’ARIA è il punto dove quasi tutti sbagliano. Lo stato di «menu aperto» appartiene solo alla freccia: è lei a portare aria-haspopup=“menu” e aria-expanded. Mettere aria-expanded sull’intero gruppo, o peggio sull’azione primaria — che non apre niente — è l’errore classico che confonde lo screen reader. Dalla freccia, ↓ apre e va alla prima voce; nel menu ↑/↓ scorrono, Esc chiude e il focus torna alla freccia. L’entrata è di ~220ms, l’uscita più rapida; con reduced motion l’animazione collassa a 1ms.
La scelta che parte da ciò che scrivi
Quinto modo, e ribalta tutti gli altri: invece di navigare gerarchie, scrivi cosa vuoi e l’azione emerge. La command palette stile ⌘K è la risposta più radicale alla domanda del capitolo, perché non nasconde nessun contesto: parte dall’intenzione scritta dell’utente.
Command palette · ⌘K dentro un <dialog>
Il contenitore è un <dialog> nativo, ed è qui il guadagno-manifesto del capitolo: showModal() regala il focus trap gratis — il Tab resta intrappolato dentro senza un solo handler per ciclare gli elementi focalizzabili — più Esc che chiude (evento cancel) e il ::backdrop che oscura tutto il resto. Sopra, una textbox con role=“combobox” collegata a una role=“listbox”: digiti, la lista filtra in tempo reale, le frecce muovono l’opzione attiva via aria-activedescendant, Invio esegue. ⌘K/Ctrl+K la aprono da qualsiasi punto.
Onesta anche nel vuoto: se nessun comando combacia, lo diciamo — «Nessun comando trovato» — invece di mostrare una lista finta o l’elenco completo come se niente fosse. È la stessa regola dei capitoli sugli stati vuoti: il nulla va nominato, non mascherato. La palette è potentissima per chi vive sulla tastiera, ma resta un acceleratore, non l’unica via: ogni comando che sta qui deve esistere anche in un menu visibile, perché non tutti sanno che ⌘K esiste. Con prefers-reduced-motion l’entrata del pannello collassa a 1ms — appare risolto, senza viaggio.
Cinque modi, una sola tensione: ogni menu è un compromesso fra darti le strade e tenerti ancorato a dove sei. Il dropdown ancorato sceglie di non muovere l’occhio; il menu contestuale di nascere sotto la mano ma restare nello schermo; la listbox di non nascondere affatto; lo split button di decidere per te finché non chiedi di più; la palette di partire da ciò che scrivi. Nessuno è «il» menu giusto: è giusto quello il cui compromesso combacia con quanto contesto l’utente può permettersi di perdere in quel momento. Offrire una scelta, alla fine, è soprattutto decidere quanto del resto sei disposto a coprire per mostrarla.
Il prossimo capitolo resta tra i componenti ma cambia di nuovo registro: dal menu che offre strade al tab che ne cambia una sotto i piedi. Anatomia di un tab — come si cambia contesto senza disorientare, quando è il pannello stesso, e non un overlay, a trasformarsi.