Tworzenie komponentu etykietki

Podstawowy opis tworzenia elementu niestandardowego tooltipu, który dostosowuje kolory i zapewnia dostępność.

W tym poście chcę podzielić się z Wami swoimi przemyśleniami na temat tworzenia elementu niestandardowego <tool-tip> dostosowującego kolory i dostępnego dla osób niepełnosprawnych. Wypróbuj wersję demonstracyjnąwyświetl kod źródłowy.

Etykieta wyświetlana w różnych przykładach i schematach kolorów

Jeśli wolisz film, oto wersja tego posta w YouTube:

Omówienie

Opis to niemodalna, nieblokująca i nieinteraktywna nakładka zawierająca dodatkowe informacje dla interfejsów. Domyślnie jest ukryty i staje się widoczny, gdy najedziesz kursorem na powiązany element lub go zaznaczysz. Etykietki nie można wybrać ani z nią bezpośrednio wchodzić w interakcję. Informacje wyświetlane w tooltipach nie zastępują etykiet ani innych ważnych informacji. Użytkownik powinien mieć możliwość pełnego wykonania zadania bez wyświetlania tooltipa.

Zaleca się: zawsze etykietuj dane wejściowe.
Nie: zamiast etykiet używaj etykiet pomocniczych.

Etykieta a etykieta przełącznika

Podobnie jak w przypadku wielu innych komponentów, i w tym przypadku istnieją różne opisy tego, czym jest tooltip. Można je znaleźć na przykład w MDN, WAI ARIA, Sarah HigleyUwzględnienie wszystkich w komponentach. Podoba mi się rozróżnienie między podpowiedziami i przełącznikami. Informacje w tooltipie powinny być nieinterakcyjne, a w przypadku toggletipa mogą zawierać interaktywność i ważne informacje. Głównym powodem różnicy jest dostępność, czyli sposób, w jaki użytkownicy mają się dostać do wyskakującego okienka i uzyskać dostęp do informacji oraz przycisków. Wskazówki stają się szybko skomplikowane.

Oto film z przełącznikiem z treściami na stronie Designcember. Jest to nakładka z interaktywnymi elementami, które użytkownik może przypiąć i przeglądać, a potem zamknąć za pomocą przycisku odrzucenia lub klawisza Esc:

W tym wyzwaniu dotyczącym interfejsu użytkownika użyliśmy etykietki narzędzia, która prawie wszystko robi za pomocą CSS. Poniżej znajdziesz instrukcje jej tworzenia.

Znacznik

Używam elementu niestandardowego <tool-tip>. Autorzy nie muszą dodawać niestandardowych elementów do komponentów internetowych, jeśli tego nie chcą. Przeglądarka będzie traktować <foo-bar> tak samo jak <div>. Możesz uznać element niestandardowy za klasę o mniejszej specyficzności. Nie używamy JavaScriptu.

<tool-tip>A tooltip</tool-tip>

To jest jak element div z tekstem. Możemy połączyć się z drzewem ułatwień dostępu czytników ekranu obsługujących tę funkcję, dodając [role="tooltip"].

<tool-tip role="tooltip">A tooltip</tool-tip>

Teraz czytniki ekranu rozpoznają je jako etykietę. W tym przykładzie pierwszy element linku ma w drzewie rozpoznany element etykiety, a drugi nie. Drugi nie ma przypisanej roli. W sekcji stylów ulepszymy ten widok drzewa.

Zrzut ekranu przedstawiający drzewo ułatwień dostępu w Narzędziach deweloperskich w Chrome, które reprezentuje kod HTML. Pokazuje link z tekstem „top”. Ma etykietkę „Hey, a tooltip!”, którą można zaznaczyć. Wewnątrz znajduje się tekst statyczny „top” i element tooltip.

Następnie musimy ustawić, aby nie można było skupić kursora na opisie. Jeśli czytnik ekranu nie rozumie roli tooltipa, użytkownicy będą musieli ustawić fokus na elemencie <tool-tip>, aby przeczytać jego zawartość. Nie jest to jednak konieczne. Czytniki ekranu dołączą zawartość do elementu nadrzędnego, dlatego nie trzeba go zaznaczać, aby był dostępny. Możemy tu użyć inert, aby żaden użytkownik nie znalazł tego tekstu na karcie:

<tool-tip inert role="tooltip">A tooltip</tool-tip>

