Dialoogvenster

Overzicht

Gebruik het systeemeigen element <dialog> voor modale en niet-modale dialoogvensters. Het biedt de toegankelijkheidssemantiek, focusverwerking, weergave van de bovenste laag en Esc-verwerking die aangepaste overlays handmatig moeten herbouwen.

Het overlaytype kiezen

Kies de eerste regel die overeenkomt. Als u het niet zeker weet, gebruikt u regel 1.

Regel 1: De overlay onderbreekt de gebruiker en blokkeert de pagina (standaard)

Gebruik deze voor bevestigingen, formulieren, inloggen, destructieve acties en elk modaal, dialoog-, lightbox- of pop-upverzoek dat niet anders aangeeft.

Gebruik <dialog> geopend als een modaal, declaratief met command="show-modal" (zie Declarative Dialog and Popover Control) of vanuit een script met showModal().

<button type="button" command="show-modal" commandfor="confirm">Delete</button>

<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this item?</h2>
  <form method="dialog">
    <button value="cancel" autofocus>Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

De browser biedt: inerte pagina, focusbeperking, bovenste laag, ::backdrop, Esc en sluiten met teruggebaar, en focus terug naar de aanroeper.

  • Voeg GEEN role="dialog", aria-modal, focusval, inert-schakelaar of Esc-handler toe.
  • Open het NIET met show() of het kenmerk open.
  • WEL: Stop het scrollen van de pagina achter de overlay met de css hieronder. Ga naar Voorkomen dat de pagina onder een modaal dialoogvenster scrollt voor meer informatie.
  • Vergrendel scrollen NIET met overflow: hidden of position: fixed op html of body.
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
  overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
  overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
  overscroll-behavior: contain;
}

Niet voor menu's of tooltips: zie regel 3.

Regel 2: De gebruiker blijft op de pagina werken terwijl het deelvenster is geopend

Gebruik dit voor zoekbalken, inspecteurs, paletten en opstelvensters die semantisch nog steeds dialoogvensters zijn.

Gebruik <dialog popover> voor een deelvenster op de bovenste laag met popover-sluiting (auto sluit met een lichte tik, manual niet). Gebruik dialog.show() alleen als het deelvenster niet in de bovenste laag mag staan.

<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
  <h2 id="find-title">Find</h2>
  <input type="search" aria-label="Search">
  <button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>

De pagina blijft interactief en wordt niet inactief gemaakt.

  • WEL een zichtbare knop Sluiten opnemen.
  • Gebruik GEEN showModal(), omdat dit de pagina blokkeert.

Niet voor blokkeringsbevestigingen: zie regel 1.

Regel 3: De overlay is geen dialoogvenster

Gebruik dit voor menu's, dropdownmenu's, tooltips, hovercards en toasts.

Gebruik een semantisch element met het kenmerk popover, niet <dialog>.

<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
  <li><a href="/rename">Rename</a></li>
</ul>

Kiezen hoe de melding kan worden gesloten

De standaard hangt af van hoe het dialoogvenster wordt geopend. Als closedby ontbreekt of ongeldig is, gedraagt showModal() zich als closerequest en gedraagt elke andere manier van openen zich als none.

  • Alleen Esc- en teruggebaar (modale standaard): stel niets in of closedby="closerequest".
  • Ook buiten klikken om te sluiten: gebruik closedby="any".
  • Alleen de ontwikkelaar sluit het (bijvoorbeeld een formulier met niet-opgeslagen wijzigingen): gebruik closedby="none" en voeg een bevestigingspad toe. Zie Afsluiting hieronder.

Richtlijnen

Toegankelijke naam en eerste aandacht

  • WEL: Geef elk dialoogvenster een naam met aria-labelledby die naar de kop verwijst of aria-label als er geen zichtbare titel is.
  • WEL: Zet autofocus op het primaire bedieningselement of het eerste veld.
  • WEL: Zet autofocus op de veilige actie (zoals Annuleren of Behouden) in een bevestiging van een destructieve actie.
  • Schrijf GEEN code om de focus terug te zetten bij sluiten. De browser stuurt deze terug naar de aanroeper.

Openen

  • Open WEL declaratief met command="show-modal" en commandfor op een <button>. Ga naar Declarative Dialog and Popover Control (Declaratieve dialoogvenster- en pop-over-beheer).
  • Gebruik WEL dialog.showModal() als je opent vanuit een script.

