Créer un menu déroulant
Ce guide couvre tous les menus pop-up : menus déroulants, contextuels, à développer et les barres de menu qui les ouvrent. L'exemple présenté est un menu de commandes d'application, où les éléments modifient l'interface (créer, enregistrer, préférences).
Commandes, liens et composition d'un menu
Deux choix indépendants. Tout le reste s'applique aux deux.
- Élément, par article. Une commande est un
<button type="button">. Une destination est un<a href>qui s'ouvre toujours dans un nouvel onglet, se copie en tant qu'URL et fonctionne sans script. Un menu déroulant peut les mélanger librement : deux liens et six commandes, ou une commande et six liens, ne changent rien ci-dessous. - Modèle d'interaction, par menu déroulant. Les touches fléchées,
Home/EndetEscen font un menu : ajoutez le groupe de sélection, laissez-le déduiremenuitemsur chaque élément, qu'il s'agisse d'un bouton ou d'un lien, et faites en sorte qu'il ressemble à un menu (une barre continue ou un déclencheur avec un indicateur de menu déroulant ; voir "Barre de menu ou boutons de menu distincts" ci-dessous). Une courte liste de liens que les utilisateurs parcourent (Tab) est une navigation : pas de groupe de sélection, pas derole="menu",role="menuitem"ouaria-haspopup="menu". Conservez le popover,Escet la restauration de la sélection, et consultez le guide de la barre de navigation.
Le contenu ne décide jamais des rôles. Un menu déroulant "Produits" contenant des liens peut être un menu, mais un menu déroulant "Compte" contenant des commandes ne peut pas être un menu de navigation.
N'utilisez le modèle de menu que pour un widget composite avec un seul point d'arrêt de tabulation et une navigation par touches fléchées. Utilisez un <div focusgroup="menu nomemory">, et non l'élément HTML <menu>. N'ajoutez pas role="menu" ni role="menuitem" dans le balisage partagé : le groupe de sélection natif déduit ces rôles, et les rôles explicites bloquent cette inférence. Ajoutez-les uniquement dans l'élément de remplacement pour navigateur non compatible ci-dessous. nomemory permet de rouvrir la liste en commençant par le premier élément. Le groupe de focus déplace le focus. Le script gère l'aperçu, l'activation et la restauration du focus.
Ouvrir et positionner le menu
Faites de chaque déclencheur un appelant de commande avec commandfor et command="toggle-popover". Le navigateur ouvre et ferme le popover au clic, à l'appui sur Entrée et à l'appui sur Espace, et conserve le déclencheur dans le groupe de fermeture par clic en dehors du popover. Ainsi, cliquer sur un déclencheur ouvert le ferme. N'utilisez pas popovertarget ni popovertargetaction. Ce modèle utilise commandfor à la place.
Placez chaque popover immédiatement après son déclencheur. Les navigateurs n'ajoutent une relation aria-details implicite que lorsque le popover n'est pas l'élément frère suivant. Ce placement permet donc d'éviter cela sans attributs supplémentaires.
<!-- Command invoker: the browser toggles the popover on click, Enter, and Space -->
<button id="file-trigger" class="trigger" type="button" aria-haspopup="menu" aria-expanded="false"
commandfor="file-menu" command="toggle-popover">
File
</button>
<!-- Immediately after its trigger, so browsers add no implicit aria-details -->
<div id="file-menu" popover="auto">
<!-- Nested, not on the popover: Chromium otherwise drops focusgroup keyboard behavior -->
<div focusgroup="menu nomemory" aria-labelledby="file-trigger">
<button type="button">New file</button>
<button type="button">Save</button>
<button id="export-trigger" type="button" aria-haspopup="menu" aria-expanded="false"
commandfor="export-menu" command="toggle-popover">
Export <span aria-hidden="true">›</span>
</button>
<!-- Submenu: popover="auto" directly after its trigger, inside the parent focusgroup -->
<div id="export-menu" popover="auto">
<div focusgroup="menu nomemory" aria-labelledby="export-trigger">
<button type="button">PDF</button>
<button type="button">Plain Text</button>
</div>
</div>
</div>
</div>
Nommez chaque menu avec aria-labelledby pointant vers son déclencheur. Conservez les indicateurs de sous-menu décoratifs dans le CSS ou ajoutez aria-hidden="true". Donnez aux déclencheurs avec aria-expanded="true" un style actif visible pour que les utilisateurs puissent savoir quel menu est ouvert ou prévisualisé.
Barre de menu ou boutons de menu distincts
Choisissez un formulaire pour les déclencheurs de premier niveau. Le rôle, le modèle de clavier et le style doivent correspondre : des boutons visuellement distincts indiquent un arrêt Tab chacun.
- Barre de menu.
role="menubar"sur le conteneur (jamais sur un popover), sous la forme d'une barre continue sans espace entre les déclencheurs. Un arrêtTab;ArrowLeft/ArrowRightpour passer d'un déclencheur à un autre ; pointez pour ouvrir les menus. Utilisez-le pour une barre de commandes d'application. - Boutons de menu distincts Chaque déclencheur est un point d'arrêt
Tabdistinct et ressemble à un bouton autonome. Pas derole="menubar", pas de touches fléchées entre les déclencheurs, pas d'aperçu lors de la sélection, pas de changement au survol. Encapsulez-les dans<nav>s'ils permettent de naviguer, sinon n'utilisez aucun rôle. Utilisez-la pour les barres d'outils et les en-têtes de page.
Le reste de ce guide décrit la barre de menu. Pour des boutons distincts, conservez tout à l'intérieur de chaque menu et supprimez le comportement au niveau de la barre de menu.
Positionner le menu avec le positionnement des ancres CSS
Utilisez le positionnement d'ancrage CSS, et non les coordonnées JavaScript. command="toggle-popover" et showPopover({ source: trigger }) définissent tous deux une ancre implicite. N'ajoutez donc pas de règles anchor-name ou position-anchor par menu. Le comportement du menu ne doit pas dépendre du positionnement de l'ancrage.
/* Top-level menus sit below their trigger. The implicit anchor comes from the invoker or source. */
[popover] {
position-area: block-end span-inline-end;
inset: auto;
}
/* A popover nested in another popover is a submenu: beside its parent item, flipping when it would overflow. */
[popover] [popover] {
position-area: inline-end span-block-end;
position-try-fallbacks: flip-inline, flip-block, --below;
}
/* Last resort for narrow viewports: directly below the trigger, indented, so the submenu never
covers its trigger. */
@position-try --below {
position-area: block-end span-inline-end;
margin-inline-start: 1rem;
/* Fit the area: the submenu's minimum width must not stop this fallback from fitting. */
min-inline-size: 0;
max-inline-size: calc(100% - 1rem);
}
Placez --below en dernier pour qu'il ne s'applique que si aucun des deux côtés ne convient. Il s'ouvre directement sous le déclencheur. Par conséquent, sur un écran étroit, il couvre les éléments frères situés sous le déclencheur. C'est le compromis accepté pour rester à côté du déclencheur. Réduisez également le sous-menu à la zone (min-inline-size: 0, max-inline-size moins le retrait). Une solution de remplacement qui ne convient pas est ignorée. Si aucune ne convient, le navigateur conserve l'emplacement de base. Ainsi, un sous-menu qui est quelques pixels trop large couvre silencieusement son déclencheur.
Pour savoir comment fonctionnent les solutions de remplacement pour le retournement sur un popover simple, consultez Menus déroulants adaptés à la fenêtre d'affichage. Ce guide positionne un groupe de boutons, et non un menu. Pour les menus, l'emplacement du sous-menu et le remplacement --below ci-dessus sont prioritaires.
Animer l'ouverture et la fermeture
Facultatif. Moins de 150 ms, opacity et un petit translate uniquement, de même pour les sous-menus, dans @media (prefers-reduced-motion: no-preference). Le curseur se déplace sur toggle, de sorte que la transition ne retarde jamais les utilisateurs du clavier.
@media (prefers-reduced-motion: no-preference) {
/* Closing: this rule is in effect once :popover-open is removed. */
[popover] {
opacity: 0;
translate: 0 -.25rem;
transition: opacity .15s, translate .15s, display .15s allow-discrete, overlay .15s allow-discrete;
}
/* Opening: faster. */
[popover]:popover-open {
opacity: 1;
translate: 0 0;
transition-duration: .1s;
@starting-style { opacity: 0; translate: 0 -.25rem; }
}
/* Switching between open menubar menus is instant, as in native menubars. */
[role="menubar"]:has(> [popover]:popover-open) > [popover] {
transition: none;
}
}
@starting-style n'est pas une répétition de [popover] : un popover fermé est display: none, et les transitions ne s'exécutent pas à partir d'un état non affiché. Il fournit donc les valeurs avant l'ouverture. Sans cela, l'ouverture se fait d'un coup et seule la fermeture est animée. display et overlay avec allow-discrete permettent de maintenir l'affichage du popover et de le conserver dans la couche supérieure jusqu'à la fin de la transition de fermeture.
Clavier et focus
N'ajoutez pas de deuxième système roving-tabindex dans un focusgroup. Le script ne couvre que ce que les invokeurs et le groupe de sélection ne font pas :
- Un seul point d'arrêt de tabulation pour la barre de menu.
Tabentre au niveau du premier déclencheur de premier niveau.ArrowLeftetArrowRightpermettent de naviguer entre les déclencheurs de premier niveau et de revenir au début ou à la fin.HomeetEndsur un déclencheur permettent d'accéder au premier ou au dernier élément du menu.- Remarque : Déclencheur auquel
Tabrenvoie. L'APG autorise soit le premier déclencheur, soit le dernier utilisé. Le guide spécifie la première option, mais la démo déplace l'arrêt de tabulation avecArrowLeftetArrowRight. Ainsi, après la navigation au clavier,Shift+Tabrevient au dernier déclencheur atteint. L'utilisation du pointeur ne le déplace pas. Par conséquent, la même séquence peut atterrir sur un déclencheur différent. Les deux comportements sont acceptables. Choisissez-en un et respectez-le.
- Remarque : Déclencheur auquel
- Aperçu La sélection d'un déclencheur de niveau supérieur affiche son menu et définit
aria-expanded="true". Le curseur reste sur le déclencheur, et aucun élément n'est sélectionné. Le floutage du déclencheur ferme un menu en mode aperçu uniquement. - Entrée et Espace sur un aperçu Il ferme l'aperçu et reste concentré sur le déclencheur. L'action
toggle-popovernative gère cela. Le script ne doit donc pas intercepter ces touches. - Flèche vers le bas sur un aperçu. Convertit l'élément en menu ouvert et sélectionne le premier élément.
- Cliquez sur un déclencheur ouvert ou prévisualisé. Ferme l'application de manière native, sans accéder au menu.
- Différence connue concernant les déclencheurs de sous-menu. Un clic sur un déclencheur de sous-menu permet également de basculer de manière native. Il peut donc fermer un sous-menu déjà prévisualisé par pointage ou en ouvrir un qui n'est pas encore apparu. Les menus natifs ne font que l'ouvrir et ne le ferment jamais. Cette action est inoffensive, donc le guide l'accepte.
- Clic du pointeur sur un déclencheur fermé. Ouvre le fichier de manière native. Le curseur reste sur le déclencheur. Aucun élément n'est sélectionné. Seules les ouvertures de clavier permettent d'accéder au menu.
- Pointez sur un élément. Il est mis en avant sans anneau, de sorte que le pointage et la mise au point ne forment qu'un seul élément.
- Pointez sur un autre déclencheur de niveau supérieur lorsqu'un menu est ouvert. Ferme l'élément et prévisualise celui sur lequel pointe le curseur, en mettant en avant son déclencheur sans anneau. Rien n'est ouvert : le pointage ne fait rien.
- Pointeur sur un déclencheur de sous-menu. Affiche un aperçu de son sous-menu après un court délai (le curseur de la souris déclenche le même aperçu lors de la sélection). Un élément frère le ferme.
- Dans un menu
ArrowUpetArrowDownpermettent de passer d'un élément activé à un autre.HomeetEndpermettent d'accéder au premier et au dernier élément activé, ce que le script gère. - Flèche vers la gauche ou vers la droite dans un menu de premier niveau ouvert Ferme le menu et prévisualise le menu adjacent en mettant l'accent sur son déclencheur, en bouclant à chaque extrémité.
ArrowDownsaisit ensuite le code. Les boutons gauche et droit ne permettent jamais d'accéder à un menu. - Flèche vers la droite sur un déclencheur de sous-menu Ouvre le sous-menu et place le curseur sur son premier élément.
- Flèche vers la droite sur un élément de sous-menu sans sous-menu enfant. Ferme la hiérarchie et prévisualise le menu de niveau supérieur suivant, tandis que la sélection reste sur son déclencheur.
ArrowDownsaisit ensuite le code. - Flèche vers la gauche ou Échap dans un sous-menu Ferme uniquement ce sous-menu et replace le curseur sur son déclencheur. Le menu parent reste ouvert.
- Appuyez sur Entrée ou sur Espace pour exécuter une commande. Active l'élément et ferme tous les menus ouverts.
- Onglet. Quitte le système de menus. Ne piégez pas le curseur et ne parcourez pas chaque commande. La fermeture ne doit pas laisser le curseur dans le contenu masqué.
L'événement command se déclenche avant que toggle-popover n'ouvre le menu. Le curseur ne peut donc pas se déplacer dans le menu à ce moment-là. Déplacez plutôt le curseur dans l'événement toggle. Le code HTML ne restaure le focus que pour le premier popover="auto" d'une pile. Les niveaux imbriqués doivent donc restaurer explicitement le focus sur leur déclencheur.
Implémenter l'interaction
Implémentez les comportements ci-dessus pour votre propre balisage. La gestion des flèches, de Home/End, du retour à la ligne et de Tab pour quitter est du code keydown ordinaire. Certaines parties sont faciles à confondre. Elles sont regroupées ci-dessous par type d'entrée.
Gestion du clavier et du curseur
- Un chemin ouvert L'ouverture par clavier appelle
trigger.click(). Par conséquent, letoggle-popoverde l'appelant est le seul moyen d'ouvrir un menu. Marquez le menu pour que le gestionnairetogglel'ouvre, le clavier s'ouvre uniquement. Lors de l'ouverture d'un pointeur,togglesélectionne le déclencheur sans anneau, carpointerdowna empêché la sélection et les touches fléchées doivent toujours fonctionner. - Aperçu de la mise au point La sélection d'un déclencheur appelle
showPopover({ source: trigger })afin que le menu s'y ancre. Ignorer le retour du curseur depuis le menu du déclencheur ou depuis une autre fenêtre (indicateur sur la fenêtreblur, coche supprimée après la fenêtrefocusou changement d'application rouvrant le dernier menu). Masquer un menu prévisualisé surblur, sauf si la sélection s'est déplacée vers celui-ci. - Déplacez le focus dans
toggle, pas danscommand.commandse déclenche avant l'ouverture du menu. - Restaurez vous-même le focus sur le déclencheur. Mettez le déclencheur au point avant
hidePopover(). Après l'activation d'un pointeur, restaurez le focus sans anneau (focus({ focusVisible: false })), ou le déclencheur affiche le contour du focus clavier après un clic de souris. Les activations du clavier conservent l'anneau. Un clic avecevent.detail === 0provient du clavier. - Fermez un niveau à la fois. Appelez
stopPropagation()surEscapeetArrowLeftdans un sous-menu, sinon l'écouteur du parent le fermera également.
Gestion des pointeurs
- Boutons d'activation/désactivation du clic du pointeur. Appelez
preventDefault()surpointerdownpour que le clic ne déplace pas la sélection et ne lance pas d'aperçu en cours de clic. Mémorisez la gâchette sur laquelle vous avez appuyé. Cliquer sur un déclencheur fermé ou prévisualisé l'ouvre ou le ferme de manière native et n'ouvre jamais le menu. Seules les ouvertures au clavier permettent d'y accéder. N'interceptez pas le clic. - Le curseur suit la sélection. Appliquez un style à
:hoveret:focus-visibleséparément. Surpointermovesur un élément, appelezfocus({ focusVisible: false }). Les navigateurs qui ignorent l'option n'affichent de toute façon pas d'anneau pour la mise au point du pointeur. - Le survol ouvre les menus. Sur
pointerenterd'un déclencheur de niveau supérieur alors qu'un autre menu de niveau supérieur est ouvert : masquez-le, activez le déclencheur survolé sans anneau et prévisualisez-le lors de l'activation (voir ci-dessus). Rien n'est ouvert : ne rien faire. - Triangle de sécurité. Un pont CSS fixe est trop étroit : il ne fonctionne que lorsque le pointeur commence près du bord du parent. Laissez CSS dessiner le pont et utilisez un script uniquement pour signaler le pointeur :
- N'exécutez cette commande que lorsque
matchMedia('(hover: hover)').matches. Elle n'est pas pertinente pour les commandes tactiles et au clavier. - Démarre lorsque le pointeur quitte un déclencheur de sous-menu dont le sous-menu est ouvert. Ignorer un sous-menu
--below, qui n'a pas de bord latéral. - Définissez
--bridge-xet--bridge-y(le pointeur, en px par rapport au sous-menu),--bridge-edge(le bord le plus proche du sous-menu x :0lorsque le pointeur est à sa gauche, sa largeur lorsqu'il est retourné) et--bridge-heightsur le sous-menu, etdata-bridgepour activer le pont. - Arrêtez-vous après environ 300 ms, lorsque le pointeur atteint un élément du sous-menu ou lorsque le sous-menu se ferme. Supprimez l'écouteur et l'attribut. Rien ne s'exécute après cela.
- Le CSS donne au sous-menu un
::beforedécoupé en triangle, du pointeur aux angles les plus proches. Il se trouve au-dessus des éléments frères sur le chemin du pointeur, de sorte qu'ils ne reçoivent jamais le pointage qui fermerait le sous-menu.clip-pathlimite également les tests de positionnement, de sorte que le reste du menu parent reste interactif. - Le sous-menu nécessite
overflow: visible(un popover est tronqué par défaut), et le pseudo-élément s'étend au-delà. Vous devez donc décaler les points de la forme de cette valeur. - Suivi : un tracé incurvé (commandes de courbe
shape()) suit mieux les arcs réels du pointeur qu'un triangle droit.
- N'exécutez cette commande que lorsque
[popover] [popover] {
overflow: visible;
&[data-bridge]::before {
content: "";
position: absolute;
inset: -100vh -100vw; /* Room for the triangle, which can extend far outside the submenu. */
clip-path: shape(
from calc(100vw + var(--bridge-x)) calc(100vh + var(--bridge-y)),
line to calc(100vw + var(--bridge-edge)) 100vh,
line to calc(100vw + var(--bridge-edge)) calc(100vh + var(--bridge-height)),
close
);
}
}
const canHover = matchMedia('(hover: hover)');
// Call on pointerleave of a submenu trigger, with that trigger's submenu.
function startBridge(submenu, event) {
if (!canHover.matches || !submenu.matches(':popover-open')) return;
const rect = submenu.getBoundingClientRect();
if (event.clientX >= rect.left && event.clientX <= rect.right) return; // --below: no side edge.
const style = submenu.style;
style.setProperty('--bridge-edge', `${event.clientX < rect.left ? 0 : rect.width}px`);
style.setProperty('--bridge-height', `${rect.height}px`);
const update = ({ clientX, clientY, target }) => {
if (target !== submenu && submenu.contains(target)) return stop(); // Reached an item.
style.setProperty('--bridge-x', `${clientX - rect.left}px`);
style.setProperty('--bridge-y', `${clientY - rect.top}px`);
};
const timer = setTimeout(() => stop(), 300);
const stop = () => {
clearTimeout(timer);
document.removeEventListener('pointermove', update);
submenu.removeEventListener('toggle', stop);
submenu.removeAttribute('data-bridge');
};
update(event);
submenu.dataset.bridge = '';
document.addEventListener('pointermove', update);
submenu.addEventListener('toggle', stop, { once: true });
}
"Hover follows focus" (élément 2) reste un gestionnaire pointermove simple : il n'a pas besoin de logique de triangle.
Autre possibilité :
- Hors du champ de vision. Fermez tous les menus ouverts lorsque la sélection se déplace en dehors du système de menus (un écouteur
documentfocusin). - Activation des commandes Annoncez-le, puis masquez le menu racine et rétablissez le focus sur son déclencheur, sans anneau si l'activation provient d'un pointeur. Pour les éléments cochables (
menuitemcheckbox,menuitemradio), attendez environ 300 ms avant de fermer pour que la coche modifiée soit visible. Définissez le style d'un déclencheur fermé comme actif uniquement sur:focus-visible. Sinon, le focus restauré (sans anneau) le laisse en surbrillance après une sélection au pointeur. aria-expanded: définissez-le uniquement dans l'écouteurtoggle. Il couvre les états d'aperçu et d'ouverture, et l'élémentaria-expandeddu déclencheur reste"true"pendant que son menu est prévisualisé ou ouvert.- Assistants : Les solutions de remplacement ci-dessous supposent que vous définissez des assistants pour votre balisage :
triggers(tous les boutons[commandfor]),menuFor(trigger),isOpen(menu),closeAndRestore(menu, { fromPointer })(mettre l'accent sur le déclencheur, sans anneau lorsquefromPointer, puismenu.hidePopover()),enterOnOpen(unWeakSetde menus à saisir une fois ouverts) etpointerTrigger(le déclencheur sur lequel un pointeur a appuyé en dernier).
Sous-menus
- Utilisez
popover="auto"pour les sous-menus, placés après le déclencheur du sous-menu dans l'élémentfocusgroupparent. Jamaispopover="manual". - L'imbrication permet au navigateur de garder le parent ouvert, de fermer les sous-menus frères, de fermer les sous-menus enfants lorsque le menu racine se ferme et de gérer la fermeture par clic extérieur.
- Le déclencheur du sous-menu conserve
aria-haspopup="menu"etaria-expanded, tandis que l'écouteurtogglemaintientaria-expandedsynchronisé. - Le script du clavier gère les éléments
EscapeetArrowLeftimbriqués comme décrit ci-dessus. - Un déclencheur de sous-menu n'exécute jamais de commande : un contrôle ne peut pas à la fois activer et ouvrir, et
Enterne peut pas signifier les deux. Placez l'action par défaut comme premier élément du sous-menu ou ajoutez un élément de commande distinct à côté du déclencheur. Si un contrôle combiné est forcé, documentez-le comme un compromis d'accessibilité.
Séparateurs et éléments cochables
- Séparez les groupes d'éléments avec
<hr>dans le groupe de focus : unseparatornatif, non sélectionnable, ignoré par le groupe de focus et le remplacement. Jamais un<div>stylisé. - Une option booléenne est un
<button type="button">avecaria-checkedet unrole="menuitemcheckbox"explicite. Focusgroup ne déduit pas ces rôles. C'est donc le seul endroit où le balisage partagé définit un rôle d'élément. - L'un des éléments d'un ensemble est un
<button type="button">avecrole="menuitemradio"etaria-checked, à l'intérieur d'unrole="group"avec unaria-label, dans le même groupe de sélection. - Le script active/désactive
aria-checked. Entersur une case à cocher active/désactive l'option et ferme le menu ;Spaceactive/désactive l'option et laisse le menu ouvert (APG). Les boutons d'option se ferment lorsque l'utilisateur appuie sur l'une de ces touches.- Affichez la coche à partir de
[aria-checked="true"]::before, et non d'un élément d'icône distinct (voir le code CSS ci-dessous).
Non abordé ici
- Activation feedback. Les menus natifs font clignoter l'élément choisi avant de se fermer. Si vous le souhaitez, affichez une confirmation visuelle d'environ 100 ms avant
hidePopover(). Ne retardez pas la restauration de la sélection ni l'annonce. - Saisie semi-automatique Focusgroup n'en a pas. Si nécessaire, sur l'élément imprimable
keydown, faites correspondre les libellés des éléments activés du niveau actuel et sélectionnez la correspondance suivante après l'élément actuel, en bouclant. Ne pas intercepterSpace.
Vérifier avant de terminer
Une instruction ne peut pas garantir un comportement. Testez donc le résultat. Ouvrez la page et vérifiez chacun de ces éléments avec un vrai clavier et une vraie souris. Corrigez et testez à nouveau ceux qui échouent :
- Clavier
Tabpermet d'accéder au premier déclencheur une fois,ArrowDownpermet d'entrer,Escapepermet de fermer un niveau et de revenir au déclencheur, etTabpermet de fermer tous les menus. - Pointeur ouvert. Cliquer sur un déclencheur ouvre son menu avec le focus sur le déclencheur et sans anneau. Cliquer à nouveau le ferme.
- Commande de pointeur. Cliquer sur une commande ferme tous les menus et n'affiche aucun anneau de sélection sur le déclencheur.
- Sous-menu. Si vous déplacez le curseur en diagonale du milieu d'un déclencheur de sous-menu vers un élément de sous-menu, celui-ci reste ouvert. Si vous le déplacez vers un élément frère non associé, il se ferme.
- Écran étroit À 375 px, aucun sous-menu ne couvre son propre déclencheur.
- Coches Les éléments cochés sont signalés par une coche.
- Pas de restes. Aucune erreur de console, et aucun
popovertarget,popovertargetactionouaria-detailssur les déclencheurs.
Aligner le contenu des éléments de menu
Lorsqu'une ligne comporte une icône, un libellé et un indicateur de raccourci ou de sous-menu, réservez un emplacement de début de taille fixe pour l'icône ou la coche, et placez le contenu de fin à la fin de la ligne. Les icônes ont une taille connue. Un emplacement fixe permet donc de garder les libellés alignés, qu'un élément comporte ou non une icône. N'utilisez pas subgrid pour les lignes : tout enfant intégré supplémentaire (un badge, par exemple) devient son propre élément de grille et perturbe la mise en page. Masquez le texte du raccourci pour les technologies d'assistance lorsqu'il ne fait que répéter une touche qui contrôle déjà l'élément.
[focusgroup~="menu"] {
--menu-icon-size: 1.25rem;
}
/* Target items only. A closed submenu popover inside the focusgroup must keep display: none. */
[focusgroup~="menu"] > button,
[focusgroup~="menu"] > [role="group"] > button {
display: flex;
align-items: center;
gap: .5rem;
}
/* The leading slot holds the icon or check mark. */
[focusgroup~="menu"] button::before {
content: "";
flex: none;
inline-size: var(--menu-icon-size);
}
/* Check mark for checked items. This selector must be at least as specific as the base
::before rule above, or the empty content wins and the check mark never shows. */
[focusgroup~="menu"] button[aria-checked="true"]::before {
content: "✓";
}
/* Shortcut or submenu indicator at the inline end. */
[focusgroup~="menu"] .hint {
margin-inline-start: auto;
}
Placez un indicateur ou un raccourci de fin dans un <span class="hint" aria-hidden="true"> à l'intérieur du bouton. Le déclencheur du sous-menu annonce déjà son pop-up.
Amélioration progressive et solutions de remplacement
Solutions de remplacement et compatibilité des navigateurs pour focusgroup
Conservez focusgroup="menu nomemory" dans le balisage. Si 'focusGroup' in HTMLElement.prototype est défini sur "false", installez les touches Haut, Bas, Début et Fin, définissez role="menu" sur chaque conteneur de menu et role="menuitem" sur les boutons sans rôle. Ne modifiez pas les éléments cochables, <hr> et role="group". Ne l'installez jamais lorsqu'un focusgroup natif est présent. "Gauche", "Droite", "Aperçu", "Ouvrir" et "Fermer" sont des scripts d'auteur dans les deux cas.
if (!('focusGroup' in HTMLElement.prototype)) {
document.querySelectorAll('[focusgroup~="menu"]').forEach(group => {
group.setAttribute('role', 'menu');
group.querySelectorAll('button:not([role])').forEach(item => {
item.setAttribute('role', 'menuitem');
});
group.addEventListener('keydown', event => {
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
// Own items only: a nested submenu has its own focusgroup, a role="group" wrapper does not.
const items = [...group.querySelectorAll('button')]
.filter(item => item.closest('[focusgroup]') === group)
.filter(item => !item.disabled && item.getAttribute('aria-disabled') !== 'true');
const current = items.indexOf(document.activeElement);
if (current < 0) return;
const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 :
(current + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
event.preventDefault();
event.stopImmediatePropagation();
items[next].focus({ preventScroll: true });
}, true);
});
}
Solutions de remplacement et compatibilité des navigateurs pour Popover
L'API Popover est principalement une amélioration progressive, mais ses comportements déterminants (promotion de la couche supérieure, fermeture par clic en dehors de l'élément et appel de popovertarget) n'ont pas d'équivalent CSS uniquement. Les anciens navigateurs ont besoin d'un polyfill ou d'un remplacement manuel si vous préférez ne pas en fournir.
Polyfill : pour que l'attribut popover soit compatible avec les anciens navigateurs, chargez @oddbird/popover-polyfill de manière conditionnelle. OBLIGATOIRE : Détectez la fonctionnalité en vérifiant la propriété popover sur HTMLElement.prototype, et chargez le polyfill uniquement lorsque la compatibilité native est manquante. Ne le chargez PAS de manière inconditionnelle.
Avec un bundler ou une carte d'importation :
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Sans bundler, importez depuis un CDN dans un <script type="module"> :
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Mise en garde concernant l'application de styles : le polyfill ne peut pas définir la véritable pseudo-classe :popover-open. Il applique donc une classe .\:popover-open à la place. OBLIGATOIRE : Combinez les deux avec :is() ou :where(), sinon les navigateurs qui ne disposent pas de :popover-open ignorent l'intégralité de la règle :
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Vous pouvez également utiliser position: fixed pour une solution de remplacement sans polyfill et calculer manuellement les coordonnées via getBoundingClientRect(), ou vous appuyer sur le positionnement par défaut avec inset: auto si cela convient à votre cas d'utilisation.
Solutions de remplacement et compatibilité des navigateurs pour le positionnement des ancres
Sans le positionnement d'ancrage CSS, un popover revient au placement centré de la fenêtre d'affichage du user-agent. Ne l'utilisez jamais pour un menu : cela détache le menu de son déclencheur, et un sous-menu centré recouvre son parent. Masquez les règles ancrées derrière @supports (position-area: block-end) et positionnez le menu à partir du script en cas d'échec :
- Placez le menu sous son déclencheur (niveau supérieur) ou à côté (sous-menu), en le basculant de l'autre côté et en le fixant à la fenêtre d'affichage lorsqu'il déborde.
- Exécutez-la dans le gestionnaire
togglelorsque le menu s'ouvre, puis appliquez le résultat en tant queleftettopdans le menu. Utilisezposition: fixedavecinset: autoetmargin: 0pour que les coordonnées soient relatives à la fenêtre d'affichage. - Conservez ces remplacements dans
@supports not (position-area: block-end)afin que les navigateurs avec positionnement d'ancrage ne soient pas affectés.
Si vous ciblez des implémentations antérieures dans lesquelles showPopover({ source }) n'établit pas d'ancrage implicite (pas de source sur ToggleEvent.prototype), attribuez à chaque déclencheur un anchor-name unique et à chaque menu un position-anchor correspondant à partir du script, au lieu d'écrire des règles CSS par menu.
Pour les navigateurs qui ne disposent pas du tout du positionnement des ancres, consultez Menus déroulants adaptés à la fenêtre d'affichage pour la route polyfill.
Solutions de secours et compatibilité des navigateurs pour les commandes Invoker
Sans appelants, command="toggle-popover" ne fait rien. Le script doit donc activer/désactiver le menu.
- Détectez l'interface, pas une instance :
'commandForElement' in HTMLButtonElement.prototype. - Sur chaque déclencheur
click, appelezpreventDefault()pour qu'un navigateur avec des appelants ne puisse pas basculer deux fois. - Si le menu est ouvert, fermez-le avec
closeAndRestore(menu). Sinon, appelezmenu.showPopover({ source: trigger }). - Marquez le menu dans
enterOnOpen, sauf si le clic provient du déclencheur enfoncé (pointerTrigger), de sorte que seules les ouvertures au clavier accèdent au menu. EnteretSpacefonctionnent toujours, car ils génèrent un clic sur les boutons.