Kolejny zrzut ekranu przedstawiający drzewo ułatwień dostępu w Narzędziach dla programistów w Chrome. Tym razem element Tooltip (Tooltip) jest niewidoczny.

Następnie zdecydowałem się użyć atrybutów jako interfejsu do określenia pozycji opisu. Domyślnie wszystkie elementy <tool-tip> mają pozycję „góra”, ale można ją dostosować, dodając element tip-position:

<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>

Zrzut ekranu z linkiem i etykietą po prawej stronie z napisem „Etykieta”.

W takich przypadkach używam atrybutów zamiast klas, aby element <tool-tip> nie miał przypisanych kilku pozycji jednocześnie. Może być tylko 1 lub brak.

Na koniec umieść elementy <tool-tip> w elemencie, dla którego chcesz wyświetlić tekst pomocniczy. Tutaj udostępniam tekst alt użytkownikom ze wzrokiem, umieszczając obraz i element <tool-tip> w elemencie <picture>:

<picture>
  <img alt="The GUI Challenges skull logo" width="100" src="...">
  <tool-tip role="tooltip" tip-position="bottom">
    The <b>GUI Challenges</b> skull logo
  </tool-tip>
</picture>

Zrzut ekranu z obrazem z etykietą „The GUI Challenges skull
logo”.

Tutaj umieszczam element <tool-tip> wewnątrz elementu <abbr>:

<p>
  The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>

Zrzut ekranu z podkreślonym skrótem HTML i wyświetloną nad nim etykietą „Hyper Text Markup Language”.

Ułatwienia dostępu

Ponieważ zdecydowałem się na tworzenie tooltips, a nie toggletips, ta sekcja jest znacznie prostsza. Najpierw opiszę, jakie wrażenia chcemy zapewnić użytkownikom:

  1. W ograniczonych przestrzeniach lub na zatłoczonych interfejsach ukryj dodatkowe komunikaty.
  2. Gdy użytkownik najedzie myszą na element, wybierze go lub dotknie, aby z nim wejść w interakcję, wyświetla się wiadomość.
  3. Gdy kończy się najechanie kursorem, skupienie lub dotknięcie, ukryj wiadomość.
  4. Na koniec upewnij się, że ruch jest ograniczony, jeśli użytkownik preferuje ograniczony ruch.

Naszym celem jest wysyłanie wiadomości uzupełniających na żądanie. Osoba widząca może użyć myszy lub klawiatury, aby wyświetlić wiadomość i ją przeczytać. Użytkownik czytnika ekranu może skupić się na wiadomości, aby ją wyświetlić, i usłyszeć ją za pomocą narzędzia.

Zrzut ekranu z VoiceOver w systemie MacOS czytającym link z opisem

W poprzedniej sekcji omówiliśmy drzewo dostępności, rolę tooltipa i stan nieaktywny. Teraz wystarczy przetestować i sprawdzać, czy użytkownik zobaczy odpowiedni komunikat. Podczas testowania nie było jasne, która część wiadomości audio jest etykietą. Można to też zobaczyć podczas debugowania w drzewie ułatwień dostępu. Tekst linku „top” jest uruchamiany razem, bez wahania, z „Look, tooltips!”. Czytnik ekranu nie dzieli ani nie identyfikuje tekstu jako zawartości tooltipa.

Zrzut ekranu przedstawiający drzewo dotyczące ułatwień dostępu w Narzędziach deweloperskich w Chrome, w którym tekst linku brzmi „top Hey, a tooltip!”

Dodaj do elementu <tool-tip> pseudoelement przeznaczony tylko dla czytników ekranu, a my dodamy własny tekst promptu dla niewidomych użytkowników.

