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 อย่างน้อย 1 รายการเป็น Highlight ช่วงหลายช่วงที่ใช้รูปแบบเดียวกันจะอยู่ในไฮไลต์เดียว
const searchHighlight = new Highlight(...matchingRanges);
3. ลงทะเบียนไฮไลต์ในรีจิสทรี
ลงทะเบียน 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 และ Longhand, 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 ดังนั้นโปรดระมัดระวังในการเก็บรักษา 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);
}
}