Drop-down-Menü erstellen
Dieser Leitfaden behandelt alle Arten von Pop-up-Menüs: Drop-down-, Kontext- und Dreipunkt-Menüs sowie die Menüleisten, über die sie geöffnet werden. Das Beispiel ist ein Befehlsmenü einer Anwendung, in dem Elemente die Benutzeroberfläche ändern (Erstellen, Speichern, Einstellungen).
Befehle, Links und Bestandteile eines Menüs
Zwei unabhängige Optionen. Alles andere hier gilt für beide.
- Element, pro Artikel. Ein Befehl ist ein
<button type="button">; ein Ziel ist ein<a href>, das sich in einem neuen Tab öffnet, als URL kopiert werden kann und ohne Script funktioniert. In einem Drop-down-Menü können sie beliebig kombiniert werden: Zwei Links und sechs Befehle oder ein Befehl und sechs Links ändern nichts an den folgenden Angaben. - Interaktionsmodell, per Drop-down-Menü Pfeiltasten,
Home/EndundEscmachen es zu einem Menü: Fügen Sie die Fokusgruppe hinzu, lassen Sie siemenuitemfür jedes Element ableiten, egal ob Schaltfläche oder Link, und lassen Sie es wie ein Menü aussehen (eine durchgehende Leiste oder ein Trigger mit einer Drop-down-Anzeige; siehe „Menüleiste oder separate Menüschaltflächen“ unten). Eine kurze Liste von Links, durch die NutzerTab, ist die Navigation: keine Fokusgruppe, keinrole="menu",role="menuitem"oderaria-haspopup="menu"; Popover,Escund Fokuswiederherstellung beibehalten und den Leitfaden zur Navigationsleiste lesen.
Die Rollen werden nie durch den Inhalt bestimmt. Ein Drop-down-Menü „Produkte“ mit Links kann ein Menü sein, ein Drop-down-Menü „Konto“ mit Befehlen kann keine Navigation sein.
Verwenden Sie das Menümuster nur für ein zusammengesetztes Widget mit einem Tabstopp und Navigation mit den Pfeiltasten. Verwenden Sie ein <div focusgroup="menu nomemory">-Element, nicht das HTML-Element <menu>. Fügen Sie im freigegebenen Markup keine role="menu"- oder role="menuitem"-Elemente hinzu: Die native Fokusgruppe leitet diese Rollen ab und explizite Rollen blockieren diese Ableitung. Fügen Sie sie nur im Fallback für nicht unterstützte Browser unten hinzu. Mit nomemory wird beim erneuten Öffnen das erste Element angezeigt. Die Fokusgruppe verschiebt den Fokus. Das Skript übernimmt die Vorschau, Aktivierung und Wiederherstellung des Fokus.
Menü öffnen und positionieren
Machen Sie jeden Trigger mit commandfor und command="toggle-popover" zu einem Befehlsaufrufer. Der Browser öffnet und schließt das Popover bei Klick, Eingabe und Leertaste und behält den Trigger in der Light-Dismiss-Gruppe des Popovers bei. Wenn Sie also auf einen geöffneten Trigger klicken, wird er geschlossen. Verwenden Sie nicht popovertarget oder popovertargetaction. In diesem Muster wird stattdessen commandfor verwendet.
Platzieren Sie jedes Popover direkt nach dem zugehörigen Trigger. Browser fügen nur dann eine implizite aria-details-Beziehung hinzu, wenn das Popover nicht das nächste gleichgeordnete Element ist. Durch diese Platzierung wird das ohne zusätzliche Attribute vermieden.
<!-- 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>
Benennen Sie jedes Menü mit aria-labelledby, das auf den Trigger verweist. Behalten Sie dekorative Untermenü-Indikatoren in CSS bei oder fügen Sie aria-hidden="true" hinzu. Gib Auslösern mit aria-expanded="true" einen sichtbaren aktiven Stil, damit Nutzer erkennen können, welches Menü geöffnet ist oder in der Vorschau angezeigt wird.
Menüleiste oder separate Menüschaltflächen
Wählen Sie ein Formular für die Trigger auf oberster Ebene aus. Rolle, Tastaturmodell und Stil müssen übereinstimmen: Visuell getrennte Schaltflächen signalisieren jeweils einen Tab-Stopp.
- Menüleiste
role="menubar"im Container (nie in einem Pop-over), als eine durchgehende Leiste ohne Lücken zwischen den Triggern. EinTab-Stopp;ArrowLeft/ArrowRight– zwischen Triggern wechseln; Bewegen des Mauszeigers über ein Element öffnet Menüs. Für eine Befehlsleiste in einer Anwendung verwenden. - Separate Menüschaltflächen: Jeder Trigger ist ein eigener
Tab-Stopp und sieht wie eine eigenständige Schaltfläche aus. Keinrole="menubar", keine Pfeiltasten zwischen Triggern, keine Vorschau bei Fokus, kein Wechseln beim Bewegen des Mauszeigers. Wenn sie navigieren, in<nav>einschließen, andernfalls keine Rolle. Für Symbolleisten und Seitenüberschriften verwenden.
Im weiteren Verlauf dieses Leitfadens wird das Menüleistenformular beschrieben. Wenn Sie separate Schaltflächen verwenden möchten, behalten Sie alle Elemente in den einzelnen Menüs bei und verzichten Sie auf das Verhalten auf Menüleistenebene.
Menü mit CSS-Ankerpositionierung positionieren
Verwenden Sie die CSS-Ankerpositionierung, nicht JavaScript-Koordinaten. Sowohl command="toggle-popover" als auch showPopover({ source: trigger }) legen einen impliziten Anker fest. Fügen Sie daher keine anchor-name- oder position-anchor-Regeln pro Menü hinzu. Das Menüverhalten darf nicht von der Ankerpositionierung abhängen.
/* 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);
}
Lassen Sie --below am Ende, damit es nur angewendet wird, wenn keine der beiden Seiten passt. Es wird direkt unter dem Trigger geöffnet, sodass es auf einem schmalen Bildschirm die gleichgeordneten Elemente unter dem Trigger verdeckt. Das ist der akzeptierte Kompromiss, um das Menü neben dem Trigger zu platzieren. Das Untermenü sollte sich ebenfalls auf den Bereich (min-inline-size: 0, max-inline-size minus Einzug) verkleinern. Ein Fallback, der nicht passt, wird übersprungen. Wenn keiner passt, behält der Browser die Basisplatzierung bei. Ein Untermenü, das ein paar Pixel zu breit ist, deckt dann unbemerkt seinen Trigger ab.
Informationen zur Funktionsweise von Fallbacks für das Umkehren bei einem einfachen Pop-over finden Sie unter Viewport-Aware Dropdowns. In diesem Leitfaden wird eine Schaltflächengruppe und kein Menü positioniert. Bei Menüs haben die Platzierung des Untermenüs und der oben genannte --below-Fallback Vorrang.
Öffnen und Schließen animieren
Optional. Unter 150 ms, nur opacity und ein kleines translate, dasselbe für Untermenüs, innerhalb von @media (prefers-reduced-motion: no-preference). Der Fokus wird auf toggle verschoben, sodass der Übergang für Tastaturnutzer nie verzögert wird.
@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 ist keine Wiederholung von [popover]: Ein geschlossenes Popover ist display: none und Übergänge werden nicht von einem nicht gerenderten Zustand ausgeführt. Daher werden die Vorher-Werte für das Öffnen bereitgestellt. Ohne sie wird das Öffnen abrupt dargestellt und nur das Schließen wird animiert. display und overlay mit allow-discrete sorgen dafür, dass das Popover gerendert und auf der obersten Ebene bleibt, bis der Übergang zum Schließen beendet ist.
Tastatur und Fokus
Fügen Sie kein zweites roving-tabindex-System innerhalb einer Fokusgruppe hinzu. Das Script deckt nur das ab, was die Aufrufer und die Fokusgruppe nicht tun:
- Ein Tabstopp für die Menüleiste.
Tabwird beim ersten Trigger auf oberster Ebene ausgelöst. MitArrowLeftundArrowRightkönnen Sie zwischen den Triggern auf oberster Ebene wechseln und am Ende jeweils umbrechen. MitHomeundEndauf einem Trigger wird das Menü beim ersten oder letzten Element geöffnet.- Hinweis: Welcher Trigger
Tabzurückgibt. Die APG lässt entweder den ersten oder den zuletzt verwendeten Trigger zu. In der Anleitung wird die erste Option angegeben, aber in der Demo wird der Tabstopp mitArrowLeftundArrowRightverschoben, sodass nach der TastaturnavigationShift+Tabzum zuletzt erreichten Trigger zurückgekehrt wird. Die Verwendung des Zeigers verschiebt ihn nicht, sodass dieselbe Sequenz auf einem anderen Trigger landen kann. Beide Verhaltensweisen sind akzeptabel. Wählen Sie eine aus und bleiben Sie dabei.
- Hinweis: Welcher Trigger
- Vorabversion. Wenn ein Trigger auf oberster Ebene fokussiert wird, wird sein Menü angezeigt und
aria-expanded="true"festgelegt. Der Fokus bleibt auf dem Trigger und es wird kein Element fokussiert. Wenn der Trigger unscharf wird, wird ein Menü geschlossen, das nur für die Vorschau gedacht ist. - Eingabetaste und Leertaste in einer Vorschau Sie schließen die Vorschau und konzentrieren sich wieder auf den Trigger. Die native
toggle-popover-Aktion übernimmt dies, daher darf das Script diese Tasten nicht abfangen. - Pfeil nach unten auf einer Vorschau. Wandelt es in ein geöffnetes Menü um und fokussiert das erste Element.
- Klicken Sie auf einen geöffneten oder in der Vorschau angezeigten Trigger. Schließt das Menü nativ, ohne es zu öffnen.
- Bekannter Unterschied bei Untermenü-Triggern. Ein Klick mit dem Mauszeiger auf einen Untermenü-Trigger schaltet ebenfalls nativ um, sodass ein Untermenü geschlossen werden kann, das bereits in der Vorschau angezeigt wurde, oder ein Untermenü geöffnet werden kann, das noch nicht angezeigt wurde. Native Menüs öffnen es nur und schließen es nie. Das ist unbedenklich, daher wird es vom Guide akzeptiert.
- Klicken Sie auf einen geschlossenen Trigger. Öffnet sie nativ. Der Fokus bleibt auf dem Trigger. Es wird kein Element fokussiert. Nur das Öffnen der Tastatur wird im Menü angezeigt.
- Mauszeiger über einem Element Fokussiert das Element ohne Ring, sodass Hover und Fokus ein Element sind.
- Der Mauszeiger wird über einen anderen Trigger auf oberster Ebene bewegt, während ein Menü geöffnet ist. Schließt es und zeigt eine Vorschau des Elements an, auf das der Mauszeiger bewegt wird. Der Trigger wird ohne Ring fokussiert. Nichts geöffnet: Beim Bewegen des Mauszeigers passiert nichts.
- Mauszeiger über einem Untermenü-Trigger Nach einer kurzen Verzögerung wird eine Vorschau des Untermenüs angezeigt. Bei Mauszeigerfokus wird dieselbe Vorschau angezeigt. Ein gleichgeordnetes Element schließt es.
- In einem Menü: Mit
ArrowUpundArrowDownkönnen Sie zwischen aktivierten Elementen wechseln. MitHomeundEndwird zum ersten und letzten aktivierten Element gewechselt, das vom Skript verarbeitet wird. - Linkspfeil oder Rechtspfeil in einem geöffneten Menü der obersten Ebene Schließt das Menü und zeigt eine Vorschau des angrenzenden Menüs an, wobei der Fokus auf dem Trigger liegt. Am Ende wird das Menü umgebrochen.
ArrowDowngibt sie dann ein. Mit „Links“ und „Rechts“ gelangst du nie in ein Menü. - Rechtspfeil auf einem Untermenü-Trigger. Öffnet das Untermenü und legt den Fokus auf das erste Element.
- Pfeil nach rechts auf einem Untermenüpunkt ohne untergeordnetes Untermenü. Schließt die Hierarchie und zeigt eine Vorschau des nächsten Menüs auf oberster Ebene an, während der Fokus auf dem Trigger bleibt.
ArrowDowngibt sie dann ein. - Linkspfeil oder Escape in einem Untermenü Schließt nur dieses Untermenü und setzt den Fokus auf den Trigger zurück. Das Elternmenü bleibt geöffnet.
- Eingabetaste oder Leertaste bei einem Befehl drücken Aktiviert die Option und schließt alle geöffneten Menüs.
- Tab: Verlässt das Menüsystem. Der Fokus darf nicht hängen bleiben und es darf nicht jeder Befehl durchlaufen werden. Beim Schließen darf der Fokus nicht auf ausgeblendeten Inhalten bleiben.
Das Ereignis command wird ausgelöst, bevor toggle-popover das Menü öffnet. Der Fokus kann also nicht in das Menü verschoben werden. Verschieben Sie stattdessen den Fokus im Ereignis toggle. HTML stellt den Fokus nur für das erste popover="auto" in einem Stack wieder her. Bei verschachtelten Ebenen muss der Fokus daher explizit auf den Trigger zurückgesetzt werden.
Interaktion implementieren
Implementieren Sie die oben genannten Verhaltensweisen für Ihr eigenes Markup. Die Verarbeitung von Pfeilen, Home/End, Zeilenumbrüchen und Tab zum Beenden ist normaler keydown-Code. Einige Teile sind leicht falsch zu machen. Sie sind unten nach Eingabetyp gruppiert.
Tastatur- und Fokusverwaltung
- Ein offener Pfad. Beim Öffnen über die Tastatur wird
trigger.click()aufgerufen. Dastoggle-popoverdes Aufrufers ist also die einzige Möglichkeit, ein Menü zu öffnen. Markiere das Menü, damit dertoggle-Handler es aufruft. Die Tastatur wird nur geöffnet. Wenn ein Zeiger geöffnet wird, fokussierttogglestattdessen den Trigger ohne Ring, dapointerdownden Fokus verhindert hat und die Pfeiltasten weiterhin funktionieren müssen. - Vorschau bei Fokus: Wenn ein Trigger fokussiert wird, wird
showPopover({ source: trigger })aufgerufen, damit das Menü daran verankert wird. Ignoriere die Rückkehr des Fokus aus dem Menü des Triggers oder aus einem anderen Fenster (Flag im Fensterblur, ein Tick nach Fensterfocusgelöscht oder beim Wechseln der App wird das letzte Menü wieder geöffnet). Vorschaumenü aufblurausblenden, es sei denn, der Fokus wurde darauf verschoben. - Fokus in
toggleverschieben, nicht incommand.commandwird ausgelöst, bevor das Menü geöffnet wird. - Fokus selbst auf den Trigger zurücksetzen: Fokus auf den Trigger vor
hidePopover()legen. Nach einer Zeigeraktivierung wird der Fokus ohne Ring (focus({ focusVisible: false })) wiederhergestellt oder der Trigger zeigt nach einem Mausklick den Tastaturfokus-Umriss an. Bei Tastaturaktivierungen bleibt der Ring erhalten. Ein Klick mitevent.detail === 0kam von der Tastatur. - Schließen Sie eine Ebene nach der anderen. Rufen Sie
stopPropagation()fürEscapeundArrowLeftin einem Untermenü auf. Alternativ kann das Untermenü auch über den Listener des übergeordneten Elements geschlossen werden.
Umgang mit Zeigern
- Umschalten per Zeigerklick Rufen Sie
preventDefault()fürpointerdownauf, damit der Klick den Fokus nicht verschiebt und eine Vorschau während des Klicks startet. Merke dir, welcher Trigger gedrückt wurde. Wenn Sie auf einen geschlossenen oder in der Vorschau angezeigten Trigger klicken, wird er nativ geöffnet oder geschlossen und das Menü wird nicht aufgerufen. Das Menü wird nur geöffnet, wenn Sie die Tastatur verwenden. Fangen Sie den Klick nicht ab. - Hover folgt Fokus.
:hoverund:focus-visibleseparat gestalten. Rufe beipointermoveüber einem Elementfocus({ focusVisible: false })auf. Browser, die die Option ignorieren, zeichnen ohnehin keinen Ring für den Zeigerfokus. - Wenn Sie den Mauszeiger auf ein Menü bewegen, wird es geöffnet. Bei
pointerentereines Triggers auf oberster Ebene, während ein anderes Menü auf oberster Ebene geöffnet ist: Das Menü wird ausgeblendet, der Trigger, auf den der Mauszeiger bewegt wurde, wird ohne Ring fokussiert und die Vorschau wird beim Fokussieren angezeigt (siehe oben). Nichts geöffnet: nichts tun. - Sicheres Dreieck Eine feste CSS-Brücke ist zu schmal: Sie funktioniert nur, wenn der Mauszeiger in der Nähe des Rands des übergeordneten Elements beginnt. Lassen Sie die Brücke von CSS zeichnen und verwenden Sie das Script nur, um den Zeiger zu melden:
- Führe dies nur aus, wenn
matchMedia('(hover: hover)').matches. Für Touch- und Tastatureingaben ist es irrelevant. - Starten, wenn der Mauszeiger einen Untermenü-Trigger verlässt, dessen Untermenü geöffnet ist. Überspringe ein
--below-Untermenü, das keinen Seitenrand hat. - Legen Sie
--bridge-xund--bridge-y(den Zeiger in Pixel relativ zum Untermenü),--bridge-edge(die x-Koordinate des Untermenüs am Rand:0, wenn sich der Zeiger links davon befindet, die Breite, wenn es umgedreht wurde) und--bridge-heightfür das Untermenü fest unddata-bridge, um die Brücke zu aktivieren. - Beenden Sie die Ansage nach etwa 300 ms, wenn der Mauszeiger ein Element im Untermenü erreicht oder wenn das Untermenü geschlossen wird. Entfernen Sie den Listener und das Attribut. Danach wird nichts mehr ausgeführt.
- CSS gibt dem Untermenü einen
::before, der auf ein Dreieck vom Zeiger zu den Ecken in der Nähe zugeschnitten ist. Es befindet sich über den gleichgeordneten Elementen im Pfad des Mauszeigers, sodass diese nie das Hover-Ereignis erhalten, das das Untermenü schließen würde.clip-pathschränkt auch die Trefferprüfung ein, sodass der Rest des übergeordneten Menüs interaktiv bleibt. - Das Untermenü benötigt
overflow: visible(ein Popover wird standardmäßig abgeschnitten) und das Pseudoelement ragt darüber hinaus. Verschieben Sie die Punkte der Form also um diesen Betrag. - Follow-up: Ein gekrümmter Pfad (
shape()-Kurvenbefehle) folgt echten Zeigerbögen besser als ein gerades Dreieck.
- Führe dies nur aus, wenn
[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“ (Punkt 2) bleibt ein einfacher pointermove-Handler, für den keine Dreieckslogik erforderlich ist.
Weitere Hinweise:
- Außerhalb des Fokus. Schließen Sie alle geöffneten Menüs, wenn der Fokus außerhalb des Menüsystems liegt (ein
document-focusin-Listener). - Befehlsaktivierung Ansagen, dann das Stammmenü ausblenden und den Fokus auf den Auslöser zurücksetzen, ohne Ring, wenn die Aktivierung von einem Zeiger stammt. Warten Sie bei Elementen, die abgehakt werden können (
menuitemcheckbox,menuitemradio), etwa 300 ms, bevor Sie sie schließen, damit das geänderte Häkchen sichtbar ist. Stile einen geschlossenen Trigger nur für:focus-visibleals aktiv. Andernfalls bleibt er nach einer Auswahl mit dem Zeiger hervorgehoben, wenn der Fokus wiederhergestellt wird (ohne Ring). aria-expanded: Legen Sie diesen Wert nur imtoggle-Listener fest. Er deckt die Zustände „Vorschau“ und „Geöffnet“ ab. Deraria-expandeddes Triggers bleibt"true", während das Menü in der Vorschau angezeigt oder geöffnet wird.- Hilfskräfte: Bei den folgenden Fallbacks wird davon ausgegangen, dass Sie Hilfsprogramme für Ihr Markup definieren:
triggers(alle[commandfor]-Schaltflächen),menuFor(trigger),isOpen(menu),closeAndRestore(menu, { fromPointer })(fokussiert den Trigger, ohne Ring, wennfromPointer, dannmenu.hidePopover()),enterOnOpen(einWeakSetvon Menüs, die nach dem Öffnen aufgerufen werden) undpointerTrigger(der Trigger, der zuletzt von einem Zeiger gedrückt wurde).
Untermenüs
- Verwenden Sie
popover="auto"für Untermenüs, die nach dem Untermenü-Trigger imfocusgroupdes übergeordneten Elements platziert werden. Niepopover="manual". - Durch die Verschachtelung kann der Browser das übergeordnete Menü geöffnet lassen, gleichgeordnete Untermenüs schließen, untergeordnete Untermenüs schließen, wenn das Stammmenü geschlossen wird, und das Menü schließen, wenn außerhalb geklickt wird.
- Der Untermenü-Trigger behält
aria-haspopup="menu"undaria-expandedbei. Dertoggle-Listener hältaria-expandedsynchron. - Das Tastaturskript verarbeitet verschachtelte
Escape- undArrowLeft-Elemente wie oben beschrieben. - Ein Untermenü-Trigger führt nie auch einen Befehl aus: Ein Steuerelement kann nicht gleichzeitig aktivieren und öffnen, und
Enterkann nicht beides bedeuten. Legen Sie die Standardaktion als ersten Untermenüpunkt fest oder fügen Sie neben dem Trigger ein separates Befehlselement hinzu. Wenn ein kombiniertes Steuerelement erzwungen wird, dokumentieren Sie dies als Kompromiss bei der Barrierefreiheit.
Trennzeichen und auswählbare Elemente
- Trennen Sie Artikelgruppen mit
<hr>innerhalb der Fokusgruppe: ein nativesseparator, nicht fokussierbar, von der Fokusgruppe und dem Fallback übersprungen. Niemals ein formatiertes<div>. - Eine boolesche Option ist ein
<button type="button">mitaria-checkedund einem explizitenrole="menuitemcheckbox". Focusgroup leitet diese Rollen nicht ab. Dies ist also die einzige Stelle, an der ein gemeinsames Markup eine Elementrolle festlegt. - Eines der Elemente in einem Set ist ein
<button type="button">mitrole="menuitemradio"undaria-checkedin einemrole="group"mit einemaria-labelin derselben Fokusgruppe. - Script-Umschalter
aria-checked. Enterauf einem Kästchen aktiviert oder deaktiviert es und schließt das Menü.Spaceaktiviert oder deaktiviert es und das Menü bleibt geöffnet (APG). Die Optionsfelder werden geschlossen, wenn eine der beiden Tasten gedrückt wird.- Rendern Sie das Häkchen aus
[aria-checked="true"]::before, nicht als separates Symbolelement (siehe CSS unten).
Nicht hier abgedeckt
- Feedback zur Aktivierung In nativen Menüs blinkt das ausgewählte Element, bevor es geschlossen wird. Falls gewünscht, kann vor
hidePopover()eine visuelle Bestätigung von ca. 100 ms angezeigt werden. Die Wiederherstellung des Fokus oder die Ansage darf sich dadurch nicht verzögern. - Typeahead. Focusgroup hat keine. Falls erforderlich, werden auf druckbaren
keydowndie aktivierten Elementlabels der aktuellen Ebene abgeglichen und der nächste Abgleich nach dem aktuellen Element wird fokussiert, wobei ein Zeilenumbruch erfolgt.Spacenicht abfangen.
Vor dem Abschließen bestätigen
Eine Anweisung kann das Verhalten nicht garantieren. Testen Sie daher das Ergebnis. Öffne die Seite und überprüfe die einzelnen Elemente mit der echten Tastatur und Maus. Behebe alle Fehler und teste die Elemente noch einmal:
- Tastatur
Tablandet einmal auf dem ersten Trigger;ArrowDownöffnet das Menü;Escapeschließt eine Ebene und setzt den Fokus wieder auf den Trigger;Tabschließt alle Menüs. - Pointer geöffnet. Wenn du auf einen Trigger klickst, wird das zugehörige Menü geöffnet. Der Trigger ist dann fokussiert und es wird kein Ring angezeigt. Wenn du noch einmal darauf klickst, wird das Menü geschlossen.
- Pointer-Befehl. Wenn Sie auf einen Befehl klicken, werden alle Menüs geschlossen und es wird kein Fokusring auf dem Trigger angezeigt.
- Untermenü Wenn Sie sich diagonal von der Mitte eines Untermenü-Triggers zu einem Untermenüpunkt bewegen, bleibt das Untermenü geöffnet. Wenn Sie sich zu einem nicht zugehörigen gleichgeordneten Element bewegen, wird es geschlossen.
- Schmaler Bildschirm Bei 375 px verdeckt kein Untermenü seinen eigenen Trigger.
- Häkchen Ausgewählte Elemente sind mit einem Häkchen gekennzeichnet.
- Keine Reste. Keine Konsolenfehler und keine
popovertarget,popovertargetactionoderaria-detailsbei Triggern.
Inhalt des Menüpunkts ausrichten
Wenn eine Zeile ein Symbol, ein Label und eine Verknüpfung oder eine Untermenüanzeige enthält, reservieren Sie einen führenden Slot mit fester Größe für das Symbol oder Häkchen und verschieben Sie den nachfolgenden Inhalt an das Inline-Ende. Symbole haben eine bekannte Größe. Ein fester Slot sorgt also dafür, dass Labels ausgerichtet bleiben, unabhängig davon, ob ein Element ein Symbol hat oder nicht. Verwenden Sie subgrid nicht für Zeilen: Jedes zusätzliche untergeordnete Inline-Element (z. B. ein Badge) wird zu einem eigenen Rasterelement und beeinträchtigt das Layout. Tastenkombinationstext vor Hilfstechnologien verbergen, wenn er nur eine Taste wiederholt, die das Element bereits bedient.
[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;
}
Fügen Sie einen nachgestellten Indikator oder eine Tastenkombination in ein <span class="hint" aria-hidden="true"> innerhalb der Schaltfläche ein. Der Untermenü-Trigger kündigt sein Pop-up bereits an.
Progressive Verbesserung und Fallbacks
Fallbacks und Browserunterstützung für focusgroup
Behalten Sie focusgroup="menu nomemory" im Markup bei. Wenn 'focusGroup' in HTMLElement.prototype „false“ ist, installieren Sie die Tasten „Nach oben“, „Nach unten“, „Pos1“ und „Ende“, legen Sie role="menu" für jeden Menücontainer und role="menuitem" für Schaltflächen ohne Rolle fest. Lassen Sie auswählbare Elemente, <hr> und role="group" unverändert. Installieren Sie es niemals, wenn eine native Fokusgruppe vorhanden ist. „Left“, „Right“, „preview“, „open“ und „close“ sind in jedem Fall Autorenskripts.
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);
});
}
Fallbacks und Browserunterstützung für Popover
Die Popover API ist größtenteils eine progressive Verbesserung, aber ihre definierenden Verhaltensweisen – die Hochstufung auf die oberste Ebene, das Schließen durch Klicken außerhalb des Popovers und der Aufruf von popovertarget – haben kein CSS-Äquivalent. Ältere Browser benötigen ein Polyfill oder ein manuelles Fallback, wenn Sie kein Polyfill bereitstellen möchten.
Polyfill: Damit das Attribut popover in älteren Browsern unterstützt wird, lade @oddbird/popover-polyfill bedingt. OBLIGATORISCH: Prüfe, ob die Funktion unterstützt wird, indem du nach der Property popover in HTMLElement.prototype suchst, und lade das Polyfill nur, wenn die native Unterstützung fehlt. Lade es NICHT bedingungslos.
Mit einem Bundler oder einer Importzuordnung:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Ohne Bundler importieren Sie aus einem CDN in einem <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Einschränkung beim Styling: Das Polyfill kann die echte Pseudoklasse :popover-open nicht definieren, daher wird stattdessen die Klasse .\:popover-open angewendet. OBLIGATORISCH: Kombinieren Sie die beiden mit :is() oder :where(). Andernfalls verwerfen Browser, die :popover-open nicht unterstützen, die gesamte Regel:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Alternativ können Sie für einen Legacy-Fallback ohne Polyfill position: fixed verwenden und die Koordinaten manuell über getBoundingClientRect() berechnen oder die Standardpositionierung mit inset: auto verwenden, wenn das für den Anwendungsfall akzeptabel ist.
Fallbacks und Browserunterstützung für die Ankerpositionierung
Ohne CSS-Ankerpositionierung wird ein Popover auf die zentrierte Platzierung im Darstellungsbereich des User-Agents zurückgesetzt. Verwenden Sie das niemals für ein Menü: Es trennt das Menü von seinem Trigger und ein zentriertes Untermenü verdeckt das übergeordnete Element. Verbergen Sie die verankerten Regeln hinter @supports (position-area: block-end) und positionieren Sie das Menü über das Skript, wenn die Verankerung fehlschlägt:
- Platzieren Sie das Menü unter dem Auslöser (oberste Ebene) oder daneben (Untermenü). Wenn es überläuft, wird es auf die andere Seite verschoben und an den Darstellungsbereich angepasst.
- Führen Sie sie im
toggle-Handler aus, wenn das Menü geöffnet wird, und wenden Sie das Ergebnis alsleftundtopauf das Menü an. Verwenden Sieposition: fixedmitinset: autoundmargin: 0, damit die Koordinaten relativ zum Darstellungsbereich sind. - Behalten Sie diese Überschreibungen in
@supports not (position-area: block-end)bei, damit Browser mit Ankerpositionierung nicht betroffen sind.
Wenn Sie frühere Implementierungen verwenden, bei denen showPopover({ source }) keinen impliziten Anker herstellt (kein source für ToggleEvent.prototype), weisen Sie jedem Trigger eine eindeutige anchor-name und jedem Menü eine passende position-anchor aus dem Skript zu, anstatt CSS-Regeln pro Menü zu schreiben.
Für Browser, die überhaupt keine Ankerpositionierung unterstützen, finden Sie unter Viewport-Aware Dropdowns Informationen zur Polyfill-Route.
Fallbacks und Browserunterstützung für Invoker-Befehle
Ohne Aufrufer hat command="toggle-popover" keine Funktion. Das Menü muss also über das Script ein- und ausgeblendet werden.
- Führen Sie eine Feature-Erkennung für die Schnittstelle durch, nicht für eine Instanz:
'commandForElement' in HTMLButtonElement.prototype. - Rufe bei jedem Trigger
clickpreventDefault()auf, damit ein Browser mit Aufrufern nicht zweimal umschalten kann. - Wenn das Menü geöffnet ist, schließen Sie es mit
closeAndRestore(menu). Andernfalls rufen Siemenu.showPopover({ source: trigger })auf. - Markieren Sie das Menü in
enterOnOpen, es sei denn, der Klick stammt vom gedrückten Trigger (pointerTrigger). So wird das Menü nur über die Tastatur geöffnet. EnterundSpacefunktionieren weiterhin, da sie einen Klick auf Schaltflächen generieren.