Membuat komponen tooltip

Ringkasan dasar tentang cara mem-build elemen kustom tooltip yang adaptif terhadap warna dan mudah diakses.

Dalam postingan ini, saya ingin berbagi pendapat tentang cara membuat elemen kustom <tool-tip> yang adaptif warna dan dapat diakses. Coba demo dan lihat sumbernya.

Tooltip ditampilkan berfungsi di berbagai contoh dan skema warna

Jika Anda lebih suka video, berikut versi YouTube dari postingan ini:

Ringkasan

Tooltip adalah overlay non-modal, non-blocking, dan non-interaktif yang berisi informasi tambahan untuk antarmuka pengguna. Elemen ini disembunyikan secara default dan menjadi tidak disembunyikan saat elemen terkait diarahkan kursor atau difokuskan. Tooltip tidak dapat dipilih atau berinteraksi secara langsung. Tooltip bukan pengganti label atau informasi bernilai tinggi lainnya. Pengguna harus dapat menyelesaikan tugasnya sepenuhnya tanpa tooltip.

Lakukan: selalu beri label pada input Anda.
Jangan: mengandalkan tooltip, bukan label

Toggletip vs Tooltip

Seperti banyak komponen, ada berbagai deskripsi tentang tooltip, misalnya di MDN, ARIA WAI, Sarah Higley, dan Komponen Inklusif. Saya menyukai pemisahan antara {i>tooltip<i} dan tips tombol. Tooltip harus berisi informasi tambahan noninteraktif, sedangkan ujung tombol dapat berisi interaktivitas dan informasi penting. Alasan utama pemisahan ini adalah aksesibilitas, bagaimana pengguna diharapkan membuka pop-up dan memiliki akses ke informasi dan tombol di dalamnya. Toggletip menjadi rumit dengan cepat.

Berikut adalah video toggletip dari situs Designcember; overlay dengan interaktivitas yang dapat disematkan pengguna untuk membuka dan menjelajahi, lalu ditutup dengan tombol tutup ringan atau tombol escape:

Tantangan GUI ini menggunakan tooltip, yang ingin melakukan hampir semua hal dengan CSS, dan berikut cara membuatnya.

Markup

Saya memilih untuk menggunakan elemen kustom <tool-tip>. Penulis tidak perlu membuat elemen khusus menjadi komponen web jika mereka tidak menginginkannya. Browser akan memperlakukan <foo-bar> seperti <div>. Anda dapat menganggap elemen kustom seperti nama class dengan tingkat kekhususan yang lebih rendah. Tidak ada JavaScript yang terlibat.

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

Ini seperti div dengan beberapa teks di dalamnya. Kita dapat mengaitkan ke hierarki aksesibilitas pembaca layar yang mampu dengan menambahkan [role="tooltip"].

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

Sekarang, untuk pembaca layar, elemen ini dikenali sebagai tooltip. Lihat dalam contoh berikut bagaimana elemen link pertama memiliki elemen tooltip yang dikenali dalam hierarkinya dan elemen kedua tidak? Akun kedua tidak memiliki peran tersebut. Di bagian gaya, kita akan memperbaiki tampilan hierarki ini.

Screenshot
Hierarki Aksesibilitas Chrome DevTools yang mewakili HTML. Menampilkan
link dengan teks &#39;top ; Memiliki tooltip: Hai, tooltip!&#39; yang dapat difokuskan. Di dalamnya
terdapat teks statis &#39;top&#39; dan elemen tooltip.

Selanjutnya, kita perlu tooltip agar tidak dapat difokuskan. Jika pembaca layar tidak memahami peran tooltip, pengguna dapat memfokuskan <tool-tip> untuk membaca konten, dan pengalaman pengguna tidak memerlukannya. Pembaca layar akan menambahkan konten ke elemen induk sehingga tidak perlu fokus agar dapat diakses. Di sini, kita dapat menggunakan inert untuk memastikan tidak ada pengguna yang tidak sengaja menemukan konten tooltip ini dalam alur tab mereka:

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

