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ą i wyświetl kod źródłowy.
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.
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 Higley i Uwzglę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.
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>
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>
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>
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>
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:
- W ograniczonych przestrzeniach lub na zatłoczonych interfejsach ukryj dodatkowe komunikaty.
- Gdy użytkownik najedzie myszą na element, wybierze go lub dotknie, aby z nim wejść w interakcję, wyświetla się wiadomość.
- Gdy kończy się najechanie kursorem, skupienie lub dotknięcie, ukryj wiadomość.
- 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.
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.
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ę:”.
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()
.
: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()
i :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:
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%;
}
}
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
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,
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
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.
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
- kod źródłowy na GitHubie,