&::before {
  content: "; Has tooltip: ";
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

Poniżej możesz zobaczyć zaktualizowane drzewo ułatwień dostępu, które zawiera teraz średnik po tekście linku i prompt dla etykietki „Ma etykietkę:”.

Zaktualizowany zrzut ekranu przedstawiający stronę Accessibility Tree w Narzędziach dla programistów w Chrome, na której tekst linku ma ulepszony zapis, a w wyskakującym okienku widać komunikat „Hey, a tooltip!”

Teraz, gdy użytkownik czytnika ekranu skupi się na linku, usłyszy „top” (góra) i krótką pauzę, a potem „has tooltip: look, tooltips” (ma tooltip: spójrz, tooltips). Dzięki temu czytnik ekranu wyświetli użytkownikowi kilka przydatnych wskazówek dotyczących wygody użytkownika. Zatrzymanie oddziela tekst linku od opisu. Poza tym gdy czytnik ekranu usłyszy komunikat „has tooltip”, może go łatwo anulować, jeśli słyszał go już wcześniej. Jest to bardzo podobne do szybkiego najechania kursorem i odjęcia go od kursora, ponieważ widzisz już dodatkowy komunikat. To było bardzo dobre rozwiązanie.

Style

Element <tool-tip> będzie elementem podrzędnym elementu, którego dotyczy, czyli dodatkowej wiadomości, więc zacznijmy od podstaw efektu nakładki. Wyłącz go z obiegu dokumentów za pomocą position absolute:

tool-tip {
  position: absolute;
  z-index: 1;
}

Jeśli element nadrzędny nie jest kontekstem nakładania, tekst podpowiedzi zostanie umieszczony w najbliższym takim kontekście, co nie jest pożądanym działaniem. W bloku jest nowy selektor, który może Ci pomóc: :has().

Obsługa przeglądarek

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Źródło

:has(> tool-tip) {
  position: relative;
}

Nie martw się zbytnio obsługą przeglądarki. Pamiętaj, że te etykiety są tylko dodatkiem. Jeśli to nie pomoże, nie powinno być problemu. Po drugie, w sekcji JavaScript wdrożymy skrypt, który spowoduje polyfillowanie funkcji potrzebnych w przypadku przeglądarek bez obsługi :has().

Następnie sprawdź, czy tooltipy nie są interaktywne, aby nie przechwytywały zdarzeń kursora od elementu nadrzędnego:

tool-tip {
  
  pointer-events: none;
  user-select: none;
}

Następnie ukryj etykietę za pomocą funkcji zanikania, aby można było ją przełączyć za pomocą efektu przejścia:

tool-tip {
  opacity: 0;
}

:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
}

:is():has() wykonują tu większość pracy, dzięki czemu tool-tip zawierające elementy nadrzędne są świadome interakcji użytkownika, co pozwala przełączać widoczność podrzędnego tooltipa. Użytkownicy myszy mogą najechać kursorem, użytkownicy klawiatury i czytników ekranu mogą ustawić fokus, a użytkownicy urządzeń dotykowych mogą kliknąć.

Ponieważ wyświetlanie i ukrywanie nakładki działa już dla widzących użytkowników, nadszedł czas na dodanie stylów do motywu, pozycjonowania i dodania trójkąta do okienka. Te style zaczynają używać właściwości niestandardowych, które rozwijają to, co już mamy, ale też dodają cienie, typografię i kolory, aby wyglądało to jak unosząca się tooltip:

Zrzut ekranu pokazujący podpowiedź w trybie ciemnym, która pojawia się nad linkiem „block-start”.

