Podstawowe omówienie sposobu tworzenia elementu niestandardowego z etykietką, który dostosowuje kolor i jest dostępny.
W tym poście chcę podzielić się moimi przemyśleniami na temat tworzenia <tool-tip>
elementu niestandardowego, który dostosowuje się do koloru i jest dostępny. Wypróbuj
wersję demonstracyjną i wyświetl
kod źródłowy.
Jeśli wolisz film, obejrzyj tę wersję posta w YouTube:
Przegląd
Etykietka to niemodalna, nieblokująca i nieinteraktywna nakładka zawierająca dodatkowe informacje o interfejsach użytkownika. Jest domyślnie ukryty i staje się widoczny, gdy użytkownik najedzie kursorem na powiązany element lub go zaznaczy. Etykietki nie można wybrać ani z nią bezpośrednio wejść w interakcję. Etykietki nie zastępują etykiet ani innych ważnych informacji. Użytkownik powinien być w stanie w pełni wykonać zadanie bez etykietki.

Nie: polegaj na etykietkach zamiast na etykietach.
Etykietka przełączana a etykietka
Podobnie jak w przypadku wielu komponentów, istnieją różne opisy tego, czym jest etykietka narzędzi, np. w MDN, WAI ARIA, Sarah Higley i Inclusive Components. Podoba mi się rozdzielenie etykietek i przełączników. Etykietka narzędzi powinna zawierać nieinteraktywne informacje dodatkowe, a etykietka przełączana może zawierać interaktywne i ważne informacje. Głównym powodem podziału jest dostępność. Jak użytkownicy mają nawigować do wyskakującego okienka i uzyskać dostęp do informacji oraz przycisków w nim zawartych? Toggletipy szybko stają się skomplikowane.
Oto film przedstawiający wysuwany panel z witryny Designcember. Jest to nakładka z elementami interaktywnymi, którą użytkownik może przypiąć i przeglądać, a następnie zamknąć, klikając poza nią lub naciskając klawisz Escape:
W tym wyzwaniu dotyczącym interfejsu użytkownika zastosowano podpowiedź, która ma być w większości oparta na CSS. Oto jak ją utworzyć.
Znacznik
Wybrałem(-am) element niestandardowy <tool-tip>
. Autorzy nie muszą przekształcać elementów niestandardowych w komponenty internetowe, jeśli nie chcą. Przeglądarka będzie traktować <foo-bar>
tak samo jak <div>
. Element niestandardowy można traktować jako nazwę klasy o mniejszej precyzyjności. Nie jest wymagany JavaScript.
<tool-tip>A tooltip</tool-tip>
To jest jak element div z tekstem w środku. Możemy powiązać się z drzewem ułatwień dostępu
kompatybilnych czytników ekranu, dodając [role="tooltip"]
.
<tool-tip role="tooltip">A tooltip</tool-tip>
Teraz czytniki ekranu rozpoznają go jako etykietkę. Czy widzisz w przykładzie poniżej, że pierwszy element linku ma w drzewie rozpoznany element etykietki, a drugi nie? Drugi użytkownik nie ma tej roli. W sekcji stylów ulepszymy ten widok drzewa.
Następnie musimy sprawić, aby etykietka nie była elementem, który można zaznaczyć. Jeśli czytnik ekranu nie rozpoznaje roli etykietki, umożliwi użytkownikom skupienie się na elemencie <tool-tip>
, aby odczytać jego zawartość, co nie jest potrzebne. Czytniki ekranu dołączą treść do elementu nadrzędnego, więc nie musi on być zaznaczony, aby był dostępny. W tym miejscu możemy użyć inert
, aby mieć pewność, że żaden użytkownik nie znajdzie przypadkowo treści tego elementu podpowiedzi w przepływie kart:
<tool-tip inert role="tooltip">A tooltip</tool-tip>
Następnie wybrałem atrybuty jako interfejs do określania pozycji etykietki. Domyślnie wszystkie <tool-tip>
przyjmują pozycję „top”, ale można ją dostosować w przypadku poszczególnych elementów, dodając tip-position
:
<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>
Zwykle używam atrybutów zamiast klas w przypadku takich rzeczy, aby element <tool-tip>
nie mógł mieć jednocześnie przypisanych wielu pozycji.
Może być tylko jedna lub żadna.
Na koniec umieść elementy <tool-tip>
w elemencie, dla którego chcesz wyświetlać etykietkę. Tekst alt
udostępniam użytkownikom widzącym, umieszczając obraz i <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>
w elemencie
<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ż wybrałem tworzenie etykietek, a nie etykietek przełączanych, ta sekcja jest znacznie prostsza. Najpierw opiszę, jakie wrażenia użytkownika chcemy osiągnąć:
- W przypadku ograniczonej przestrzeni lub przeładowanych interfejsów ukrywaj dodatkowe wiadomości.
- Gdy użytkownik najedzie kursorem na element, wybierze go lub dotknie, wyświetl komunikat.
- Gdy wskaźnik myszy przestanie wskazywać element, element przestanie być aktywny lub dotyk się zakończy, ponownie ukryj wiadomość.
- Na koniec upewnij się, że ruch jest ograniczony, jeśli użytkownik określił preferencję dotyczącą ograniczenia ruchu.
Naszym celem jest wyświetlanie dodatkowych wiadomości na żądanie. Użytkownik korzystający z myszy lub klawiatury może najechać kursorem na wiadomość, aby ją wyświetlić i przeczytać. Użytkownik czytnika ekranu, który nie widzi, może skupić się na wiadomości, aby ją odsłonić i usłyszeć za pomocą swojego narzędzia.

