Podstawowe informacje o tworzeniu elementu niestandardowego z etykietami z możliwością dostosowania kolorów i z dostępem.
W tym poście chcę podzielić się przemyśleniami na temat tworzenia łatwo dostępnego i dostosowającego się do kolorów elementu niestandardowego <tool-tip>
. Wypróbuj
wersję demonstracyjną i wyświetl źródło.
Jeśli wolisz film, oto wersja tego posta na YouTube:
Przegląd
Etykietka to niemodalna, nieblokująca i nieinteraktywna nakładka z dodatkowymi informacjami o interfejsach. Jest domyślnie ukryte i staje się widoczne, gdy użytkownik najedzie kursorem na powiązany element lub go aktywuje. Nie można wybrać etykietki ani wchodzić z nią w interakcję bezpośrednio. Etykietki nie zastępują etykiet ani innych cennych informacji. Użytkownik powinien mieć możliwość ukończenia zadania bez etykietki.
Zamień wskazówkę i etykietkę
Podobnie jak w przypadku wielu komponentów, etykietki mają różne opisy, na przykład w MDN, WAI ARIA, Sarah Higley i Komponenty inkluzywne. Podoba mi się oddzielenie etykietek od etykietek. Etykietka powinna zawierać nieinteraktywne informacje dodatkowe, a wskazówka może zawierać elementy interaktywne i ważne informacje. Główną przyczyną tego podziału są ułatwienia dostępu, sposób poruszania się użytkowników do wyskakującego okienka i uzyskiwania dostępu do zawartych w nim informacji i przycisków. W przypadku opcji przełączania szpilki szybko się skomplikowanie.
Oto film z wskazówką ze strony Projektcember. Zawiera ona interaktywną nakładkę, którą użytkownik może przypiąć i przejrzeć, a następnie zamknąć za pomocą przycisku zamykania światłem lub klawisza Escape:
To wyzwanie GUI wyłoniło się z podpowiedziami, jak wykorzystać CSS do niemal wszystkich. Tutaj dowiesz się, jak go utworzyć.
Markup
Wybieram element niestandardowy <tool-tip>
. Autorzy nie muszą tworzyć
niestandardowych elementów w komponentach internetowych. Przeglądarka będzie traktować właściwość <foo-bar>
tak samo jak <div>
. Taki element to na przykład
nazwa klasy o mniejszej szczegółowości. Nie musisz używać JavaScriptu.
<tool-tip>A tooltip</tool-tip>
Jest to element div z tekstem. Możemy dodać do drzewa ułatwień dostępu
umiejętne czytniki ekranu, dodając element [role="tooltip"]
.
<tool-tip role="tooltip">A tooltip</tool-tip>
Czytniki ekranu są teraz rozpoznawane jako etykietki. Poniższy przykład pokazuje, jak pierwszy element link ma w drzewie rozpoznawany element etykietki, a drugi nie. Druga osoba nie ma tej roli. W sekcji Style poprawimy ten widok drzewa.
Teraz ta etykietka musi być wyłączona z możliwości zaznaczenia. Jeśli czytnik ekranu nie zrozumie roli etykietki, będzie mógł zaznaczyć element <tool-tip>
, aby przeczytać treść. Obsługa nie jest potrzebna. Czytniki ekranu dołączają treść do elementu nadrzędnego, dzięki czemu jej dostępność nie wymaga zaznaczenia. Dzięki tagowi inert
możemy mieć pewność, że żaden użytkownik nie przypadkowo natknie się na etykietkę w trakcie swojej karty:
<tool-tip inert role="tooltip">A tooltip</tool-tip>
Następnie użyłem atrybutów jako interfejsu do określenia pozycji etykietki. Domyślnie wszystkie obiekty <tool-tip>
przyjmują pozycję „góry”, ale można ją dostosować w elemencie, dodając parametr tip-position
:
<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>
W takich sytuacjach zamiast klas używam atrybutów, a <tool-tip>
nie może mieć przypisanych do nich wielu pozycji naraz.
Może być tylko jedna lub żadna.
Na koniec umieść elementy <tool-tip>
wewnątrz elementu, dla którego chcesz dodać etykietkę. Udostępniam tekst alt
osobom widzącym, umieszczając obraz i <tool-tip>
wewnątrz elementu <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>
umieszczam <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ż zdecydowałem się tworzyć etykietki, a nie przełączniki, ta sekcja jest znacznie prostsza. Na początek wyjaśnię, co chcemy zrobić dla użytkownika:
- Jeśli masz za mało przestrzeni lub zatłoczony interfejs, ukryj dodatkowe komunikaty.
- Gdy użytkownik najedzie kursorem na element, aktywuje jego zaznaczenie lub użyje dotyku, aby wejść w interakcję z elementem, wyświetl komunikat.
- Po najechaniu kursorem, wskazaniu lub dotknięciu powoduje ponowne ukrycie wiadomości.
- Jeśli użytkownik określił preferencję redukcji ruchu, upewnij się też, że ruch jest ograniczony.
Naszym celem jest uzupełnianie przekazu na żądanie. Osoba widząca mysz lub klawiaturę może najechać kursorem na wiadomość, aby ją odczytać. Niedowidzący użytkownik czytnika ekranu może skupić się na ujawnieniu komunikatu, odbierając go za pomocą narzędzia.
W poprzedniej sekcji omówiliśmy drzewo ułatwień dostępu, rolę etykietki i bezwłasność. Pozostaje jeszcze przetestowanie i sprawdzenie, czy interfejs użytkownika prawidłowo wyświetla etykietkę. Po przetestowaniu nie wiadomo, która część słyszalnego komunikatu to etykietka. Widać go też podczas debugowania w drzewie ułatwień dostępu. Tekst linku „top” jest cyklicznie tworzony razem, ale za pomocą polecenia „Look, tooltips!”. Czytnik ekranu nie psuje tekstu ani nie identyfikuje tekstu jako treści etykietki.
Dodaj do elementu <tool-tip>
tylko pseudoelement z czytnikiem ekranu, a my będziemy mogli dodać własny tekst promptu dla 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 znajduje się zaktualizowane drzewo ułatwień dostępu: po tekście linku widać teraz średnik oraz etykietkę „Zawiera etykietkę ”.
Teraz, gdy użytkownik czytnika ekranu zaznaczy link, wyświetla się napis „Do góry”, po chwili na chwilę pauza i komunikat „Zawiera etykietkę: spojrzenie, etykietki”. Daje to użytkownikowi czytnika ekranu kilka przydatnych wskazówek dotyczących UX. Dobrze odseparuje on tekst linku od etykietki. Dodatkowo po pojawieniu się komunikatu „ma etykietkę” użytkownik czytnika ekranu może go łatwo anulować, jeśli już go słyszał. Przypomina kojarzący się z szybkim uruchamianiem i usuwaniem najechania, tak jak zaobserwowaliśmy już dodatkowy komunikat. Wydaje się, że wszystko jest w porządku.
Style
Element <tool-tip>
będzie elementem podrzędnym elementu, w którym reprezentuje dodatkowy komunikat. Zacznijmy więc od podstawowych informacji o efektu nakładki. Wyjdź z dokumentu za pomocą position absolute
:
tool-tip {
position: absolute;
z-index: 1;
}
Jeśli element nadrzędny nie jest kontekstem skumulowanym, etykietka umieści się na najbliższym, a nie tego oczekujemy. Dostępny jest nowy selektor, który może Ci pomóc: :has()
:
:has(> tool-tip) {
position: relative;
}
Nie przejmuj się zbytnio obsługą przeglądarek. Po pierwsze, pamiętaj, że etykietki
są uzupełniające. Jeśli funkcja nie działa, wszystko powinno być w porządku. Po drugie, w sekcji JavaScript wdrożymy skrypt do uzupełniania funkcji wymaganych w przeglądarkach bez obsługi standardu :has()
.
Teraz zmienimy etykietki w nieinteraktywne, aby nie odbierały zdarzeń wskaźnika z elementu nadrzędnego:
tool-tip {
…
pointer-events: none;
user-select: none;
}
Następnie ukryj etykietkę z przezroczystością, żebyśmy mogli ją przenieść z przenikaniem:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
i :has()
wykonują najcięższą pracę w tym miejscu, dzięki czemu zawierające elementy nadrzędne tool-tip
wiedzą o interakcji użytkownika z użytkownikiem i umożliwiają przełączanie widoczności etykietki podrzędnej. Użytkownicy korzystający z myszy mogą się poruszać po najechaniu kursorem, używać klawiatury i czytnika ekranu, a użytkownicy dotykowi – klikać.
Gdy nakładka Pokaż i ukrycie działa dla osób, które widzą, musisz dodać do bąbelka różne style, pozycjonować je i dodawać do bąbelka kształt trójkąta. Poniższe style zaczynają korzystać z właściwości niestandardowych, bazując na dotychczasowych miejscach, ale też dodając cienie, typografię i kolory, dzięki czemu wygląda ona 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;
}
Korekty motywu
Etykietka ma tylko kilka kolorów do zarządzania, ponieważ kolor tekstu jest dziedziczony ze strony przez systemowe słowo kluczowe CanvasText
. Utworzyliśmy właściwości niestandardowe do przechowywania wartości, więc możemy aktualizować tylko te właściwości, a reszta zajmuje się motywem:
@media (prefers-color-scheme: light) {
tool-tip {
--_bg: white;
--_shadow-alpha: 15%;
}
}
W przypadku jasnego motywu dostosowujemy tło na białe, a znacznie zmniejszamy ich intensywność, dostosowując ich przezroczystość.
Od prawej do lewej
Do obsługi trybów czytania od prawej do lewej właściwość niestandardowa zapisuje wartość kierunku dokumentu jako wartość odpowiednio -1 lub 1.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
Może to pomóc w pozycjonowaniu etykietki:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
Zwróć też uwagę na to, gdzie 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);
}
Funkcji tej można też używać do przekształceń logicznych w translateX()
:
--_x: calc(var(--isRTL) * -3px * -1);
Pozycjonowanie etykiet
Umieść etykietkę w logicznym położeniu zgodnie z właściwościami inset-block
lub inset-inline
, aby obsługiwać zarówno fizyczne, jak i logiczne pozycje etykiet. Poniższy kod przedstawia styl każdego z 4 pozycji w przypadku kierunków „od lewej do prawej” i „od prawej do lewej”.
Wyrównanie do góry i blok
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 do środka
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 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 w tekście
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 przestawiliśmy tylko widoczność etykietki. W tej sekcji najpierw zaczniemy animować przezroczystość u wszystkich użytkowników, ponieważ jest to ogólnie bezpieczne z ograniczonym przejściem ruchu. Następnie przeprowadzimy animację pozycji przekształcenia, tak aby etykietka wysuwała się z elementu nadrzędnego.
Bezpieczne i wymierne przejście na nową wersję.
Określ styl elementu etykietki przezroczystość przejścia i przekształcenie 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;
}
Dodaje ruch do przejścia
Jeśli po każdej ze stron może pojawić się etykietka, jeśli użytkownik nie przeszkadza w ruchu, lekko ustaw właściwość translateX, oddalając ją o niewielką odległość:
@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 stan to „out”, ponieważ stan „w” to translateX(0)
.
JavaScript
Uważam, że obsługa JavaScript jest opcjonalna. Wynika to z faktu, że do wykonania zadania w interfejsie użytkownika nie powinno się wymagać odczytywania żadnej z tych etykiet. Jeśli więc etykietki całkowicie się nie powiodą, nie powinno to stanowić problemu. Oznacza to również, że etykietki możemy
traktować jako stopniowo ulepszone. Ostatecznie wszystkie przeglądarki będą obsługiwać :has()
, a ten skrypt będzie dostępny.
Skrypt polyfill spełnia 2 czynności i działa tylko wtedy, gdy przeglądarka nie obsługuje :has()
. Najpierw sprawdź, czy masz zespół pomocy :has()
:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Następnie znajdź elementy nadrzędne elementów <tool-tip>
i nadaj im nazwę klasy, z którą będą 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óre korzystają z tej nazwy klasy, symulując selektor :has()
, aby uzyskać dokładnie takie samo działanie:
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. Jeśli :has()
nie będzie obsługiwany, wszystkie przeglądarki będą wyświetlać etykietki.
Podsumowanie
Skoro już wiesz, jak to zrobiłem, to jak to zrobić? 🙂 Czekam na interfejs API popup
, który ułatwia korzystanie ze wskazówek, górną warstwę w przypadku braku bitew z indeksem oraz interfejs API anchor
do lepszego umieszczania elementów w oknie. Będę używać etykiet.
Stwórzmy różne metody i nauczmy się wszystkiego, jak rozwijać się w internecie.
Utwórz demonstrację i udostępnię mi linki na Twitterze, a dodam ją do sekcji remiksów w ramach społeczności poniżej.
Remiksy społeczności
Na razie niczego tu nie ma.
Zasoby
- Kod źródłowy w GitHubie