tool-tip {
  --_p-inline: 1.5ch;
  --_p-block: .75ch;
  --_triangle-size: 7px;
  --_bg: hsl(0 0% 20%);
  --_shadow-alpha: 50%;

  --_bottom-tip: conic-gradient(from -30deg at bottom, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) bottom / 100% 50% no-repeat;
  --_top-tip: conic-gradient(from 150deg at top, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) top / 100% 50% no-repeat;
  --_right-tip: conic-gradient(from -120deg at right, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) right / 50% 100% no-repeat;
  --_left-tip: conic-gradient(from 60deg at left, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) left / 50% 100% no-repeat;

  pointer-events: none;
  user-select: none;

  opacity: 0;
  transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
  transition: opacity .2s ease, transform .2s ease;

  position: absolute;
  z-index: 1;
  inline-size: max-content;
  max-inline-size: 25ch;
  text-align: start;
  font-size: 1rem;
  font-weight: normal;
  line-height: normal;
  line-height: initial;
  padding: var(--_p-block) var(--_p-inline);
  margin: 0;
  border-radius: 5px;
  background: var(--_bg);
  color: CanvasText;
  will-change: filter;
  filter:
    drop-shadow(0 3px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
    drop-shadow(0 12px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
}

/* create a stacking context for elements with > tool-tips */
:has(> tool-tip) {
  position: relative;
}

/* when those parent elements have focus, hover, etc */
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
  transition-delay: 200ms;
}

/* prepend some prose for screen readers only */
tool-tip::before {
  content: "; Has tooltip: ";
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

/* tooltip shape is a pseudo element so we can cast a shadow */
tool-tip::after {
  content: "";
  background: var(--_bg);
  position: absolute;
  z-index: -1;
  inset: 0;
  mask: var(--_tip);
}

/* top tooltip styles */
tool-tip:is(
  [tip-position="top"],
  [tip-position="block-start"],
  :not([tip-position]),
  [tip-position="bottom"],
  [tip-position="block-end"]
) {
  text-align: center;
}

Dostosowania motywu

Wskazówka ma tylko kilka kolorów, ponieważ kolor tekstu jest dziedziczony ze strony za pomocą słowa kluczowego systemu CanvasText. Ponieważ utworzyliśmy właściwości niestandardowe do przechowywania wartości, możemy zaktualizować tylko te właściwości niestandardowe i pozostawić resztę tematowi:

@media (prefers-color-scheme: light) {
  tool-tip {
    --_bg: white;
    --_shadow-alpha: 15%;
  }
}

Zrzut ekranu z wersją jaśniej i ciemnej etykiety.

W przypadku motywu jasnego tło jest białe, a cienie są znacznie słabsze, ponieważ zmieniamy ich przezroczystość.

Od prawej do lewej

Aby obsługiwać tryby czytania od prawej do lewej, właściwość niestandardowa będzie przechowywać wartość kierunku dokumentu jako -1 lub 1.

tool-tip {
  --isRTL: -1;
}

tool-tip:dir(rtl) {
  --isRTL: 1;
}

Możesz go użyć, aby ustawić pozycję etykiety:

tool-tip[tip-position="top"]) {
  --_x: calc(50% * var(--isRTL));
}

Pomożemy Ci też znaleźć trójkąt:

tool-tip[tip-position="right"]::after {
  --_tip: var(--_left-tip);
}

tool-tip[tip-position="right"]:dir(rtl)::after {
  --_tip: var(--_right-tip);
}

Można ich też używać do transformacji logicznych w przypadku translateX():

--_x: calc(var(--isRTL) * -3px * -1);

Umieszczenie etykiety

Umieść etykietkę w logiczny sposób za pomocą właściwości inset-block lub inset-inline, aby obsługiwać zarówno pozycje fizyczne, jak i logiczne etykiet. Poniższy kod pokazuje, jak stylizowane są 4 pozycje w przypadku kierunków od lewej do prawej i od prawej do lewej.

Wyrównanie do góry i blokowanie

Zrzut ekranu pokazujący różnicę między pozycją górną od lewej do prawej a pozycją górną od prawej do lewej.

tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position])) {
  inset-inline-start: 50%;
  inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
  --_x: calc(50% * var(--isRTL));
}

tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))::after {
  --_tip: var(--_bottom-tip);
  inset-block-end: calc(var(--_triangle-size) * -1);
  border-block-end: var(--_triangle-size) solid transparent;
}

wyrównanie do prawej i wyrównanie do końca wiersza,

Zrzut ekranu pokazujący różnicę między pozycją prawą od lewej do prawej a pozycją końcową od prawej do lewej.

tool-tip:is([tip-position="right"], [tip-position="inline-end"]) {
  inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
  inset-block-end: 50%;
  --_y: 50%;
}

tool-tip:is([tip-position="right"], [tip-position="inline-end"])::after {
  --_tip: var(--_left-tip);
  inset-inline-start: calc(var(--_triangle-size) * -1);
  border-inline-start: var(--_triangle-size) solid transparent;
}

tool-tip:is([tip-position="right"], [tip-position="inline-end"]):dir(rtl)::after {
  --_tip: var(--_right-tip);
}

Wyrównanie do dołu i zakończenia bloku

Zrzut ekranu pokazujący różnicę między umieszczeniem w lewym dolnym rogu a umieszczeniem na końcu bloku po prawej stronie.

tool-tip:is([tip-position="bottom"], [tip-position="block-end"]) {
  inset-inline-start: 50%;
  inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
  --_x: calc(50% * var(--isRTL));
}

tool-tip:is([tip-position="bottom"], [tip-position="block-end"])::after {
  --_tip: var(--_top-tip);
  inset-block-start: calc(var(--_triangle-size) * -1);
  border-block-start: var(--_triangle-size) solid transparent;
}

wyrównanie do lewej i wyrównanie do początku w wierszu.

Zrzut ekranu pokazujący różnicę między pozycją lewostronną (z lewej do prawej) a pozycją początkową w wierszu (z prawej do lewej).