W poprzedniej sekcji omówiliśmy drzewo dostępności, rolę etykietki i atrybut inert. Pozostało nam przetestować to rozwiązanie i sprawdzić, czy użytkownikowi wyświetla się odpowiedni komunikat etykietki. Podczas testowania nie było jasne, która część komunikatu dźwiękowego jest etykietką. Można to też zobaczyć podczas debugowania w drzewie ułatwień dostępu. Tekst linku „top” jest połączony bez wahania z tekstem „Look, tooltips!”. Czytnik ekranu nie dzieli ani nie rozpoznaje tekstu jako treści etykietki.
Dodaj do elementu <tool-tip>
pseudoelement dostępny tylko dla czytników ekranu, aby dodać własny tekst podpowiedzi dla osób niewidomych.
&::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 znajdziesz zaktualizowane drzewo ułatwień dostępu, które zawiera teraz średnik po tekście linku i prompt do etykietki „Has tooltip: ” (Zawiera etykietkę:).
Teraz, gdy użytkownik czytnika ekranu skupi się na linku, usłyszy „góra”, a potem po krótkiej przerwie „ma etykietkę: zobacz etykietki”. Dzięki temu użytkownik czytnika ekranu otrzyma kilka przydatnych wskazówek dotyczących wygody użytkowania. Dzięki temu tekst linku jest wyraźnie oddzielony od etykietki. Dodatkowo, gdy zostanie odczytany komunikat „zawiera etykietkę”, użytkownik czytnika ekranu może łatwo go anulować, jeśli słyszał go już wcześniej. Przypomina to szybkie najeżdżanie kursorem i cofanie go, ponieważ dodatkowy komunikat jest już widoczny. To było dobre wyrównanie UX.
Style
Element <tool-tip>
będzie elementem podrzędnym elementu, dla którego ma wyświetlać dodatkowe informacje, więc zacznijmy od podstawowych elementów 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 układania, etykietka zostanie umieszczona w najbliższym kontekście układania, co nie jest pożądane. Na bloku pojawił się nowy selektor, który może Ci pomóc: :has()
.
:has(> tool-tip) {
position: relative;
}
Nie martw się zbytnio obsługą przeglądarek. Pamiętaj, że te podpowiedzi są dodatkowe. Jeśli nie działają, nie powinno być problemu. Po drugie, w sekcji JavaScript wdrożymy skrypt, który uzupełni funkcje potrzebne w przeglądarkach bez obsługi :has()
.
Następnie sprawimy, że etykietki nie będą interaktywne, aby nie przechwytywały zdarzeń wskaźnika z elementu nadrzędnego:
tool-tip {
…
pointer-events: none;
user-select: none;
}
Następnie ukryj etykietkę z przezroczystością, aby można było przejść do niej za pomocą efektu przenikania:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
i :has()
wykonują tu najważniejszą pracę, informując element tool-tip
zawierający elementy nadrzędne o interakcjach użytkownika, aby przełączać widoczność podrzędnego elementu podpowiedzi. Użytkownicy myszy mogą najeżdżać kursorem, użytkownicy klawiatury i czytnika ekranu mogą zaznaczać, a użytkownicy urządzeń dotykowych mogą klikać.
Gdy funkcja wyświetlania i ukrywania nakładki działa już w przypadku użytkowników widzących, czas dodać style do motywów, pozycjonowania i dodawania trójkątnego kształtu do dymku. Poniższe style zaczynają korzystać z właściwości niestandardowych, bazując na tym, co już mamy, ale dodając też cienie, typografię i kolory, aby wyglądało to jak pływająca etykietka:
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;
}
Dostosowywanie motywu
Etykietka ma tylko kilka kolorów do zarządzania, 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 aktualizować tylko te właściwości, a resztę pozostawić motywowi:
@media (prefers-color-scheme: light) {
tool-tip {
--_bg: white;
--_shadow-alpha: 15%;
}
}
W przypadku jasnego motywu dostosowujemy tło do koloru białego i znacznie zmniejszamy intensywność cieni, dostosowując ich krycie.
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że to pomóc w określeniu położenia etykietki:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
Pomoże też określić, gdzie znajduje się 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 też używać do przekształceń logicznych w przypadku translateX()
:
--_x: calc(var(--isRTL) * -3px * -1);
Pozycjonowanie etykietki
Ustaw pozycję etykietki w logiczny sposób za pomocą właściwości inset-block
lub inset-inline
, aby obsługiwać zarówno fizyczne, jak i logiczne pozycje etykietki. Poniższy kod pokazuje, jak każdy z 4 rodzajów pozycji jest stylizowany w przypadku kierunku pisania od lewej do prawej i od prawej do lewej.
Wyrównanie do góry i do początku bloku
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 na końcu 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 do końca 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 na początku wiersza
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 przełączaliśmy tylko widoczność etykietki. W tej sekcji najpierw animujemy krycie dla wszystkich użytkowników, ponieważ jest to ogólnie bezpieczne przejście z ograniczonym ruchem. Następnie animujemy pozycję przekształcenia, aby etykietka narzędzi wysuwała się z elementu nadrzędnego.
Bezpieczne i sensowne domyślne przejście
Nadaj elementowi etykietki stylu przejścia nieprzezroczystości i przekształcenia, np. tak:
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ę etykietka, jeśli użytkownik akceptuje ruch, nieznacznie zmień pozycję właściwości translateX, aby zapewnić niewielką odległość, jaką musi pokonać:
@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 ustawiasz stan „out”, ponieważ stan „in” ma wartość translateX(0)
.
JavaScript
Moim zdaniem JavaScript jest opcjonalny. Żadna z tych podpowiedzi nie powinna być niezbędna do wykonania zadania w interfejsie. Jeśli więc etykietki całkowicie zawiodą, nie powinno to stanowić większego problemu. Oznacza to również, że możemy traktować
etykietki jako stopniowo ulepszane. Ostatecznie wszystkie przeglądarki będą obsługiwać :has()
i ten skrypt będzie można całkowicie usunąć.
Skrypt polyfill wykonuje 2 czynności i robi to tylko wtedy, gdy przeglądarka nie obsługuje :has()
. Najpierw sprawdź, czy :has()
jest obsługiwane:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Następnie znajdź elementy nadrzędne elementów <tool-tip>
i nadaj im nazwę klasy, aby z nimi pracować:
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
}
Następnie wstaw zestaw stylów, które używają tej nazwy klasy, symulując :has()
selektor, aby uzyskać dokładnie to samo zachowanie:
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ć etykietki, jeśli element :has()
nie jest obsługiwany.
Podsumowanie
Teraz, gdy wiesz, jak to zrobiłem, jak Ty byś to zrobił? 🙂 Nie mogę się doczekać interfejsu API, który ułatwi tworzenie podpowiedzi, najwyższej warstwy, która pozwoli uniknąć problemów z indeksem z, oraz interfejsu API, który ułatwi pozycjonowanie elementów w oknie.popup
anchor
Do tego czasu będę tworzyć
etykietki.
Urozmaićmy nasze podejście i poznajmy wszystkie sposoby tworzenia treści w internecie.
Utwórz demo, wyślij mi na Twitterze linki, a ja dodam je do sekcji remiksów społeczności poniżej.
Remiksy społeczności
Na razie jest tu pusto.
Zasoby
- Kod źródłowy na GitHubie