Membuat komponen tooltip

Ringkasan dasar tentang cara membangun elemen kustom tooltip yang adaptif warna dan dapat diakses.

Dalam postingan ini, saya ingin menyampaikan pendapat tentang cara mem-build elemen kustom <tool-tip> yang adaptif dan mudah diakses. Coba demonya dan lihat sumbernya.

Tooltip ditampilkan berfungsi di berbagai contoh dan skema warna

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

Ringkasan

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

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

Alihkan Tip vs. Tooltip

Seperti banyak komponen, terdapat berbagai deskripsi tentang tooltip, misalnya dalam MDN, WAI ARIA, Sarah Higley, dan Komponen Inklusif. Saya menyukai pemisahan antara tooltip dan tip. Tooltip harus berisi informasi tambahan non-interaktif, sedangkan tombol beralih dapat berisi interaktivitas dan informasi penting. Alasan utama kesenjangan ini adalah aksesibilitas, bagaimana pengguna diharapkan untuk membuka pop-up dan memiliki akses ke informasi dan tombol di dalamnya. Tombol beralih menjadi rumit dengan cepat.

Berikut ini video tombol beralih dari situs Designcember; overlay dengan interaktivitas yang dapat dibuka dan dijelajahi pengguna, lalu ditutup dengan tombol tutup ringan atau tombol escape:

Tantangan GUI ini membahas tooltip, yaitu 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 bisa menganggap elemen khusus seperti nama kelas dengan kurang spesifik. 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, bagi pembaca layar, alat ini dikenali sebagai tooltip. Lihat dalam contoh berikut, bagaimana elemen link pertama memiliki elemen tooltip yang dikenali dalam hierarkinya, sedangkan elemen kedua tidak? Yang kedua tidak memiliki peran itu. Di bagian gaya kita akan memperbaiki tampilan hierarki ini.

Screenshot
Diagram Aksesibilitas Chrome DevTools yang menampilkan 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 memerlukan tooltip agar 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 sehingga 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.

Saya kemudian memilih menggunakan atribut sebagai antarmuka untuk menentukan posisi tooltip. Secara default, semua <tool-tip> akan mengasumsikan posisi "atas", tetapi posisinya 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 menampilkan &#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 ada.

Terakhir, tempatkan elemen <tool-tip> di dalam elemen yang ingin Anda berikan tooltip-nya. Di sini saya membagikan teks alt kepada pengguna berpenglihatan normal 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
Tantangan GUI&#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
sebuah paragraf dengan akronim HTML digarisbawahi dan tooltip di atasnya
yang menyatakan &#39;Hyper Text Markup Language&#39;.

Aksesibilitas

Karena saya memilih untuk membuat tooltip dan bukan beralih tip, bagian ini jauh lebih sederhana. Pertama, saya akan menjelaskan pengalaman pengguna yang diinginkan:

  1. Dalam 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, memfokuskan, atau menyentuh 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 on demand. Pengguna mouse atau keyboard yang dapat melihat pesan dapat mengarahkan kursor untuk membuka pesan, membacanya dengan mata mereka. Pengguna pembaca layar yang tidak berpenglihatan bisa fokus untuk membuka pesan, lalu menerimanya secara audio melalui alat mereka.

Screenshot MacOS VoiceOver yang membaca link dengan tooltip

Di bagian sebelumnya, kita telah membahas hierarki aksesibilitas, peran tooltip dan inert, yang perlu dilakukan adalah mengujinya dan memverifikasi pengalaman pengguna dengan tepat mengungkapkan pesan tooltip kepada pengguna. Setelah pengujian, kami tidak dapat memastikan bagian mana dari pesan suara yang merupakan tooltip. Kolom ini juga dapat dilihat saat proses debug di hierarki aksesibilitas, teks link "top" dijalankan bersamaan, tanpa ragu-ragu, dengan "Look, tooltips!". Pembaca layar tidak merusak atau mengidentifikasi teks sebagai konten tooltip.

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

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

&::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 sekarang memiliki titik koma setelah teks link dan perintah untuk tooltip "Memiliki tooltip: ".

Screenshot yang diperbarui dari Pohon Aksesibilitas Chrome DevTools tempat
teks link memiliki frasa yang diperbaiki, &#39;top ; Memiliki tooltip: Hei, sebuah tooltip!&#39;.