Afsluiting

  • Sluit WEL af met <form method="dialog">-knoppen of command="close".
  • Kies closedby bewust: any (licht sluiten en Esc), closerequest (alleen Esc, de standaardinstelling voor de modus) of none (alleen sluiten door ontwikkelaar).
  • Gebruik WEL command="request-close" of requestClose() voor de knop Annuleren als de logica voor niet-opgeslagen wijzigingen moet worden gerespecteerd. Ze activeren eerst cancel en negeren closedby.
  • WEL DOEN: Behandel closedby als progressive enhancement. Esc en het mobiele gebaar voor terug sluiten al modale vensters zonder dit.

Hooking into close

  • Lees dialog.returnValue WEL in een close-listener om te weten welke knop is gebruikt en reset deze met dialog.returnValue = ''. Esc en andere verzoeken om te sluiten stellen geen waarde in. Browsers verschillen in of ze de vorige waarde wissen, dus zonder de reset kan een verouderde waarde lekken. '' behandelen als 'gesloten zonder keuze'.
  • WEL cancel gebruiken om Esc en request-close te onderscheppen, maar vertrouw niet op preventDefault() voor herhaaldelijk drukken op Esc zonder gebruikersactivatie.
  • WEL: Gebruik closedby="none" met een expliciete bevestigingsstap voor onvolledige formulieren.
  • WEL de gebruiker vertellen waarom het dialoogvenster open is gebleven als cancel wordt voorkomen: toon een zichtbaar bericht en verwijs ernaar met aria-describedby vanuit het bedieningselement dat de focus krijgt (zoals Verwijderen), zodat een schermlezer het bericht voorleest als de focus wordt verplaatst. Zonder deze functie wordt alleen de naam van de nieuwe knop aangekondigd als de focus naar een nieuwe knop wordt verplaatst.
  • Laat de bevestiging NIET de bedieningselementen onder de cursor verplaatsen. Reserveer de ruimte van het bericht (verberg het met visibility, niet met display of hidden) en voeg de destructieve knop toe op een plek waar bestaande knoppen niet worden verplaatst (bijvoorbeeld de eerste in een rechts uitgelijnde rij), zodat een tweede klik op dezelfde plek de wijzigingen van de gebruiker niet ongedaan kan maken.

Niet-modale dialoogvensters

  • Gebruik WEL <dialog popover> (auto of manual) voor een deelvenster op de bovenste laag en dialog.show() als je de bovenste laag niet wilt.
  • WEL: Voeg een zichtbare sluitknop toe aan elk niet-modaal dialoogvenster.

Achtergrond en styling

Toegankelijkheid

  • Voeg GEEN role="dialog", aria-modal, handmatige focusval of inert toe die overschakelt naar een systeemeigen modaal venster.
  • Gebruik WEL een native <button> met een toegankelijke naam voor sluitingsopties.
  • WEL: Gebruik één consistent kopniveau voor dialoogtitels.

Reservestrategieën

closedby (dialog-closedby)

Zonder closedby wordt een modaal nog steeds gesloten met Esc en het teruggebaar. Alleen klikken buiten het modale venster werkt niet meer, dus dit is graceful degradation. closedby="none" wordt op dezelfde manier genegeerd: Esc activeert cancel en sluit daarna het dialoogvenster. Houd de beveiliging voor niet-opgeslagen wijzigingen in een cancel-handler, niet alleen in closedby="none", zodat deze ook beschermt waar closedby ontbreekt. Als je Baseline-doel lichte afwijzing vereist, voeg je dit toe waar closedby ontbreekt:

if (!('closedBy' in HTMLDialogElement.prototype)) {
  // Test the pointer position, not event.target: the <dialog> is also the target for its own
  // padding and the gaps between its children, so a target check would close on those clicks
  const outside = (e, dialog) => {
    const r = dialog.getBoundingClientRect();
    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
  };
  for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
    let pressedOutside = false;
    dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
    // Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
    dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
  }
}

overscroll-behavior

Behandel scroll-containment als progressive enhancement. Gebruik geen functiedetectie: @supports meldt de property als ondersteund, zelfs als niet-scrollbare containers deze negeren. Als dit niet wordt afgedwongen, bevat het dialoogvenster nog steeds scrollen wanneer de eigen content overloopt en maakt showModal() de pagina nog steeds inactief. Val niet terug op overflow: hidden of position: fixed op html of body, waardoor de hoofdscrollbar wordt verwijderd en de indeling wordt verschoven.

Aanroeperopdrachten (command / commandfor)

Als je Baseline-doel geen Invoker-opdrachten bevat, behoud je dezelfde opmaak en laad je de polyfill voorwaardelijk (dezelfde functionaliteit, als progressive enhancement). Detecteer de interface en laad deze nooit onvoorwaardelijk:

// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill'); // npm install invokers-polyfill
}

Alleen regel 2 en 3 gebruiken Popover.

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.