최근 CSS Custom Highlight를 위한 Universal Selector ::highlight(*)를 관련 CSS 표준에 추가하고(대부분 CSS Custom Highlights 표준이 아닌 CSS Pseudos 표준이 수정됨) Chromium에 구현했고, Chrome 158에서 배포된다.
이 글에서는 이 기능이 무엇인지, 왜 유용한지 소개한다.
CSS Custom Highlights란?
CSS Custom Highlights를 사용하면 텍스트를 별도의 요소로 감싸지 않고도 임의의 텍스트 범위에 스타일을 적용할 수 있다.
예를 들면,
// ::highlight(search) { background-color: yellow }
const ranges = matchOffsets.map(([start, end]) =>
new StaticRange({ startContainer: textNode, startOffset: start,
endContainer: textNode, endOffset: end }))
CSS.highlights.set("search", new Highlight(...ranges))
// DOM 변경 없음: 검색을 다시 실행하면 registry entry만 교체된다
span::highlight(awesome)이나 #foo::highlight(bar)처럼 특정 요소로 범위를 좁힐 수 있다.
Custom Highlights는 2025년 6월부터 Baseline newly available 상태이므로, 위 코드는 모든 브라우저에서 동작한다.
문제점
custom highlight의 이름을 매번 지정해야 한다.
만약 모든 highlight에 적용되는 전역 규칙을 만들고 싶을 때는 어떻게 해야 할까?
마땅한 방법이 현재까진 없다.
예시,
::highlight(spelling) {
text-decoration-color: yellow;
text-decoration-line: underline; /* 중복! */
text-decoration-style: wavy; /* 중복! */
}
/* ... */
::highlight(punctuation) {
text-decoration-color: red;
text-decoration-line: underline; /* 중복! */
text-decoration-style: wavy; /* 중복! */
}
중복된 스타일이 20개가 넘는다면 어떨까? 작업이 번거로워지고, 공통 스타일을 바꿀 때마다 여러 곳을 고쳐야 한다.
::highlight를 위한 Universal Selector 도입
이 문제를 해결하기 위해 CSSWG는 이름이 지정된 모든 highlight에 매칭되는 * 인자를 추가하기로 결정했다.
2026년 9월 30일, 내가 올린 Spec PR이 머지됐다.
이제 위 예시는 아래처럼 중복을 제거할 수 있다.
::highlight(*) {
text-decoration-line: underline;
text-decoration-style: wavy;
}
::highlight(spelling) {
text-decoration-color: yellow;
}
::highlight(punctuation) {
text-decoration-color: red;
}
결과는 앞의 중복된 버전과 같다. 이름이 지정된 모든 custom highlight(spelling, punctuation)가 universal(*) 규칙의 스타일을 사용하기 때문이다.
::highlight(*)의 specificity는 0이다
CSS Selectors에는 각 규칙의 “가중치”를 계산해 어떤 selector가 우선하는지 결정하는 specificity 알고리즘이 있는데, 간단하게 말해서 두 규칙이 같은 속성을 지정하면 cascade는 specificity를 기준으로 어느 쪽이 이길지 결정한다. 자세한 내용은 MDN 문서를 참고.
현재 널리 쓰이는 highlight(custom ident를 쓰는 경우)는 태그 이름과 같은 specificity를 가진다. 하지만 *를 쓰는 highlight는 specificity가 0이 되도록 바꿨다.
구현 사이드에서는 인자가 *일 때 명시적으로 0을 반환한다:
inline unsigned CSSSelector::SpecificityForOneSelector() const {
// ...
case kPseudoHighlight:
if (Argument() == UniversalSelectorAtom()) {
return 0;
}
[[fallthrough]];
0은 가장 낮은 specificity이므로 이름이 있는 custom highlight가 항상 이긴다. (단, p::highlight(*)는 p의 specificity가 더해져 ::highlight(foo)와 동등해진다.)
예를 들면,
::highlight(*) {
color: red; /* 패배... */
background-color: blue;
}
::highlight(foo) {
color: green; /* 승리! */
}
따라서 foo에는 최종적으로 color: green과 background-color: blue가 적용된다.
fallback
::highlight(*)를 쓰면 등록된 모든 highlight가 universal 규칙을 기본 스타일처럼 사용한다.
예를 들면:
<style>
::highlight(*) {
color: red;
}
</style>
<p id="target">Hello, highlights!</p>
<script>
const range = new Range();
range.selectNodeContents(document.getElementById("target"));
CSS.highlights.set("bar", new Highlight(range));
</script>
등록된 custom highlight bar는 universal 규칙을 따르므로 color: red로 그려진다.
API에 대한 더 자세한 내용은 MDN을 참고(아직 업데이트 안 돼 있을 수 있음): CSS Custom Highlight API
2026년 10월 20일, Intent to Ship에서 Blink API owner 3명의 LGTM을 받았고 배포 CL을 머지했다. 이 기능은 Chrome 158에 포함되어 2026년 11월 4일에 출시될 예정이다.
자세한 내용은 Chrome Platform Status 항목, 구현 CL, DevTools 지원 CL에서 확인할 수 있다.
마치며
Spec 수정부터 기능 출시까지 직접 작업할 수 있어서 정말 기쁘다!
Spec PR을 리뷰해 준 fantasai, Chromium 패치를 리뷰해 준 Rune, 그리고 Igalia 동료 Stephen에게 감사한다. 이 작업은 Bloomberg의 후원으로 진행됐다.