CSS Custom Highlight API מאפשר לכם לעצב טווחי טקסט שרירותיים בדף בלי לשנות את מבנה ה-DOM. כך אפשר להדגיש תוצאות חיפוש, לצבוע תחביר, להציג סמנים של עריכה משותפת או לסמן שגיאות איות ודקדוק בלי לעטוף טקסט ברכיבים נוספים או להסתמך על שינויים ב-innerHTML.
הטמעה בסיסית
כדי להדגיש טווחי טקסט, צריך לאסוף את צומתי הטקסט של היעד, ליצור אובייקטים מסוג Range ו-Highlight, לרשום אותם ב-HighlightRegistry ואז לעצב אותם באמצעות הרכיב המדומה ::highlight().
1. אוספים צומתי טקסט ויוצרים טווחים
משתמשים ב-TreeWalker כדי לאסוף את כל צומתי הטקסט ברכיב היעד, ואז יוצרים אובייקטים מסוג Range שמצביעים על היסט התווים שרוצים להדגיש.
const article = document.querySelector("article");
// MANDATORY: Use TreeWalker to collect text nodes — do not manipulate innerHTML.
const treeWalker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
const allTextNodes = [];
let currentNode = treeWalker.nextNode();
while (currentNode) {
allTextNodes.push(currentNode);
currentNode = treeWalker.nextNode();
}
// MANDATORY: Set range start/end on text nodes, not element nodes.
const range = new Range();
range.setStart(textNode, matchStartIndex);
range.setEnd(textNode, matchEndIndex);
שמרו במטמון את רשימת צומתי הטקסט ובנו אותה מחדש רק כשתוכן ה-DOM משתנה בפועל, כי המעבר על העץ הוא פעולה יקרה.
2. יצירת רגעים בולטים מהטווחים
אפשר לקבץ אובייקט אחד או יותר מסוג Range לתוך Highlight. כמה טווחים שחולקים את אותו סגנון שייכים להדגשה אחת.
const searchHighlight = new Highlight(...matchingRanges);
3. רישום ההדגשה ב-Registry
רושמים כל Highlight בשם מותאם אישית באמצעות CSS.highlights, שהוא HighlightRegistry דמוי Map.
// MANDATORY: Clear previous highlights before registering new ones
// to avoid stale ranges persisting on the page.
CSS.highlights.clear();
CSS.highlights.set("search-results", searchHighlight);
כשיש כמה הדגשות חופפות, משתמשים במאפיין priority כדי לשלוט בסדר הערימה. הדגשות בעדיפות גבוהה יותר מופיעות מעל הדגשות בעדיפות נמוכה יותר.
const primary = new Highlight(...primaryRanges);
primary.priority = 1;
const secondary = new Highlight(...secondaryRanges);
secondary.priority = 0; // painted first (behind primary)
CSS.highlights.set("primary", primary);
CSS.highlights.set("secondary", secondary);
4. סגנון עם ::highlight()
משתמשים ברכיב המדומה ::highlight() ב-CSS כדי לעצב כל הדגשה רשומה לפי שם.
::highlight(search-results) {
background-color: #ffdd00;
color: black;
}
רק קבוצה מוגבלת של מאפייני CSS פועלת בתוך ::highlight(): color, background-color, text-decoration והמאפיינים המפורטים שלו, text-shadow, -webkit-text-stroke-color, -webkit-text-fill-color ו--webkit-text-stroke-width. המערכת מתעלמת ממאפיינים כמו background-image, font-size או padding.
נגישות
AVOID: להשתמש בהדגשות בהתאמה אישית במקום ב-HTML סמנטי.
קבוצות מילות מפתח בהתאמה אישית הן רק לצורך הצגה ולא נחשפות לעץ הנגישות. אם הטקסט המודגש רלוונטי מבחינה סמנטית למסמך (למשל, פסקה שהמשתמש בחר), צריך להשתמש במקום זאת ב-<mark>. הדגשות בהתאמה אישית מיועדות לאפקטים חולפים, חזותיים בלבד, כמו תוצאות חיפוש או צביעת תחביר.
הדגשות לא צריכות להסתמך רק על צבע כדי להעביר משמעות. אם הדגשה מציינת שגיאה, צריך להוסיף לה עוד אינדיקטור חזותי כמו text-decoration: wavy underline או תווית טקסט סמוכה. חשוב לוודא שיש מספיק ניגודיות בין הרקע של ההדגשה לבין צבע הטקסט כדי לעמוד בדרישות של WCAG 2.1 (לפחות 4.5:1 לטקסט רגיל).
אסטרטגיות חלופיות
בדפדפנים שלא תומכים ב-CSS Custom Highlight API, צריך לספק חוויית שימוש בסיסית ופונקציונלית שבה הטקסט עדיין קריא, גם בלי ההדגשה החזותית.
אתם יכולים לזהות תמיכה לפני השימוש ב-API:
if (CSS.highlights) {
// CSS Custom Highlight API is supported.
} else {
// Fallback: wrap matches in <mark> elements.
}
אם ההדגשה קריטית לחוויית המשתמש, אפשר להשתמש באלמנטים של <mark> כדי לעטוף את הטקסט התואם. הפעולה הזו משנה את ה-DOM, לכן חשוב לשמור על רכיבי event listener ולהימנע מפגיעה במבנה המסמך.
if (!CSS.highlights) {
// Walk text nodes and wrap matches in <mark>, preserving structure.
const walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
const nodes = [];
for (let n = walker.nextNode(); n; n = walker.nextNode()) nodes.push(n);
const term = searchTerm.toLowerCase();
for (const textNode of nodes) {
const text = textNode.textContent;
let pos = text.toLowerCase().indexOf(term);
if (pos === -1) continue;
const frag = document.createDocumentFragment();
let last = 0;
while (pos !== -1) {
frag.append(text.slice(last, pos));
const mark = document.createElement("mark");
// textContent assignment avoids HTML injection.
mark.textContent = text.slice(pos, pos + term.length);
frag.append(mark);
last = pos + term.length;
pos = text.toLowerCase().indexOf(term, last);
}
frag.append(text.slice(last));
textNode.replaceWith(frag);
}
}