tool-tip:is([tip-position="left"], [tip-position="inline-start"]) {
  inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
  inset-block-end: 50%;
  --_y: 50%;
}

tool-tip:is([tip-position="left"], [tip-position="inline-start"])::after {
  --_tip: var(--_right-tip);
  inset-inline-end: calc(var(--_triangle-size) * -1);
  border-inline-end: var(--_triangle-size) solid transparent;
}

tool-tip:is([tip-position="left"], [tip-position="inline-start"]):dir(rtl)::after {
  --_tip: var(--_left-tip);
}

Animacja

Do tej pory zmieniliśmy tylko widoczność opisu. W tej sekcji najpierw pokażemy animację przezroczystości dla wszystkich użytkowników, ponieważ jest to ogólnie bezpieczna animacja z ograniczonym ruchem. Następnie utworzymy animację zmiany pozycji, aby wskazówka wyglądała na wysuniętą z elementu nadrzędnego.

Bezpieczne i sensowne domyślne przejście

Nadaj styl elementowi etykiety, aby przejść do przezroczystości i transformacji, np. w ten sposób:

tool-tip {
  opacity: 0;
  transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
  transition: opacity .2s ease, transform .2s ease;
}

:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
  transition-delay: 200ms;
}

Dodawanie ruchu do przejścia

W przypadku każdego z boków, na których może pojawić się tooltip, jeśli użytkownik zgadza się na ruch, ustaw właściwość translateX, podając niewielką odległość do przebycia z:

@media (prefers-reduced-motion: no-preference) {
  :has(> tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_y: 3px;
  }

  :has(> tool-tip:is([tip-position="right"], [tip-position="inline-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_x: -3px;
  }

  :has(> tool-tip:is([tip-position="bottom"], [tip-position="block-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_y: -3px;
  }

  :has(> tool-tip:is([tip-position="left"], [tip-position="inline-start"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_x: 3px;
  }
}

Zwróć uwagę, że jest to stan „out”, ponieważ stan „in” to translateX(0).

JavaScript

Moim zdaniem kod JavaScript jest opcjonalny. Wynika to z tego, że żadne z nich nie powinno być wymagane do wykonania zadania w interfejsie. Jeśli więc tooltipy nie działają w ogóle, nie powinno to stanowić problemu. Oznacza to też, że możemy traktować etykietki jako stopniowo ulepszone. Ostatecznie wszystkie przeglądarki będą obsługiwać :has(), a skrypt będzie można całkowicie usunąć.

Skrypt polyfill wykonuje 2 działania, ale tylko wtedy, gdy przeglądarka nie obsługuje :has(). Najpierw sprawdź, czy :has() obsługuje:

if (!CSS.supports('selector(:has(*))')) {
  // do work
}

Następnie znajdź elementy nadrzędne <tool-tip> i nadaj im nazwę klasy, z którą mają współpracować:

if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('tool-tip').forEach(tooltip =>
    tooltip.parentNode.classList.add('has_tool-tip'))
}

Następnie wstrzyknij zestaw stylów, który używa tej nazwy klasy, symulując selektor :has()o tych samych właściwościach:

if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('tool-tip').forEach(tooltip =>
    tooltip.parentNode.classList.add('has_tool-tip'))

  let styles = document.createElement('style')
  styles.textContent = `
    .has_tool-tip {
      position: relative;
    }
    .has_tool-tip:is(:hover, :focus-visible, :active) > tool-tip {
      opacity: 1;
      transition-delay: 200ms;
    }
  `
  document.head.appendChild(styles)
}

To wszystko. Teraz wszystkie przeglądarki będą wyświetlać etykiety, jeśli :has() nie jest obsługiwana.

Podsumowanie

Teraz, gdy już wiesz, jak to zrobić, jak Ty to zrobisz? 🙂 Naprawdę nie mogę się doczekać, aż zobaczę interfejs popup API, który ułatwia tworzenie przełączników, warstwę górną, która eliminuje konieczność korzystania z indeksu z-poziomu, oraz interfejs anchor API, który ułatwia pozycjonowanie elementów w oknie. Do tego czasu będę tworzyć tooltipy.

Zróżnicujemy nasze podejścia i poznamy wszystkie sposoby tworzenia stron internetowych.

Utwórz wersję demonstracyjną, wyślij mi linki, a ja dodam je do sekcji z remiksami społeczności.

Remiksy społeczności

Na razie jest tu pusto.

Zasoby