Een dropdownmenu maken
In deze handleiding worden alle pop-upmenu's behandeld: dropdownmenu's, contextmenu's, overloopmenu's en de menubalken waarmee je ze opent. Het uitgewerkte voorbeeld is een opdrachtmenu van een app, waarin items de interface wijzigen (maken, opslaan, voorkeuren).
Opdrachten, links en wat een menu maakt
2 onafhankelijke keuzes. De rest van de informatie hier is van toepassing op beide.
- Element, per item. Een opdracht is een
<button type="button">. Een bestemming is een<a href>die nog steeds wordt geopend in een nieuw tabblad, wordt gekopieerd als een URL en werkt zonder script. In een dropdownmenu kunnen ze vrij worden gemengd: 2 links en 6 opdrachten of 1 opdracht en 6 links, verandert niets hieronder. - Interactiemodel, per dropdownmenu. Met de pijltoetsen,
Home/EndenEscmaak je er een menu van: voeg de focusgroep toe, laat dezemenuitemafleiden voor elk item, of het nu een knop of een link is, en laat het eruitzien als een menu (een doorlopende balk of een trigger met een dropdownindicator, zie 'Menubalk of afzonderlijke menuknoppen' hieronder). Een korte lijst met links waar gebruikers doorheenTabis navigatie: geen focusgroep, geenrole="menu",role="menuitem"ofaria-haspopup="menu". Behoud de popover,Escen focusherstel en bekijk de gids voor de navigatiebalk.
Content bepaalt nooit de rollen. Een dropdownmenu Producten met links kan een menu zijn, maar een dropdownmenu Account met opdrachten kan geen navigatie zijn.
Gebruik het menupatroon alleen voor een samengestelde widget met één tabstop en navigatie met de pijltoetsen. Gebruik een <div focusgroup="menu nomemory">, niet het HTML-element <menu>. Voeg geen role="menu" of role="menuitem" toe aan de gedeelde opmaak: de native focusgroep leidt die rollen af en expliciete rollen blokkeren die afleiding. Voeg ze alleen toe aan de reserve-items voor niet-ondersteunde browsers hieronder. nomemory zorgt dat het opnieuw openen begint bij het eerste item. Focusgroup verplaatst de focus. Het script verwerkt het voorbeeld, de activering en het herstel van de focus.
Het menu openen en positioneren
Maak van elke trigger een opdrachtaanroeper met commandfor en command="toggle-popover". De browser opent en sluit de popover bij klikken, Enter en spatie, en houdt de trigger binnen de groep voor lichte sluiting van de popover, zodat klikken op een open trigger deze sluit. Gebruik niet popovertarget of popovertargetaction. Dit patroon gebruikt commandfor.
Plaats elke popover onmiddellijk na de trigger. Browsers voegen alleen een impliciete aria-details-relatie toe als de popover niet de volgende sibling is. Deze plaatsing voorkomt dit zonder extra kenmerken.
<!-- 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>
Geef elk menu een naam met aria-labelledby die naar de trigger wijst. Houd decoratieve submenu-indicatoren in CSS of voeg aria-hidden="true" toe. Geef triggers met aria-expanded="true" een zichtbare actieve stijl, zodat gebruikers weten welk menu is geopend of wordt weergegeven als voorbeeld.
Menubalk of afzonderlijke menuknoppen
Kies één formulier voor de triggers op het hoogste niveau. De rol, het toetsenbordmodel en de stijl moeten overeenkomen: visueel gescheiden knoppen geven elk één Tab-stop aan.
- Menubalk.
role="menubar"op de container (nooit op een pop-over), opgemaakt als één doorlopende balk zonder gaten tussen triggers. EénTab-stop;ArrowLeft/ArrowRightom tussen triggers te schakelen; plaats de muisaanwijzer op een menu om het te openen. Gebruik voor een opdrachtbalk van een app. - Afzonderlijke menuknoppen. Elke trigger is een eigen
Tab-stop en ziet eruit als een losse knop. Geenrole="menubar", geen pijltoetsen tussen triggers, geen voorbeeld bij focus, geen schakelen bij aanwijzen. Verpak in<nav>als ze navigeren, anders geen rol. Gebruik deze voor werkbalken en paginakopteksten.
In de rest van deze gids wordt het formulier voor de menubalk beschreven. Voor afzonderlijke knoppen houd je alles in elk menu en laat je het gedrag op menubalkniveau vallen.
Het menu positioneren met CSS-ankerpositionering
Gebruik CSS-ankerpositionering, geen JavaScript-coördinaten. Zowel command="toggle-popover" als showPopover({ source: trigger }) stellen een impliciet anker in, dus voeg geen anchor-name- of position-anchor-regels per menu toe. Menugedrag mag niet afhankelijk zijn van ankerpositionering.
/* 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);
}
Zet --below als laatste, zodat deze alleen wordt toegepast als geen van beide kanten past. Het menu wordt direct onder de trigger geopend, dus op een smal scherm bedekt het de items onder de trigger. Dit is de geaccepteerde afweging om naast de trigger te blijven. Laat het submenu ook krimpen tot het gebied (min-inline-size: 0, max-inline-size min de inspringing). Een fallback die niet past, wordt overgeslagen. Als geen enkele fallback past, behoudt de browser de basisplaatsing. Een submenu dat een paar pixels te breed is, bedekt dan de trigger zonder dat de gebruiker dit merkt.
Ga naar Viewport-Aware Dropdowns voor meer informatie over hoe de reserves voor omdraaien werken in een gewone popover. In die handleiding wordt een knoppengroep geplaatst, geen menu. Voor menu's hebben de plaatsing van het submenu en de --below-terugval hierboven voorrang.
Openen en sluiten animeren
Optioneel. Minder dan 150 ms, alleen opacity en een kleine translate, hetzelfde voor submenu's, binnen @media (prefers-reduced-motion: no-preference). De focus gaat verder op toggle, zodat de overgang toetsenbordgebruikers nooit vertraagt.
@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 is geen herhaling van [popover]: een gesloten popover is display: none en overgangen worden niet uitgevoerd vanuit een niet-gerenderde status, dus deze levert de voor-waarden voor de opening. Zonder deze functie wordt de open stand vastgezet en wordt alleen de sluiting geanimeerd. display en overlay met allow-discrete zorgen dat de popover wordt weergegeven en in de bovenste laag blijft totdat de sluitingstransitie is afgelopen.
Toetsenbord en focus
Voeg geen tweede roving-tabindex-systeem toe aan een focusgroep. Het script behandelt alleen wat de aanroepers en focusgroep niet doen:
- Eén tabstop voor de menubalk.
Tabkomt binnen bij de eerste trigger op het hoogste niveau.ArrowLeftenArrowRightbewegen tussen triggers op het hoogste niveau en gaan aan beide kanten terug naar het begin.HomeenEndop een trigger openen het menu bij het eerste of laatste item.- Opmerking: naar welke trigger
Tabterugkeert. De APG staat de eerste trigger of de laatst gebruikte trigger toe. In de gids staat de eerste, maar in de demo wordt de tabstop verplaatst metArrowLeftenArrowRight, dus na toetsenbordnavigatie keertShift+Tabterug naar de laatste trigger die is bereikt. Het gebruik van de aanwijzer verplaatst de aanwijzer niet, dus dezelfde reeks kan op een andere trigger terechtkomen. Beide gedragingen zijn acceptabel. Kies er een en houd deze consistent.
- Opmerking: naar welke trigger
- Voorbeeld. Als je de focus op een trigger op het hoogste niveau plaatst, worden het menu en de sets van de trigger getoond
aria-expanded="true". De focus blijft op de trigger en er wordt geen item geselecteerd. Als je de trigger vervaagt, wordt een menu met alleen een voorbeeld gesloten. - Enter en spatie in een voorbeeld. Ze sluiten het voorbeeld en houden de focus op de trigger. De systeemeigen actie
toggle-popoververwerkt dit, dus het script mag deze toetsen niet onderscheppen. - Pijl-omlaag op een voorbeeld. Hiermee wordt het menu geopend en wordt het eerste item geselecteerd.
- Klik met de muisaanwijzer op een geopende of voorbeeldtrigger. Sluit het menu op de normale manier. Het menu wordt niet geopend.
- Bekend verschil in submenu-triggers. Een muisklik op een submenu-trigger schakelt ook standaard, zodat een submenu dat al een voorbeeld heeft gegeven, kan worden gesloten of een submenu dat nog niet is verschenen, kan worden geopend. Systeemeigen menu's openen het alleen en sluiten het nooit. Dit is onschadelijk, dus de gids accepteert het.
- Cursor klikt op een gesloten trigger. Opent het bestand in de oorspronkelijke app. De focus blijft op de trigger. Er is geen item geselecteerd. Alleen als je het toetsenbord opent, wordt het menu geopend.
- Plaats de cursor op een item. Focust zonder ring, dus plaatsen en focussen zijn één item.
- De cursor over een andere trigger op het hoogste niveau plaatsen terwijl een menu is geopend. Sluit het en toont een voorbeeld van het item waar de cursor op staat, waarbij de trigger zonder ring wordt gefocust. Niets geopend: de muisaanwijzer doet niets.
- Cursor op een submenutrigger. Toont na een korte vertraging een voorbeeld van het submenu (aanwijzerfocus voert hetzelfde voorbeeld bij focus uit). Een item op hetzelfde niveau sluit het.
- In een menu.
ArrowUpenArrowDownwisselen tussen aangezette items.HomeenEndgaan naar het eerste en laatste aangezette item, dat door het script wordt verwerkt. - Pijl-links of pijl-rechts in een geopend menu op het hoogste niveau. Sluit het menu en toont een voorbeeld van het aangrenzende menu met de focus op de trigger, waarbij aan beide kanten wordt teruggelopen.
ArrowDownvoert deze dan in. Links en Rechts komen nooit in een menu terecht. - ArrowRight op een submenutrigger. Opent het submenu en plaatst de focus op het eerste item.
- ArrowRight op een submenu-item zonder onderliggend submenu. Sluit de hiërarchie en toont een voorbeeld van het volgende menu op het hoogste niveau terwijl de focus op de trigger blijft.
ArrowDownvoert deze dan in. - ArrowLeft of Escape in een submenu. Sluit alleen dat submenu en herstelt de focus op de trigger. Het menu voor ouders blijft open.
- Enter of spatie op een opdracht. Hiermee activeer je de optie en sluit je alle geopende menu's.
- Tabblad. Verlaat het menusysteem. Zorg dat de focus niet vast komt te zitten en dat je niet door elke opdracht hoeft te stappen. Na het sluiten mag de focus niet op verborgen content blijven staan.
De gebeurtenis command wordt geactiveerd voordat toggle-popover het menu opent, dus de focus kan daar niet naar het menu worden verplaatst. Verplaats de focus in plaats daarvan in de gebeurtenis toggle. HTML herstelt de focus alleen voor de eerste popover="auto" in een stapel, dus geneste niveaus moeten de focus expliciet herstellen naar hun trigger.
De interactie implementeren
Implementeer het bovenstaande gedrag voor je eigen opmaak. Pijl, Home/End, terugloop en Tab-to-exit-afhandeling is gewone keydown-code. Sommige onderdelen zijn makkelijk verkeerd te doen. Deze zijn hieronder gegroepeerd op invoertype.
Toetsenbord- en focusverwerking
- Eén open pad. Als je het menu opent met het toetsenbord, wordt
trigger.click()aangeroepen. Detoggle-popovervan de aanroeper is dus de enige manier om een menu te openen. Markeer het menu zodat detoggle-handler het opent. Alleen het toetsenbord wordt geopend. Als een aanwijzer wordt geopend, richttogglezich op de trigger zonder ring, omdatpointerdownfocus heeft voorkomen en de pijltoetsen nog steeds moeten werken. - Voorbeeld bij focus. Als je een trigger activeert, wordt
showPopover({ source: trigger })aangeroepen, zodat het menu eraan wordt verankerd. Focus negeren die terugkeert vanuit het eigen menu van de trigger of vanuit een ander venster (vlag op vensterblur, vinkje verwijderd na vensterfocusof schakelen tussen apps opent het laatste menu opnieuw). Een voorbeeldmenu verbergen opblur, tenzij de focus ernaartoe is verplaatst. - Verplaats de focus in
toggle, niet incommand.commandwordt geactiveerd voordat het menu wordt geopend. - Herstel de focus zelf naar de trigger. Focus de trigger vóór
hidePopover(). Herstel na een aanwijzeractivering de focus zonder een ring (focus({ focusVisible: false })) of de trigger toont de toetsenbordfocusomtrek na een muisklik. Toetsenbordactiveringen behouden de ring. Een klik metevent.detail === 0kwam van het toetsenbord. - Sluit één niveau tegelijk. Roep
stopPropagation()aan opEscapeenArrowLeftin een submenu, anders sluit de listener van het bovenliggende element het ook.
Aanwijzerverwerking
- Schakelaars voor aanwijzerklikken. Roep
preventDefault()aan oppointerdownzodat de klik de focus niet verplaatst en een voorbeeld start tijdens de klik. Onthoud welke trigger is ingedrukt. Als je op een gesloten of voorbeeldtrigger klikt, wordt deze standaard geopend of gesloten en wordt het menu nooit geopend. Alleen als je het toetsenbord opent, wordt het menu geopend. Onderschep de klik niet. - De cursor volgt de focus. Stijl
:hoveren:focus-visibleafzonderlijk. Als je de muisaanwijzer oppointermoveplaatst, wordtfocus({ focusVisible: false })aangeroepen. Browsers die de optie negeren, tekenen geen ring voor de focus van de muisaanwijzer. - Als je de muisaanwijzer op een menu plaatst, wordt het geopend. Bij
pointerentervan een trigger op het hoogste niveau terwijl een ander menu op het hoogste niveau is geopend: verberg het, focus de trigger waar de muisaanwijzer op staat zonder een ring en een voorbeeld bij focus (zie hierboven) toont een voorbeeld. Niets open: doe niets. - Veilige driehoek. Een vaste CSS-brug is te smal: deze werkt alleen als de cursor dicht bij de rand van het bovenliggende element begint. Laat CSS de brug tekenen en gebruik het script alleen om de aanwijzer te rapporteren:
- Voer dit alleen uit als
matchMedia('(hover: hover)').matches. Dit is niet relevant voor aanraken en toetsenbord. - Start wanneer de cursor een submenu-trigger verlaat waarvan het submenu is geopend. Sla een submenu van
--belowover, dat geen zijrand heeft. - Stel
--bridge-xen--bridge-y(de aanwijzer, in px ten opzichte van het submenu),--bridge-edge(de x van de dichtstbijzijnde rand van het submenu:0als de aanwijzer zich links ervan bevindt, de breedte als deze is omgedraaid) en--bridge-heightin op het submenu endata-bridgeom de brug aan te zetten. - Stop na ongeveer 300 ms, wanneer de muisaanwijzer een item in het submenu bereikt of wanneer het submenu wordt gesloten. Verwijder de listener en het kenmerk. Daarna wordt er niets meer uitgevoerd.
- CSS geeft het submenu een
::beforedie is bijgesneden tot een driehoek van de aanwijzer naar de hoeken aan de rand. Het element bevindt zich boven de onderliggende items op het pad van de cursor, zodat ze nooit de muisaanwijzer krijgen die het submenu zou sluiten.clip-pathbeperkt ook hit-tests, zodat de rest van het bovenliggende menu interactief blijft. - Het submenu heeft
overflow: visiblenodig (een popover wordt standaard afgekapt) en het pseudo-element steekt er voorbij, dus verschuif de punten van de vorm met die mate. - Vervolg: een gebogen pad (
shape()curve-opdrachten) volgt echte aanwijzerbogen beter dan een rechte driehoek.
- Voer dit alleen uit als
[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 (item 2) blijft een gewone pointermove-handler: er is geen driehoekslogica nodig.
En verder:
- Buiten de focus. Sluit elk geopend menu als de focus buiten het menusysteem terechtkomt (een
documentfocusin-listener). - Opdrachtactivering. Kondig dit aan, verberg het hoofdmenu en herstel de focus naar de trigger, zonder een ring als de activering afkomstig was van een aanwijzer. Wacht voor items die kunnen worden aangevinkt (
menuitemcheckbox,menuitemradio) ongeveer 300 ms voordat je sluit, zodat het gewijzigde vinkje zichtbaar is. Stijl een gesloten trigger alleen als actief op:focus-visible. Anders blijft de herstelde focus (zonder ring) gemarkeerd na een aanwijzerselectie. aria-expanded. Stel deze alleen in detoggle-listener in. Dit geldt voor de voorbeeld- en geopende status. Dearia-expandedvan de trigger blijft"true"terwijl het menu wordt getoond of geopend.- Helpers. De onderstaande fallbacks gaan uit van helpers die je definieert voor je opmaak:
triggers(alle[commandfor]-knoppen),menuFor(trigger),isOpen(menu),closeAndRestore(menu, { fromPointer })(focus op de trigger, zonder ring alsfromPointer, danmenu.hidePopover()),enterOnOpen(eenWeakSetvan menu's die je kunt openen als ze eenmaal zijn geopend) enpointerTrigger(de trigger die het laatst is ingedrukt door een aanwijzer).
Submenu's
- Gebruik
popover="auto"voor submenu's, geplaatst na de submenu-trigger in defocusgroupvan het bovenliggende element. Nooitpopover="manual". - Met nesten kan de browser het bovenliggende menu openhouden, subsubmenu's sluiten, onderliggende submenu's sluiten als het hoofdmenu wordt gesloten en het sluiten van het menu afhandelen als er buiten het menu wordt geklikt.
- De submenu-trigger behoudt
aria-haspopup="menu"enaria-expanded. Detoggle-listener houdtaria-expandedgesynchroniseerd. - Het toetsenbordscript verwerkt geneste
EscapeenArrowLeftzoals hierboven beschreven. - Een submenu-trigger voert nooit ook een opdracht uit: één bedieningselement kan niet zowel activeren als openen, en
Enterkan niet beide betekenen. Zet de standaardactie als het eerste submenu-item of voeg een afzonderlijk opdrachtitem toe naast de trigger. Als een gecombineerde bediening wordt afgedwongen, documenteer je dit als een toegankelijkheidscompromis.
Scheidingstekens en aanvinkbare items
- Afzonderlijke productgroepen met
<hr>in de focusgroep: een nativeseparator, niet focusbaar, overgeslagen door focusgroep en de fallback. Nooit een gestileerde<div>. - Een booleaanse optie is een
<button type="button">metaria-checkeden een explicieterole="menuitemcheckbox". Focusgroup leidt deze rollen niet af, dus dit is de enige plek waar gedeelde opmaak een itemrol instelt. - Een van de sets is een
<button type="button">metrole="menuitemradio"enaria-checked, in eenrole="group"met eenaria-label, in dezelfde focusgroep. - Scriptschakelaars
aria-checked. Enterop een selectievakje schakelt de optie in of uit en sluit het menu.Spaceschakelt de optie in of uit en houdt het menu open (APG). Radio's sluiten met beide toetsen.- Geef het vinkje weer vanuit
[aria-checked="true"]::before, niet als een afzonderlijk icoonelement (zie de CSS hieronder).
Hier niet behandeld
- Feedback over activering. Systeemeigen menu's laten het gekozen item knipperen voordat ze worden gesloten. Toon indien gewenst een visuele bevestiging van ongeveer 100 ms vóór
hidePopover(). Vertraag het herstel van de focus of de aankondiging niet. - Typeahead. Focusgroep heeft geen. Pas indien nodig op afdrukbare
keydownde aangezette itemlabels van het huidige niveau aan en focus de volgende overeenkomst na het huidige item, met terugloop. OnderschepSpaceniet.
Verifiëren voordat je afrondt
Een instructie kan geen gedrag garanderen, dus test het resultaat. Open de pagina en controleer elk van deze met het echte toetsenbord en de echte muis. Los eventuele problemen op en test opnieuw:
- Toetsenbord.
Tablandt één keer op de eerste trigger,ArrowDownopent,Escapesluit één niveau en zet de focus terug op de trigger,Tabsluit elk menu. - Cursor openen. Als je op een trigger klikt, wordt het menu geopend met de focus op de trigger en zonder ring. Als je nog een keer klikt, wordt het menu gesloten.
- Opdracht voor aanwijzer. Als je op een opdracht klikt, worden alle menu's gesloten en wordt er geen focusring op de trigger getoond.
- Submenu. Als je diagonaal van het midden van een submenutrigger naar een submenu-item gaat, blijft het submenu geopend. Als je naar een niet-gerelateerd item op hetzelfde niveau gaat, wordt het submenu gesloten.
- Smal scherm. Bij 375 px bedekt geen enkel submenu de eigen trigger.
- Vinkjes. Aangevinkte items hebben een vinkje.
- Geen restjes. Geen consolefouten en geen
popovertarget,popovertargetactionofaria-detailsvoor triggers.
Content van menu-items uitlijnen
Als een rij een icoon, label en sneltoets of submenu-indicator heeft, reserveer je een voorloopruimte van vaste grootte voor het icoon of vinkje en verplaats je de volgcontent naar het inline einde. Iconen hebben een bekend formaat, dus een vaste ruimte zorgt dat labels uitgelijnd blijven, of een item nu een icoon heeft of niet. Gebruik subgrid niet voor rijen: een extra inline onderliggend item (bijvoorbeeld een badge) wordt een eigen rasteritem en verstoort de vormgeving. Verberg de tekst van de sneltoets voor ondersteunende technologie als deze alleen een toets herhaalt die het item al 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;
}
Plaats een indicator of sneltoets aan het einde in een <span class="hint" aria-hidden="true"> in de knop. De submenu-trigger kondigt de pop-up al aan.
Progressive enhancement en fallbacks
Fallbacks en browserondersteuning voor focusgroep
Laat focusgroup="menu nomemory" in de opmaak staan. Als 'focusGroup' in HTMLElement.prototype onwaar is, installeer je Omhoog, Omlaag, Home en End, stel je role="menu" in voor elke menucontainer en role="menuitem" voor knoppen zonder rol. Laat items die kunnen worden aangevinkt, <hr> en role="group" met rust. Installeer deze nooit als er een systeemeigen focusgroep aanwezig is. Links, rechts, voorbeeld, openen en sluiten zijn in beide gevallen auteursrechten.
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 en browserondersteuning voor Popover
De Popover API is voornamelijk progressive enhancement, maar de bepalende gedragingen (promotie van de bovenste laag, licht sluiten en aanroepen van popovertarget) hebben geen equivalent dat alleen CSS gebruikt. Oudere browsers hebben een polyfill nodig, of een handmatige fallback als je er liever geen verzendt.
Polyfill: Als je het kenmerk popover wilt ondersteunen in oudere browsers, laad je @oddbird/popover-polyfill voorwaardelijk. VERPLICHT: Voer een functiedetectie uit door te controleren op de property popover op HTMLElement.prototype en laad de polyfill alleen als systeemeigen ondersteuning ontbreekt. Laad de polyfill NIET onvoorwaardelijk.
Met een bundler of importtoewijzing:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Zonder bundler importeer je vanuit een CDN in een <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Opmerking over de stijl: De polyfill kan de echte pseudoklasse :popover-open niet definiëren, dus wordt in plaats daarvan de klasse .\:popover-open toegepast. VERPLICHT: Combineer de 2 met :is() of :where(), anders negeren browsers zonder :popover-open de hele regel:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Als alternatief kun je voor een verouderde fallback zonder polyfill position: fixed gebruiken en de coördinaten handmatig berekenen via getBoundingClientRect() of vertrouwen op de standaardpositionering met inset: auto als dat acceptabel is voor de use case.
Fallbacks en browserondersteuning voor ankerpositionering
Zonder CSS-ankerpositionering valt een popover terug naar de gecentreerde viewport-plaatsing van de UA. Gebruik dat nooit voor een menu: het menu wordt dan losgekoppeld van de trigger en een gecentreerd submenu bedekt het bovenliggende menu. Beperk de verankerde regels achter @supports (position-area: block-end) en positioneer het menu vanuit het script als dit mislukt:
- Plaats het menu onder de trigger (hoogste niveau) of ernaast (submenu). Draai het menu naar de andere kant en klem het vast aan de viewport als het overloopt.
- Voer deze uit in de
toggle-handler als het menu wordt geopend en pas het resultaat toe alsleftentopin het menu. Gebruikposition: fixedmetinset: autoenmargin: 0zodat de coördinaten relatief zijn aan de viewport. - Houd deze overschrijvingen binnen
@supports not (position-area: block-end), zodat browsers met ankerpositionering niet worden beïnvloed.
Als je eerdere implementaties target waarbij showPopover({ source }) geen impliciet anker tot stand brengt (geen source op ToggleEvent.prototype), geef je elke trigger een unieke anchor-name en elk menu een overeenkomende position-anchor vanuit het script, in plaats van css-regels per menu te schrijven.
Voor browsers zonder ankerpositionering ga je naar Viewport-Aware Dropdowns voor de polyfill-route.
Fallbacks en browserondersteuning voor Invoker-opdrachten
Zonder aanroepers doet command="toggle-popover" niets, dus het script moet het menu aan- of uitzetten.
- Detecteer de functie van de interface, niet van een instantie:
'commandForElement' in HTMLButtonElement.prototype. - Roep bij elke trigger
clickpreventDefault()aan, zodat een browser met aanroepers niet 2 keer kan schakelen. - Als het menu is geopend, sluit je het met
closeAndRestore(menu). Bel andersmenu.showPopover({ source: trigger }). - Markeer het menu in
enterOnOpen, tenzij de klik afkomstig is van de ingedrukte trigger (pointerTrigger), zodat alleen toetsenbordopeningen het menu openen. EnterenSpacewerken nog wel, omdat ze een klik op knoppen genereren.