Screenshot lain dari Hierarki Aksesibilitas Chrome DevTools, kali ini elemen tooltip tidak ada.

Kemudian, saya memilih untuk menggunakan atribut sebagai antarmuka untuk menentukan posisi tooltip. Secara default, semua <tool-tip> akan berada di posisi "atas", tetapi posisi dapat disesuaikan pada elemen dengan menambahkan tip-position:

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

Screenshot
link dengan tooltip di sebelah kanan yang bertuliskan &#39;Tooltip&#39;.

Saya cenderung menggunakan atribut, bukan class untuk hal-hal seperti ini, sehingga <tool-tip> tidak dapat memiliki beberapa posisi yang ditetapkan padanya secara bersamaan. Hanya boleh ada satu atau tidak sama sekali.

Terakhir, tempatkan elemen <tool-tip> di dalam elemen yang ingin Anda berikan tooltip. Di sini saya membagikan teks alt kepada pengguna yang dapat melihat dengan menempatkan gambar dan <tool-tip> di dalam elemen <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>

Screenshot
gambar dengan tooltip yang bertuliskan &#39;Logo tengkorak GUI Challenges&#39;.

Di sini saya menempatkan <tool-tip> di dalam elemen <abbr>:

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

Screenshot
paragraf dengan akronim HTML yang digarisbawahi dan tooltip di atasnya
yang menyatakan &#39;Hyper Text Markup Language&#39;.

Aksesibilitas

Karena saya telah memilih untuk membuat tooltip, bukan toggletip, bagian ini jauh lebih sederhana. Pertama, izinkan saya menguraikan pengalaman pengguna yang kita inginkan:

  1. Di ruang yang terbatas atau antarmuka yang berantakan, sembunyikan pesan tambahan.
  2. Ketika pengguna mengarahkan kursor, memfokuskan, atau menggunakan sentuhan untuk berinteraksi dengan elemen, menampilkan pesan.
  3. Saat pengarahan kursor, fokus, atau sentuhan berakhir, sembunyikan pesan lagi.
  4. Terakhir, pastikan gerakan dikurangi jika pengguna telah menentukan preferensi untuk gerakan yang dikurangi.

Tujuan kami adalah pesan tambahan on demand. Pengguna mouse atau keyboard yang dapat melihat dapat mengarahkan kursor untuk membuka pesan, membacanya dengan mata mereka. Pengguna pembaca layar yang tidak dapat melihat dapat berfokus untuk menampilkan pesan, yang diterima secara lisan melalui alat mereka.

Screenshot VoiceOver MacOS yang membaca link dengan tooltip

Di bagian sebelumnya, kita telah membahas hierarki aksesibilitas, peran tooltip, dan inert. Yang tersisa adalah mengujinya dan memverifikasi pengalaman pengguna yang sesuai untuk menampilkan pesan tooltip kepada pengguna. Setelah pengujian, tidak jelas apakah bagian mana dari pesan suara yang merupakan tooltip. Ini dapat dilihat saat melakukan proses debug di hierarki aksesibilitas, teks link "atas" dijalankan bersama, tanpa ragu-ragu, dengan kata "Lihat, tooltip!". Pembaca layar tidak merusak atau mengidentifikasi teks sebagai konten {i>tooltip<i}.

Screenshot
Hierarki Aksesibilitas Chrome DevTools dengan teks link yang bertuliskan
&#39;top Hey, a tooltip!&#39;.

Tambahkan elemen pseudo khusus pembaca layar ke <tool-tip> dan kita dapat menambahkan teks perintah kita sendiri untuk pengguna tunanetra.

&::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;
}

Di bawah ini Anda dapat melihat hierarki aksesibilitas yang diperbarui, yang kini memiliki titik koma setelah teks link dan perintah untuk tooltip "Memiliki tooltip: ".

