סקירה בסיסית של אופן בניית רכיב מותאם אישית של tooltip שמותאם לצבעים ונגיש.
בפוסט הזה אני רוצה לשתף את המחשבות שלי על איך ליצור <tool-tip>
רכיב בהתאמה אישית עם נגישות ועם התאמה לצבעים. אפשר לנסות את ההדגמה ולראות את המקור.
אם אתם מעדיפים לצפות בסרטון, הנה גרסת YouTube של הפוסט הזה:
סקירה כללית
הסבר קצר הוא שכבת-על לא אינטראקטיבית, לא חוסמת ולא מודאלית, שמכילה מידע נוסף על ממשקי משתמש. הוא מוסתר כברירת מחדל ומוצג כשמעבירים את העכבר מעל אלמנט משויך או מתמקדים בו. אי אפשר לבחור הסבר קצר או לבצע בו פעולות ישירות. הסברים קצרים לא יכולים להחליף תוויות או מידע חשוב אחר. המשתמש צריך להיות מסוגל להשלים את המשימה שלו בלי הסבר קצר.

לא מומלץ: להסתמך על תיאורי כלים במקום על תוויות
הסבר קצר לעומת תיאור קצר
בדומה לרכיבים רבים, יש תיאורים שונים של מהו תיאור קצר, למשל ב-MDN, WAI ARIA, Sarah Higley ו-Inclusive Components. אהבתי את ההפרדה בין תיאורי הכלים לבין תיאורי המעבר. הטולטיפ צריך להכיל מידע נוסף לא אינטראקטיבי, בעוד שהטוגלטיפ יכול להכיל אינטראקטיביות ומידע חשוב. הסיבה העיקרית לפער היא נגישות. איך מצפים מהמשתמשים לנווט אל החלון הקופץ ולגשת למידע ולכפתורים שבו? הסברים קצרים הופכים למורכבים במהירות.
הנה סרטון של תיאור קצר ממתג באתר Designcember. זהו כיסוי עם אינטראקטיביות שהמשתמש יכול להצמיד כדי לפתוח ולעיין בו, ואז לסגור אותו באמצעות סגירה קלה או מקש Escape:
כדי לעמוד באתגר הזה, יצרנו tooltip שמתבסס כמעט כולו על CSS. כך עושים את זה.
Markup
בחרתי להשתמש באלמנט מותאם אישית <tool-tip>
. אם מחברים לא רוצים, הם לא צריכים להפוך רכיבים מותאמים אישית לרכיבי אינטרנט. הדפדפן יתייחס ל-<foo-bar>
בדיוק כמו אל <div>
. אפשר לחשוב על רכיב מותאם אישית כמו על שם מחלקה עם פחות ספציפיות. אין שימוש ב-JavaScript.
<tool-tip>A tooltip</tool-tip>
זה כמו תג div עם טקסט בפנים. אנחנו יכולים להתחבר לעץ הנגישות של קוראי מסך מתאימים על ידי הוספת [role="tooltip"]
.
<tool-tip role="tooltip">A tooltip</tool-tip>
עכשיו, קוראי המסך מזהים אותו כהסבר קצר. בדוגמה הבאה אפשר לראות שלאלמנט הקישור הראשון יש אלמנט מוכר של תיאור קצר בעץ שלו, אבל לאלמנט השני אין. בקישור השני אין תפקיד. בקטע 'סגנונות' נשפר את תצוגת העץ הזו.
בשלב הבא צריך לוודא שאי אפשר להתמקד בתיבת הטיפים. אם קורא המסך לא מבין את התפקיד של תיאור הכלי, הוא יאפשר למשתמשים להתמקד ב-<tool-tip>
כדי לקרוא את התוכן, וחוויית המשתמש לא צריכה את זה. קוראי מסך יוסיפו את התוכן לרכיב ההורה, ולכן לא צריך להגדיר את המיקוד כדי להפוך אותו לנגיש. כאן אפשר להשתמש ב-inert
כדי לוודא שאף משתמש לא ימצא בטעות את תוכן הטיפ הזה ברצף הכרטיסיות שלו:
<tool-tip inert role="tooltip">A tooltip</tool-tip>
לאחר מכן בחרתי להשתמש במאפיינים כממשק כדי לציין את המיקום של הכלי לטיפים. כברירת מחדל, כל ה-<tool-tip>
s יניחו מיקום 'עליון', אבל אפשר להתאים אישית את המיקום ברכיב על ידי הוספת tip-position
:
<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>
אני בדרך כלל משתמש במאפיינים במקום במחלקות לדברים כאלה, כדי שלא יהיו כמה מיקומים שמוקצים ל-<tool-tip>
בו-זמנית.
יכול להיות רק אחד או אף אחד.
לבסוף, ממקמים את האלמנטים <tool-tip>
בתוך האלמנט שרוצים להוסיף לו תיאור קצר. כאן אני משתף את הטקסט alt
עם משתמשים עם ראייה תקינה על ידי הצבת תמונה וalt
בתוך רכיב <picture>
:<tool-tip>
<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>
כאן אני מציב <tool-tip>
בתוך רכיב <abbr>
:
<p>
The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>
נגישות
מכיוון שבחרתי ליצור תיאורי כלים ולא תיאורים מתחלפים, הקטע הזה פשוט יותר. קודם כל, אפרט את חוויית המשתמש הרצויה שלנו:
- במרחבים מצומצמים או בממשקים עמוסים, כדאי להסתיר הודעות משניות.
- כשמשתמש מעביר את העכבר מעל אלמנט, מתמקד בו או משתמש במגע כדי לבצע אינטראקציה איתו, ההודעה נחשפת.
- כשמעבירים את העכבר, מתמקדים או מסיימים את המגע, ההודעה מוסתרת שוב.
- לבסוף, מוודאים שכל התנועות מופחתות אם המשתמש ציין העדפה לתנועות מופחתות.
המטרה שלנו היא לספק הודעות משלימות לפי דרישה. משתמשים בעכבר או במקלדת עם ראייה יכולים להעביר את העכבר מעל ההודעה כדי לחשוף אותה ולקרוא אותה באמצעות העיניים. משתמש בקורא מסך שלא רואה יכול להתמקד בהודעה כדי לחשוף אותה, והוא ישמע אותה דרך הכלי.

