Gratulujeme! Úspěšně jste se přihlásili k odběru novinek Frontend Garden.
Ajaj. Při pokusu o přihlášení k odběru novinek došlo k chybě. Zkuste to prosím znovu.
Frontend Garden
  • Články
  • Slovník nové
  • Instagram
  • GitHub
12. duben 2023 —
  • HTML,
  • JavaScript
— čtení na 7 min.

Element <dialog>: nevymýšlej znovu modál!

Vídáme je všude. Vyskakují na nás s potvrzením cookies, odesíláme přes ně tweety, otevírají se v nich menu či jenom prostá upozornění. Každý je implementuje po svém, a přitom již dospělo nativní řešení v podobě elementu <dialog>. Pojďme ho poznat společně do hloubky.

Tomáš Litera

Tomáš Litera

Tomáš je webový vývojář, kajakář a skaut z Kolína. Zaměřuje se na React, Node a serverless řešení, ale i HTML, CSS a JS obecně. Rád objevuje nové řeky a učí mladé lidi, jak přežít v přírodě. Více o autorovi

Každá UI knihovna pojmenovává komponenty trochu jinak. Co se týká anatomie, eventuálně implementace, tak jsou si často některé komponenty podobné (byť každá může být vhodná pro jiné účely). S variantou jedné takové se ale setkáme snad všude. A to je Modal. Někde se jmenuje Dialog či Popup. V různých mutacích se pak ještě objevuje jako Drawer či Offcanvas (dialog, který vyjíždí ze strany). Všechny ale mají jedno společné. Objeví se nad současným rozhraním a prioritizují pro uživatele konkrétní akci či informaci. A ve všech případech je jejich implementace dosti složitá. Pojďme se tedy podívat, jak si takovou implementaci zjednodušit a zpříjemnit pomocí nativního elementu <dialog>.

Seznamte se, <dialog> element

Anatomie komponenty Dialog
Anatomie komponenty Dialog — Zdroj: Adobe Spectrum
Anatomie komponenty Modal
Anatomie komponenty Modal — Zdroj: UX Movement

Část z nás se jistě při vytváření a stylování modálu již určitě zapotila. Vždy je to stále dokola to samé:

  • postavit dialog a jeho obsah,
  • udělat overlay se ztmaveným backdropem,
  • vytvořit API pro otevírání a zavírání modálu,
  • a nakonec vyřešit zavírání při stisknutí klávesy Escape, kliknutí mimo <dialog> (na backdrop) a jiné okrajové případy (třeba zabránit scrollování na stránce).

Tolik věcí se opakuje, a přitom tu máme element <dialog>, který nám tohle (téměř) všechno servíruje na stříbrném podnosu. A ještě k tomu řeší i přístupnost.

První náhled na element <dialog> už nám udělal Tomáš Pustelník ve své přednášce na WebExpu – HTML can do that. Pojďme si to zrekapitulovat.

Samotný dialog je relativně jednoduchý element. Nemá žádné vlastnosti až na atribut open, který určuje, jestli bude dialog při načtení stránky otevřen. Zde však pozor, takto otevřený dialog je tzv. nemodální (mj. nelze zavřít klávesou Escape) a v samotném HTML neexistuje žádný způsob, jak jednou zavřený dialog znovu otevřít. Ovládání výlučně přes JavaScript je proto preferovanou variantou. Ale o tom později.

<dialog open>
  <p>Greetings, one and all!</p>
  <form method="dialog">
    <button>OK</button>
  </form>
</dialog>
Výchozí vzhled elementu <dialog>
Výchozí vzhled elementu <dialog>
Non-modal dialogs: use them for non-critical interactions that don't block the user (like toasters).
Nemodální dialogy jsou určeny pro nekritické interakce, které neblokují uživatele. Zdroj: MODALZ MODALZ MODALZ

Zajímavé to začíná být až v kombinaci s elementem <form>, pokud má atribut method="dialog", anebo pokud má jeho odesílací tlačítko nastaveno formmethod="dialog". (Tlačítko musí být typu submit, což je ostatně výchozí hodnota atributu type pro <button>.) V takovém případě je po stisknutí tlačítka stav formuláře uložen, dialog zavřen a návratová hodnota dialog.returnValue je nastavena na hodnotu tlačítka.

<main>
  <button id="open">Open dialog</button>
  <dialog>
    <form method="dialog">
    <h1>Hello, would you like to do something cool?<h1/>
    <button id="close" value="cancel">Go to hell!</button>
    <button id="confirm" value="confirm">Let's go!</button>
  </dialog>
</main>

Tím to však nekončí. Element <dialog> přidává nově také pseudoelement ::backdrop. Díky tomu lze snadno nastylovat vrstvu, která se zobrazuje za dialogem – typicky jako ztmavení nedosažitelného obsahu.

dialog::backdrop {
  background-color: rgba(0 0 0 / 50%);
  visibility: visible;
  opacity: 1;
}

Zavři mě a otevři mě, samozřejmě JavaScriptem

A nyní k ovládání, protože ne všechno funguje samo a trocha JavaScriptu je vždy potřeba. Nicméně i zde nám <dialog> šetří psaní a nabízí nám hned několik možností, jak s ním interagovat.