Screenshot yang diperbarui dari Hierarki Aksesibilitas Chrome DevTools dengan
teks link yang memiliki frasa yang lebih baik, &#39;top ; Has tooltip: Hey, a tooltip!&#39;.

Sekarang, saat pengguna pembaca layar memfokuskan link, akan muncul pesan "atas" dan berhenti sejenak, lalu mengumumkan "memiliki tooltip: tampilan, tooltip". Hal ini memberi pengguna pembaca layar beberapa petunjuk UX yang bagus. Hesitation memberikan pemisahan yang baik antara teks link dan tooltip. Selain itu, saat "has tooltip" diumumkan, pengguna pembaca layar dapat dengan mudah membatalkannya jika sudah pernah mendengarnya. Ini sangat mengingatkan untuk mengarahkan kursor dan memindahkan kursor dengan cepat, karena Anda telah melihat pesan tambahan. Hal ini terasa seperti paritas UX yang bagus.

Gaya

Elemen <tool-tip> akan menjadi turunan dari elemen yang mewakili pesan tambahannya, jadi mari kita mulai dengan hal-hal penting untuk efek overlay. Keluarkan dari alur dokumen dengan position absolute:

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

Jika induknya bukan konteks bertumpuk, tooltip akan memosisikan dirinya ke yang terdekat, yang tidak kita inginkan. Ada pemilih baru di blok yang dapat membantu, :has():

Dukungan Browser

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

Sumber

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

Jangan terlalu khawatir dengan dukungan browser. Pertama, ingatlah bahwa tooltip ini adalah tambahan. Jika tidak berhasil, tidak apa-apa. Kedua, di bagian JavaScript, kita akan men-deploy skrip untuk melakukan polyfill pada fungsi yang kita perlukan untuk browser tanpa dukungan :has().

Selanjutnya, mari kita buat tooltip tidak interaktif sehingga tidak mencuri peristiwa pointer dari elemen induknya:

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

Kemudian, sembunyikan tooltip dengan opasitas sehingga kita dapat mentransisikan tooltip dengan crossfade:

tool-tip {
  opacity: 0;
}

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

:is() dan :has() melakukan pekerjaan berat di sini, membuat tool-tip yang berisi elemen induk menyadari interaktivitas pengguna untuk mengalihkan visibilitas tooltip turunan. Pengguna mouse dapat mengarahkan kursor, pengguna keyboard dan pembaca layar dapat berfokus, dan pengguna sentuh dapat mengetuk.

Dengan overlay tampilkan dan sembunyikan yang berfungsi untuk pengguna yang dapat melihat, saatnya menambahkan beberapa gaya untuk tema, posisi, dan menambahkan bentuk segitiga ke balon. Gaya berikut mulai menggunakan properti kustom, yang dibuat berdasarkan jarak yang sangat jauh, tetapi juga menambahkan bayangan, tipografi, dan warna sehingga terlihat seperti tooltip mengambang:

Screenshot
tooltip dalam mode gelap, yang mengambang di atas link &#39;block-start&#39;.

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;
}

Penyesuaian tema

Tooltip hanya memiliki beberapa warna untuk dikelola karena warna teks diwarisi dari halaman melalui kata kunci sistem CanvasText. Selain itu, karena kita telah membuat properti kustom untuk menyimpan nilai, kita hanya dapat memperbarui properti kustom tersebut dan membiarkan tema menangani sisanya:

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

Screenshot
tampilan tooltip versi terang dan gelap secara berdampingan.

Untuk tema terang, kita menyesuaikan latar belakang menjadi putih dan membuat bayangan jauh lebih lemah dengan menyesuaikan opasitas.

Kanan ke kiri

Untuk mendukung mode pembacaan kanan ke kiri, properti kustom akan menyimpan nilai arah dokumen ke dalam nilai masing-masing -1 atau 1.

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

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

Ini dapat digunakan untuk membantu memosisikan tooltip:

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