בקטע הקודם הסברנו על עץ הנגישות, על תפקיד הכלי לטיפים ועל המאפיין inert. מה שנשאר הוא לבדוק את זה ולוודא שחוויית המשתמש חושפת את הודעת הכלי לטיפים למשתמש בצורה מתאימה. בבדיקה שערכתי, לא ברור איזה חלק מההודעה הקולית הוא תיאור קצר. אפשר לראות את זה גם בזמן ניפוי באגים בעץ הנגישות. טקסט הקישור 'למעלה' מופיע ברצף, בלי הפסקה, עם 'תראו, תיאורי כלים!'. קורא המסך לא מפריד את הטקסט או מזהה אותו כתוכן של תיאור כלי.
מוסיפים פסאודו-אלמנט לקורא מסך בלבד לרכיב <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;
}
בהמשך אפשר לראות את עץ הנגישות המעודכן, שכולל עכשיו נקודה ופסיק אחרי טקסט הקישור והנחיה לתיאור הכלי 'יש תיאור כלי: '.
עכשיו, כשמשתמש בקורא מסך מתמקד בקישור, קורא המסך אומר 'למעלה', עוצר לרגע ואז אומר 'יש הסבר קצר: תסתכל, הסברים קצרים'. כך משתמשים בקורא מסך מקבלים כמה רמזים שימושיים לגבי חוויית המשתמש. ההשהיה יוצרת הפרדה טובה בין הטקסט של הקישור לבין תיאור הכלי. בנוסף, כשקורא מסך מכריז על "יש הסבר קצר", משתמש בקורא מסך יכול לבטל אותו בקלות אם הוא כבר שמע אותו בעבר. הפעולה הזו דומה מאוד להעברת העכבר מעל ההודעה והרחקתו ממנה במהירות, כי כבר ראיתם את ההודעה הנוספת. הרגשתי שחוויית המשתמש הייתה טובה בשני המקרים.
סגנונות
האלמנט <tool-tip>
יהיה צאצא של האלמנט שהוא מייצג הודעות משניות עבורו, ולכן נתחיל קודם עם הרכיבים החיוניים לאפקט השכבת-העל. כדי להוציא את המסמך מזרימת המסמכים, צריך ללחוץ על position absolute
:
tool-tip {
position: absolute;
z-index: 1;
}
אם הרכיב ההורה הוא לא הקשר הערימה, תיאור הכלים ימוקם ליד הרכיב הקרוב ביותר שהוא כן הקשר הערימה, וזה לא מה שאנחנו רוצים. יש כלי חדש לבחירת בלוקים שיכול לעזור, :has()
:
:has(> tool-tip) {
position: relative;
}
אל תדאגו יותר מדי לגבי תמיכה בדפדפן. קודם כול, חשוב לזכור שההסברים הקצרים האלה הם תוספת. אם הם לא עובדים, זה בסדר. בשלב השני, בקטע JavaScript, נטמיע סקריפט כדי להוסיף פוליפיל לפונקציונליות שאנחנו צריכים בדפדפנים ללא תמיכה ב-:has()
.
בשלב הבא, נגדיר את תיאורי הכלים כלא אינטראקטיביים כדי שלא יגנבו אירועי הצבעה מהרכיב ההורה שלהם:
tool-tip {
…
pointer-events: none;
user-select: none;
}
אחר כך מסתירים את ההסבר הקצר באמצעות שקיפות כדי שנוכל להעביר את ההסבר הקצר באמצעות מעבר הדרגתי:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
ו-:has()
מבצעים כאן את העבודה הקשה, וגורמים לאלמנטים האב שמכילים את tool-tip
להיות מודעים לאינטראקטיביות של המשתמש כדי להחליף את מצב החשיפה של תיאור הכלים של הצאצא. משתמשים בעכבר יכולים להעביר את העכבר מעל הרכיב, משתמשים במקלדת ובקורא מסך יכולים להתמקד בו, ומשתמשים במגע יכולים להקיש עליו.
אחרי שמוודאים שהאפשרות להצגה ולהסתרה של שכבת העל פועלת אצל משתמשים עם ראייה, מוסיפים סגנונות לעיצוב, למיקום ולהוספת הצורה המשולשת לבועה. הסגנונות הבאים מתחילים להשתמש במאפיינים מותאמים אישית, וממשיכים את מה שעשינו עד עכשיו, אבל גם מוסיפים צללים, טיפוגרפיה וצבעים כדי שזה ייראה כמו תיאור כלים צף:
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;
}
שינויים בעיצוב
יש רק כמה צבעים בתיבת הטיפ לניהול, כי צבע הטקסט עובר בירושה מהדף באמצעות מילת המפתח CanvasText
של המערכת. בנוסף, מכיוון שיצרנו מאפיינים מותאמים אישית כדי לאחסן את הערכים, אנחנו יכולים לעדכן רק את המאפיינים המותאמים אישית האלה ולתת לערכת הנושא לטפל בשאר:
@media (prefers-color-scheme: light) {
tool-tip {
--_bg: white;
--_shadow-alpha: 15%;
}
}
במצב בהיר, הרקע לבן והצללים חלשים יותר כי שקיפות הצללים מותאמת.
מימין לשמאל
כדי לתמוך במצבי קריאה מימין לשמאל, מאפיין בהתאמה אישית יאחסן את הערך של כיוון המסמך בערך של -1 או 1 בהתאמה.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
אפשר להשתמש בערך הזה כדי למקם את תיאור הכלים:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
וגם לעזור לכם לדעת איפה המשולש נמצא:
tool-tip[tip-position="right"]::after {
--_tip: var(--_left-tip);
}
tool-tip[tip-position="right"]:dir(rtl)::after {
--_tip: var(--_right-tip);
}
לבסוף, אפשר להשתמש ב- גם לטרנספורמציות לוגיות ב-translateX()
:
--_x: calc(var(--isRTL) * -3px * -1);
מיקום ההסבר הקצר
כדי לטפל במיקומים הפיזיים והלוגיים של ההסבר הקצר, צריך למקם אותו באופן לוגי באמצעות המאפיינים inset-block
או inset-inline
. הקוד הבא מראה איך כל אחד מארבעת המיקומים מעוצב גם בכיוון משמאל לימין וגם בכיוון מימין לשמאל.
יישור למעלה ולתחילת הבלוק
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;
}
יישור לימין ולסוף השורה
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);
}
יישור לתחתית ולסוף הבלוק
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;
}
יישור לשמאל ולתחילת השורה
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);
}
Animation
עד עכשיו רק החלפנו את מצב החשיפה של תיאור הכלים. בקטע הזה נציג קודם אנימציה של אטימות לכל המשתמשים, כי זה בדרך כלל מעבר בטוח עם תנועה מופחתת. לאחר מכן נפעיל אנימציה של מיקום הטרנספורמציה כך שההסבר הקצר יופיע כאילו הוא מחליק החוצה מאלמנט האב.
מעבר בטוח ומשמעותי לברירת מחדל
מעצבים את רכיב ה-tooltip כך שיעבור בין אטימות לטרנספורמציה, כמו בדוגמה הבאה:
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;
}
הוספת תנועה למעבר
לכל אחד מהצדדים שבהם יכול להופיע תיאור קצר, אם המשתמש לא מתנגד לתנועה, ממקמים מעט את המאפיין translateX על ידי מתן מרחק קטן לתנועה מ:
@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;
}
}
שימו לב שההגדרה הזו היא של מצב 'יציאה', כי מצב 'כניסה' הוא translateX(0)
.
JavaScript
לדעתי, השימוש ב-JavaScript הוא אופציונלי. הסיבה לכך היא שאף אחד מההסברים הקצרים האלה לא אמור להיות חובה כדי לבצע משימה בממשק המשתמש. לכן, אם הכלי לטיפים נכשל לחלוטין, זה לא אמור להיות עניין גדול. המשמעות היא גם שאפשר להתייחס לתיבות הטיפים כאל שיפורים הדרגתיים. בסופו של דבר, כל הדפדפנים יתמכו ב-:has()
, ואפשר יהיה להסיר את הסקריפט הזה לגמרי.
סקריפט ה-polyfill עושה שני דברים, והוא עושה אותם רק אם הדפדפן לא תומך ב-:has()
. קודם כל, בודקים אם יש תמיכה ב:has()
:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
בשלב הבא, מאתרים את רכיבי ההורה של רכיבי <tool-tip>
ונותנים להם שם מחלקה שאפשר לעבוד איתו:
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
}
לאחר מכן, מחדירים קבוצה של סגנונות שמשתמשים בשם המחלקה הזה, ומדמים את הבורר :has()
כדי לקבל בדיוק את אותה התנהגות:
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)
}
זהו, עכשיו כל הדפדפנים יציגו את תיאורי הכלים אם הם לא תומכים ב-:has()
.
סיכום
עכשיו, אחרי שסיפרתי לך איך עשיתי את זה, איך היית עושה את זה‽ 🙂 אני ממש מחכה ל-API של popup
כדי להקל על יצירת בועות מידע, ל-top
layer כדי להימנע מבעיות עם z-index, ול-API של anchor
כדי לשפר את המיקום של רכיבים בחלון. עד אז, אצור תיאורי כלים.
כדאי לגוון את הגישות שלנו וללמוד את כל הדרכים לבנות אתרים.
אפשר ליצור סרטון הדגמה, לצייץ לי קישורים, ואוסיף אותו לקטע של רמיקסים מהקהילה שבהמשך!
רמיקסים מהקהילה
עדיין אין מה לראות כאן.
משאבים
- קוד המקור ב-Github