Membuat komponen tooltip

Ringkasan dasar tentang cara membuat elemen kustom tooltip yang mudah diakses dan dapat menyesuaikan warna.

Dalam postingan ini, saya ingin membagikan pemikiran saya tentang cara membuat elemen kustom <tool-tip> yang mudah diakses dan dapat menyesuaikan warna. Coba demo dan lihat sumbernya.

Tooltip ditampilkan berfungsi di berbagai contoh dan skema warna

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

Ringkasan

Tooltip adalah overlay non-modal, non-pemblokiran, non-interaktif yang berisi informasi tambahan untuk antarmuka pengguna. Secara default, elemen ini disembunyikan dan akan ditampilkan saat elemen terkait diarahkan kursor atau difokuskan. Tips tidak dapat dipilih atau berinteraksi secara langsung. Tips alat bukan pengganti label atau informasi penting lainnya. Pengguna harus dapat menyelesaikan tugasnya sepenuhnya tanpa tips alat.

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

Toggletip vs. Tooltip

Seperti banyak komponen, ada berbagai deskripsi tentang apa itu tooltip, misalnya di MDN, WAI ARIA, Sarah Higley, dan Inclusive Components. Saya menyukai pemisahan antara tooltip dan toggletip. Tips alat harus berisi informasi tambahan non-interaktif, sedangkan toggletip dapat berisi interaktivitas dan informasi penting. Alasan utama perbedaan ini adalah aksesibilitas, bagaimana pengguna diharapkan membuka pop-up dan memiliki akses ke informasi dan tombol di dalamnya. Toggletip cepat menjadi rumit.

Berikut video toggletip dari situs Designcember; overlay dengan interaktivitas yang dapat disematkan dan dijelajahi pengguna, lalu ditutup dengan penutupan ringan atau tombol escape:

Tantangan GUI ini menggunakan tooltip, yang berupaya melakukan hampir semuanya dengan CSS, dan berikut cara membuatnya.

Markup

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

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

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

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

Sekarang, pembaca layar akan mengenalinya sebagai tooltip. Lihat pada contoh berikut bagaimana elemen link pertama memiliki elemen tooltip yang dikenali di hierarkinya dan elemen kedua tidak? Pengguna kedua tidak memiliki peran tersebut. Di bagian gaya, kita akan meningkatkan tampilan hierarki ini.

Screenshot Pohon Aksesibilitas Chrome DevTools yang merepresentasikan HTML. Menampilkan
link dengan teks &#39;top ; Has tooltip: Hey, a tooltip!&#39; yang dapat difokuskan. Di dalamnya
terdapat teks statis &#39;top&#39; dan elemen tooltip.

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

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

Screenshot lain dari Pohon 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 mengasumsikan 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;A tooltip&#39;.

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

Terakhir, tempatkan elemen <tool-tip> di dalam elemen yang ingin Anda berikan tooltip-nya. 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 bertuliskan &#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 kami inginkan:

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

Tujuan kami adalah pesan tambahan sesuai permintaan. Pengguna mouse atau keyboard yang memiliki penglihatan dapat mengarahkan kursor untuk menampilkan pesan, dan membacanya dengan mata mereka. Pengguna pembaca layar yang tidak dapat melihat dapat memfokuskan untuk menampilkan pesan, dan menerimanya secara lisan melalui alat mereka.

Screenshot VoiceOver MacOS yang membacakan link dengan tooltip

Di bagian sebelumnya, kita membahas pohon aksesibilitas, peran tooltip, dan inert. Yang tersisa adalah mengujinya dan memverifikasi bahwa pengalaman pengguna yang sesuai menampilkan pesan tooltip kepada pengguna. Saat pengujian, tidak jelas bagian pesan yang dapat didengar yang merupakan tooltip. Hal ini juga dapat dilihat saat men-debug di pohon aksesibilitas, teks link "atas" dijalankan bersamaan, tanpa keraguan, dengan "Lihat, tooltip!". Pembaca layar tidak memecah atau mengidentifikasi teks sebagai konten tooltip.

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

Tambahkan elemen semu hanya pembaca layar ke <tool-tip> dan kita dapat menambahkan teks perintah kita sendiri untuk pengguna yang tidak dapat melihat.

&::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, Anda dapat melihat hierarki aksesibilitas yang telah diupdate, yang kini memiliki titik koma setelah teks link dan perintah untuk tooltip "Memiliki tooltip: ".

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