Metody

.show()

Rovnou začnu metodou, která může zprvu vyvolat dojem nekonzistentního API. Metoda show() totiž nezobrazí dialog, jak by vývojář předpokládal, tedy v modálním stavu. Tato metoda otevírá Dialog v takzvaném nemodálním stavu, kdy interakce s obsahem mimo dialog je nadále povolena. Je to podobné jako s atributem open, který jsem popisoval na začátku.

.showModal()

Tohle je ta pravá metoda, kterou chcete použít k otevírání dialogu. Dojde k otevření dialogu tak, jak jsme všichni zvyklí, přes všechny další dialogy do nejsvrchnější vrstvy. Aktivuje se ::backdrop pseudoelement a interakce s obsahem mimo dialog je blokována. (Bohužel, posouvání obsahu pod dialogem blokováno není – zatím.)

.close()

Zavírání dialogu už je snadné, k tomu slouží jediná metoda, která navíc jako argument přijímá návratovou hodnotu, kterou chceme dostat do dialog.returnValue.

Události

Element <dialog> vyvolává dvě události.

close

Událost close je vyvolána, když je dialog zavřen tlačítkem.

cancel

Událost cancel je vyvolána, když je dialog zrušen, např. klávesou Escape. Už to samotné automaticky dialog zavírá a nám tak odpadá další část implementace.

<script>
  (() => {
    const updateButton = document.getElementById('updateDetails');
    const closeButton = document.getElementById('close');
    const dialog = document.getElementById('favDialog');
    dialog.returnValue = 'favAnimal';

    function openCheck(dialog) {
      if (dialog.open) {
        console.log('Dialog open');
      } else {
        console.log('Dialog closed');
      }
    }

    // Otevření modálního dialogu
    updateButton.addEventListener('click', () => {
      dialog.showModal();
      openCheck(dialog);
    });

    // Zavření dialogu
    closeButton.addEventListener('click', () => {
      dialog.close('animalNotChosen');
      openCheck(dialog);
    });
  })();
</script>

Pak už stačí jen vyřešit zavírání při kliknutí na backdrop a je hotovo. Ale i zde je řešení jednoduché, neboť kliknutí na backdrop dostává jako event.target daný dialog. Tím pádem stačí pouze porovnat, zda uživatel kliknul do obsahu dialogu, anebo na samotný element dialogu, a ten případně zavřít.

Element <dialog> zabírá celou obrazovku a vykresluje se v nejvyšší vrstvě (top layer)
Element <dialog> se vykresluje v nejvyšší vrstvě, mimo běžný tok dokumentu. Zdroj: Spirit Design System
Content + Backdrop
Okno dialogu i backdrop si ale musíme nastylovat sami. Zdroj: Spirit Design System

A co React?

Také implementace v Reactu se podstatně zjednodušila. Začněme samotnou komponentou Modal využívající element <dialog> a referenci na něj.

function Modal({ children }) {
  const dialogRef = React.useRef(null);

  return <dialog ref={dialogRef}>{children}</dialog>;
}

Dále potřebujeme ovládat otevřený a zavřený stav pomocí metod showModal() a close(), jež jsme popsali výše.

function Modal({ children, open }) {
  const dialogRef = React.useRef(null);

  React.useEffect(() => {
    const dialogNode = dialogRef.current;

    if (open) {
      dialogNode.showModal();
    } else {
      dialogNode.close();
    }
  }, [open]);

  return <dialog ref={dialogRef}>{children}</dialog>;
}

Poté už je potřeba pouze dořešit, co se má stát, když uživatel stiskne Escape a dojde k vyvolání události cancel.

function Modal({ children, open, onRequestClose }) {
  const dialogRef = React.useRef(null);

  React.useEffect(() => {
    const dialogNode = dialogRef.current;

    if (open) {
      dialogNode.showModal();
    } else {
      dialogNode.close();
    }
  }, [open]);

  React.useEffect(() => {
    const dialogNode = dialogRef.current;
    const handleCancel = (event) => {
      event.preventDefault();
      onRequestClose();
    };

    dialogNode.addEventListener('cancel', handleCancel);

    return () => {
      dialogNode.removeEventListener('cancel', handleCancel);
    }
  }, [onRequestClose]);

  return <dialog ref={dialogRef}>{children}</dialog>;
}

A jako třešničku na dortu můžeme přidat vrácení focusu na element, který modal otevřel, jak doporučuje WAI-ARIA.

function Modal({ children, open, onRequestClose }) {
  //  …
  const lastActiveElement = React.useRef(null);

  React.useEffect(() => {
    const node = ref.current;

    if (open) {
      lastActiveElement.current = document.activeElement;
      node.showModal();
    } else {
      node.close();
      lastActiveElement.current.focus();
    }
  }, [open]);
  // …
}

Anebo si můžete celou implementaci rozložit na hooky a základní komponentu Dialog. Modal si pak už jen sestavíte jako LEGO, jako jsme to udělali i my ve Spirit Design System.

Co si z toho odnést