Sekarang, saat pengguna pembaca layar memfokuskan link, pesan akan bertuliskan "top" dan melakukan jeda kecil, lalu mengumumkan "has tooltip: view, tooltips". Ini memberi beberapa petunjuk UX yang bagus kepada pengguna pembaca layar. Keraguan tersebut memberikan pemisahan yang jelas antara teks link dan tooltip. Selain itu, saat "memiliki tooltip" diumumkan, pengguna pembaca layar dapat membatalkannya dengan mudah jika pernah mendengarnya sebelumnya. Hal ini sangat mirip dengan tindakan mengarahkan kursor dan menggeser dengan cepat, seperti yang sudah Anda lihat dalam pesan tambahan ini. Ini terasa seperti paritas UX yang bagus.

Gaya

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

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

Jika induk bukan merupakan konteks yang bertumpuk, tooltip akan memosisikan diri ke konteks terdekat yang ada, dan ini bukan yang kita inginkan. Ada pemilih baru pada blok yang dapat membantu, :has():

Dukungan Browser

  • 105
  • 105
  • 121
  • 15,4

Sumber

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

Jangan terlalu khawatir tentang dukungan browser. Pertama, ingatlah bahwa tooltip ini bersifat tambahan. Jika mereka tidak berhasil, seharusnya tidak apa-apa. Kedua, di bagian JavaScript, kami akan men-deploy skrip untuk mem-polyfill fungsionalitas yang diperlukan untuk browser tanpa dukungan :has().

Selanjutnya, mari kita jadikan tooltip non-interaktif agar tidak mencuri peristiwa pointer dari elemen induknya:

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

Kemudian, sembunyikan tooltip dengan opasitas agar 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, sehingga membuat tool-tip yang berisi elemen induk mengetahui interaktivitas pengguna untuk mengalihkan visibilitas tooltip turunan. Pengguna mouse dapat mengarahkan kursor, keyboard dan pembaca layar dapat difokuskan, serta menyentuh pengguna.

Karena overlay tampilkan dan sembunyikan berfungsi untuk pengguna berpenglihatan normal, kini saatnya menambahkan beberapa gaya untuk penerapan tema, penempatan, dan penambahan bentuk segitiga ke balon. Gaya berikut mulai menggunakan properti kustom, yang dibangun di tempat kita begitu jauh, tetapi juga menambahkan bayangan, tipografi, dan warna agar 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 membuat properti khusus untuk menyimpan nilai, kita bisa memperbarui properti khusus itu saja dan membiarkan tema menangani sisanya:

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

Screenshot
berdampingan dari tooltip versi terang dan gelap.

Untuk tema terang, kita menyesuaikan latar belakang menjadi putih dan membuat bayangannya kurang kuat 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;
}

Tindakan ini dapat membantu dalam memosisikan tooltip:

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

Juga membantu di mana segitiga berada:

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 di 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 penataan gaya masing-masing posisi 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

Screenshot
yang menampilkan perbedaan penempatan antara posisi kanan kiri-ke-kanan
dan posisi inline-end 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 ujung blok

Screenshot
yang menampilkan perbedaan penempatan antara posisi bawah kiri-ke-kanan
dan posisi block-end 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 kiri-ke-kanan
dan posisi awal inline-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, pertama-tama kita akan menganimasikan opasitas untuk semua pengguna, karena ini adalah transisi gerakan pengurangan yang umumnya aman. Kemudian, kita akan menganimasikan posisi transformasi agar tooltip muncul bergeser keluar dari elemen induk.

Transisi default yang aman dan bermakna

Tata gaya elemen tooltip untuk mengubah 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, sebuah tooltip dapat muncul

@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 "out", karena status "in" berada di translateX(0).

JavaScript

Menurut pendapat saya, JavaScript bersifat opsional. Hal ini karena tooltip ini tidak memerlukan pembacaan untuk menyelesaikan tugas di UI Anda. Jadi, jika tooltip benar-benar gagal, seharusnya itu bukan masalah. Hal ini juga berarti kita dapat memperlakukan tooltip sebagai ditingkatkan secara 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)
}

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

Kesimpulan

Setelah Anda mengetahui cara saya melakukannya, bagaimana Anda‽ 🙂 Saya sangat menantikan popup API untuk mempermudah beralih tip, lapisan teratas untuk pertempuran tanpa indeks z, dan anchor API untuk memosisikan berbagai hal di jendela dengan lebih baik. Sementara itu, saya akan membuat tooltip.

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

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

Remix komunitas

Belum ada apa-apa di sini.

Referensi