Sekarang, saat pengguna pembaca layar memfokuskan link, link tersebut akan mengucapkan "top" dan berhenti sejenak, lalu mengucapkan "has tooltip: look, tooltips". Hal ini memberikan beberapa petunjuk UX yang bagus kepada pengguna pembaca layar. Keraguan memberikan pemisahan yang baik antara teks link dan tooltip. Selain itu, saat "memiliki tooltip" diumumkan, pengguna pembaca layar dapat dengan mudah membatalkannya jika mereka sudah mendengarnya sebelumnya. Hal ini sangat mengingatkan pada tindakan mengarahkan kursor dan berhenti mengarahkan 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 diwakilinya untuk pesan tambahan, 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 induk bukan konteks penumpukan, tooltip akan memosisikan dirinya ke konteks penumpukan terdekat, yang bukan yang kita inginkan. Ada pemilih baru di blok yang dapat membantu, :has():

Browser Support

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

Source

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

Jangan terlalu khawatir tentang dukungan browser. Pertama, ingatlah bahwa tooltip ini bersifat tambahan. Jika tidak berfungsi, tidak masalah. Kedua, di bagian JavaScript, kita akan men-deploy skrip untuk melakukan polyfill pada fungsi yang kita butuhkan untuk browser tanpa dukungan :has().

Selanjutnya, mari kita buat tooltip menjadi non-interaktif agar tidak mencuri peristiwa penunjuk dari elemen induknya:

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

Kemudian, sembunyikan tooltip dengan opacity sehingga kita dapat melakukan transisi tooltip dengan crossfade:

tool-tip {
  opacity: 0;
}

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

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

Setelah overlay tampil dan sembunyi berfungsi untuk pengguna yang dapat melihat, saatnya menambahkan beberapa gaya untuk tema, pemosisian, dan penambahan bentuk segitiga ke balon. Gaya berikut mulai menggunakan properti kustom, yang dibangun dari tempat kita berada sejauh ini, tetapi juga menambahkan bayangan, tipografi, dan warna sehingga terlihat seperti tooltip mengambang:

Screenshot tooltip dalam mode gelap, 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 berdampingan dari versi terang dan gelap tooltip.

Untuk tema terang, kami menyesuaikan latar belakang menjadi putih dan membuat bayangan jauh lebih redup dengan menyesuaikan opasitasnya.

Kanan ke kiri

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

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

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

Properti ini dapat digunakan untuk membantu memosisikan tooltip:

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

Serta membantu menentukan 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);

Penempatan tooltip

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

Perataan atas dan awal blok

Screenshot yang menunjukkan 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 menunjukkan 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 menunjukkan perbedaan penempatan antara posisi bawah kiri-ke-kanan 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 menunjukkan perbedaan penempatan antara posisi kiri 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 mengalihkan visibilitas tooltip. Di bagian ini, kita akan menganimasikan opasitas terlebih dahulu untuk semua pengguna, karena ini adalah transisi gerakan yang dikurangi yang umumnya aman. Kemudian, kita akan menganimasikan posisi transformasi sehingga tooltip tampak meluncur keluar dari elemen induk.

Transisi default yang aman dan bermakna

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 gerakan ke transisi

Untuk setiap sisi tempat tooltip dapat muncul, jika pengguna tidak keberatan dengan gerakan, posisikan sedikit properti translateX dengan memberikan jarak kecil untuk bergerak 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 menyetel status "keluar", karena status "masuk" berada di translateX(0).

JavaScript

Menurut saya, JavaScript bersifat opsional. Hal ini karena tidak ada tooltip yang harus dibaca untuk menyelesaikan tugas di UI Anda. Jadi, jika tooltip gagal sepenuhnya, tidak akan menjadi masalah besar. Artinya juga kami dapat memperlakukan tooltip sebagai peningkatan progresif. Pada akhirnya, semua browser akan mendukung :has() dan skrip ini dapat sepenuhnya dihapus.

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 sekumpulan gaya yang menggunakan nama class tersebut, yang mensimulasikan 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)
}

Selesai, sekarang semua browser akan menampilkan tooltip dengan baik jika :has() tidak didukung.

Kesimpulan

Sekarang setelah Anda tahu cara saya melakukannya, bagaimana menurut Anda? 🙂 Saya sangat menantikan popup API untuk mempermudah pembuatan toggletip, lapisan teratas tanpa pertarungan z-index, dan anchor API untuk memosisikan elemen di jendela dengan lebih baik. Sebelumnya, saya akan membuat tooltip.

Mari kita diversifikasi pendekatan kita dan pelajari semua cara untuk membangun di web.

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

Remix komunitas

Belum ada apa-apa di sini.

Resource