Výhody

  • Je potřeba napsat minimum JavaScriptu,
  • přístupnost je takřka vyřešena až na pár problémů s autofocusy,
  • existuje pseudoelement ::backdrop, který lze jednoduše stylovat (ale pozor, ne animovat – to se zatím obchází pseudoelementem ::before nebo ::after),
  • formulářový atribut method="dialog" zpřístupňuje hodnoty z formuláře a tlačítek při akci v dialogu,
  • stisknutí klávesy Escape dialog automaticky zavře.

Nevýhody

  • Rozšířenější funkcionalita potřebuje více JavaScriptu, ale zase ne o tolik ;-),
  • kliknutí na backdrop nezavře dialog automaticky – k tomu je potřeba trochu JavaScriptu,
  • některé drobnější problémy s přístupností, viz autofocusy.

Podpora v prohlížečích

Jak je vidět v následující tabulce, element <dialog> je možné již dnes využít v drtivé většině prohlížečů, přičemž na řešení dalších problémů se pracuje.

Podpora značky <dialog> v prohlížečích
Podpora značky <dialog> v prohlížečích k 12. 4. 2023 — Zdroj: CanIUse

Pokud tedy váš web nepotřebuje nějaké specialitky jako třeba podporu hodně starých verzí prohlížečů, případně nemáte extrémně omezené zdroje či čas, určitě vám doporučuji na element <dialog> přejít. Vždy asi budou nějaké případy, kdy vám nezbude než upřednostnit role="dialog" před značkou <dialog>, ale takových už bude jen a jen méně.

Nechcete-li však přidávat do projektu další, byť sebemenší závislosti jen proto, abyste mohli používat modál, sáhněte po elementu <dialog>. Nechcete přece znovu vymýšlet kolo, tedy pardon – dialog :-).

Související odkazy

  • <dialog>: The Dialog Element (MDN)
  • Use the dialog element (reasonably) (Scott O'Hara, 2023)
  • Build a Dialog Component in React (Travis Arnold, souporserious.com, 2020)
  • Building a dialog component (Adam Argyle, web.dev, 2022)

Anglickou verzi článku najdete na webu autora.


Tomáš Litera

Tomáš Litera

Tomáš je webový vývojář, kajakář a skaut z Kolína. Zaměřuje se na React, Node a serverless řešení, ale i HTML, CSS a JS obecně. Rád objevuje nové řeky a učí mladé lidi, jak přežít v přírodě. Více o autorovi

Další články od autora:

  • Jak předcházet chaotické organizaci npm skriptů
Více k tématu:
  • HTML
  • JavaScript

Sdílejte

Líbí se vám článek? Podpořte jej sdílením!

X (Twitter) Facebook LinkedIn

Komentujte

Chcete k článku něco doplnit? Našli jste chybu? Napište e-mail.

Adam Kudrna

Nejnovější články

WebExpo — čtení na 3 min.

Pozvánka na WebExpo 2025

Rok se s rokem sešel, přichází měsíc květen a s ním WebExpo 2025, které se po nejistých letech usadilo v tomto jarním termínu. Jaké bude?

  • Adam Kudrna
    Adam Kudrna
Pozvánka na WebExpo 2025
JavaScript — čtení na 10 min.

Jak předcházet chaotické organizaci npm skriptů

Řešíte, jak organizovat skripty napříč projekty přehledně a konzistentně? Inspirujte se systémem, který zjednoduší práci vám i kolegům.

  • Tomáš Litera
    Tomáš Litera
Jak předcházet chaotické organizaci npm skriptů
Reportáž — čtení na 2 min.

FrontKon 2024 obrazem

Frontendisti letos připravili třetí ročník konference FrontKon. Ta se letos historicky poprvé odehrála naživo v Praze, a to v prostorách O2 Universum.

  • Kateřina Klouček Dlouhá
    Kateřina Klouček Dlouhá
FrontKon 2024 obrazem

Odběr novinek

Zadejte svůj e-mail a nenechte si ujít další nové články!

Odesláním formuláře souhlasíte se .
Zpracování osobních údajů probíhá za účelem zasílání newsletteru. Můžete se spolehnout, že vaše osobní údaje nebudeme s nikým sdílet. Z newsletteru se můžete kdykoli odhlásit. Stejně tak můžete kdykoli požádat o úplné smazání svých osobních údajů z naší databáze.
Zkontrolujte svoji e-mailovou schránku a potvrďte své přihlášení kliknutím na odkaz.

Všechna témata

  • Bootstrap
  • CSS
  • Design
  • Dokumentace
  • HTML
  • ITCSS
  • JavaScript
  • Kariéra
  • Kvalita kódu
  • Nástroje
  • No-code
  • Přístupnost
  • PWA
  • Reportáž
  • Rozhovor
  • Rychlost
  • Sass
  • Spolupráce
  • Typografie
  • Variable fonts
  • WebExpo
  • ✏️ Napište článek
  • Autoři
  • Cookies
  • Instagram
  • GitHub

Obsah na tomto webu je publikován pod licencí Creative Commons CC BY-NC 4.0.
Frontend Garden vysázel, zastřihuje a okopává (s ♥️) Adam Kudrna.
Založeno v květnu 2019.