Магия без лишних тегов: Укрощаем Custom Highlight API
Признайся, сколько раз тебе приходилось реализовывать поиск по странице или динамическую подсветку синтаксиса? Это всегда превращалось в сомнительное удовольствие. Ты берешь текст, режешь его на куски, оборачиваешь каждое совпадение в несчастный span с классом и молишься, чтобы верстка не поплыла. А если пользователь решит скопировать этот текст? А если там сложные вложенные теги? Добро пожаловать в мир боли.
Сегодня мы разберем Custom Highlight API — технологию, которая позволяет «красить» текст прямо поверх DOM-дерева, не внося в него никаких изменений. Это как накладывать фильтр в Instagram: картинка та же, но выглядит иначе.
Как мы страдали раньше
До появления этого API у нас был ровно один путь: мутация DOM. Нам приходилось программно находить текстовые узлы, разбивать их и вставлять новые элементы. Это порождало кучу проблем:
- Производительность: На больших документах перерисовка тысяч мелких элементов вешала браузер намертво.
- Слом структуры: Скрипты, полагающиеся на текстовое содержимое (например, скринридеры или старые добрые парсеры), сходили с ума от обилия лишних тегов.
- Стилистический хаос: Управлять специфичностью вложенных спанов было тем еще квестом. Чтобы избежать конфликтов, мы пытались использовать псевдоклассы :is() и :where() для чистого кода, но корень проблемы оставался — лишняя разметка.
Короче, мы создавали монстров из тегов ради банальной смены цвета фона.
Как делать правильно в 2026 году
Custom Highlight API предлагает элегантный подход. Мы работаем с объектами Range (диапазонами), которые указывают «от этого символа до того», и регистрируем их в специальном реестре CSS.highlights. Браузер сам отрисовывает выделение на виртуальном слое, не трогая твои священные теги.
Это работает чертовски быстро и позволяет разделять логику (какие данные выделить) и визуализацию (как выделить). Чтобы твои стили выделения не перекрывались другими правилами, можно даже внедрить каскадные слои CSS (@layer) для управления приоритетами.
Алгоритм действий простой:
- Создаем один или несколько объектов Range.
- Группируем их в объект Highlight.
- Регистрируем этот Highlight в системе под уникальным именем.
- Стилизуем это имя в CSS через псевдоэлемент ::highlight().
Готовый сниппет кода
Давай набросаем простую реализацию поиска, которая подсвечивает все вхождения слова в тексте без единого лишнего дива.
// JS часть: находим и регистрируем
const textNode = document.querySelector('p').firstChild;
const searchWord = "Highlight";
const ranges = [];
let pos = 0;
while ((pos = textNode.textContent.indexOf(searchWord, pos)) >= 0) {
const range = new Range();
range.setStart(textNode, pos);
range.setEnd(textNode, pos + searchWord.length);
ranges.push(range);
pos += searchWord.length;
}
// Создаем объект подсветки и регистрируем его
const searchHighlight = new Highlight(...ranges);
CSS.highlights.set("my-custom-search", searchHighlight);
/* CSS часть: задаем стиль */
::highlight(my-custom-search) {
background-color: #ffde03;
color: #000;
text-decoration: underline;
}
Частая ошибка новичков
Самый частый фейл — забывать о динамике. Custom Highlight API «живой», но Range привязан к конкретным узлам и смещениям. Если содержимое элемента изменится (например, через innerHTML), твои диапазоны могут «съехать» или стать невалидными.
Многие новички создают Highlight один раз при загрузке страницы и удивляются, почему после обновления данных подсветка ведет себя неадекватно. Запомни: если текст в DOM изменился, тебе нужно либо пересчитать индексы в существующих Range, либо очистить старый Highlight через CSS.highlights.clear() и создать новые диапазоны. Не заставляй браузер подсвечивать пустоту!
И еще: не пытайся стилизовать в ::highlight свойства, влияющие на геометрию (типа margin или width). Доступны только цвета, тени, украшения текста и фон. Это сделано ради той самой производительности, к которой мы так стремились.
🔥 Больше фишек, готовых сниппетов и передовых подходов к CSS мы публикуем в нашем Telegram-канале. Подписывайтесь, чтобы не пропустить!