Serta membantu mengetahui lokasi segitiga:

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

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

Terakhir, juga dapat digunakan untuk transformasi logis pada translateX():

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

Pemosisi tooltip

Posisikan tooltip secara logis dengan properti inset-block atau inset-inline untuk menangani posisi tooltip fisik dan logis. Kode berikut menunjukkan cara setiap empat posisi diberi gaya untuk arah kiri ke kanan dan kanan ke kiri.

Perataan atas dan awal blok

Screenshot
yang menampilkan perbedaan penempatan antara posisi atas kiri-ke-kanan
dan posisi atas kanan-ke-kiri.

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;
}

Perataan kanan dan inline-end

Screenshot
yang menampilkan perbedaan penempatan antara posisi kanan kiri-ke-kanan
dan posisi akhir inline kanan-ke-kiri.

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);
}

Perataan bawah dan akhir blok

Screenshot
yang menampilkan perbedaan penempatan antara posisi kiri-ke-kanan
bawah dan posisi akhir blok kanan-ke-kiri.

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;
}

Perataan kiri dan inline-start

Screenshot
yang menampilkan perbedaan penempatan antara posisi mulai dari kiri-ke-kanan
dan posisi inline-start kanan-ke-kiri.

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);
}

Animasi

Sejauh ini kita hanya mengaktifkan/menonaktifkan visibilitas tooltip. Di bagian ini, pertama-tama kita akan menganimasikan opasitas untuk semua pengguna, karena ini adalah transisi gerakan yang umumnya aman dan umumnya aman. Kemudian, kita akan menganimasikan posisi transformasi sehingga tooltip akan terlihat bergeser keluar dari elemen induk.

Transisi default yang aman dan bermakna

Tata gaya elemen tooltip untuk transisi opasitas dan transformasi, seperti ini:

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;
}

Menambahkan {i>motion <i}pada transisi

Untuk setiap sisi tempat tooltip dapat muncul, jika pengguna tidak keberatan dengan gerakan, posisikan properti translateX sedikit dengan memberinya jarak kecil untuk berpindah dari:

@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;
  }
}

Perhatikan bahwa ini menetapkan status "keluar", karena status "masuk" berada di translateX(0).

JavaScript

Menurut saya, JavaScript bersifat opsional. Hal ini karena tidak ada tooltip ini yang harus dibaca untuk menyelesaikan tugas di UI Anda. Jadi, jika tooltip benar-benar gagal, seharusnya tidak masalah. Hal ini juga berarti kita dapat memperlakukan tooltip sebagai peningkatan bertahap. Pada akhirnya, semua browser akan mendukung :has() dan skrip ini dapat dihentikan sepenuhnya.

Skrip polyfill melakukan dua hal, dan hanya melakukannya jika browser tidak mendukung :has(). Pertama, periksa dukungan :has():

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

Selanjutnya, temukan elemen induk <tool-tip> dan beri nama class untuk digunakan:

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

Selanjutnya, masukkan kumpulan gaya yang menggunakan nama class tersebut, yang menyimulasikan pemilih :has() untuk perilaku yang sama persis:

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)
}

Hanya itu, sekarang semua browser akan dengan senang hati menampilkan tooltip jika :has() tidak didukung.

Kesimpulan

Setelah mengetahui cara saya melakukannya, bagaimana cara Anda‽ 🙂 Saya benar-benar menantikan popup API untuk membuat tips beralih lebih mudah, lapisan atas tanpa pertempuran indeks z, dan anchor API untuk memosisikan berbagai hal di jendela dengan lebih baik. Sebelum itu, saya akan membuat tooltip.

Mari kita diversifikasi pendekatan dan pelajari semua cara untuk mem-build di web.

Buat demo, link tweet saya, dan saya akan menambahkannya ke bagian remix komunitas di bawah.

Remix komunitas

Belum ada apa-apa di sini.

Resource