부모 요소에 클래스를 붙이려고 JavaScript를 쓰던 코드를 :has()로 지운 과정
문제 발생
이미지가 있는 카드만 레이아웃을 다르게 하고 싶었는데, CSS로는 "자식에 img가 있는 .card"를 고를 수 없어서 렌더링 시점에 클래스를 계산해 붙이고 있었습니다.
// 카드마다 자식을 확인해서 클래스를 붙인다
document.querySelectorAll(".card").forEach((card) => {
if (card.querySelector("img")) card.classList.add("card--with-image");
});서버 렌더링과 클라이언트 렌더링에서 결과가 어긋나면 깜빡임이 생겼고, 조건이 늘 때마다 이 스크립트가 길어졌습니다.
원인 분석
CSS 선택자는 오른쪽 끝 요소를 대상으로 삼고 왼쪽으로 조건을 붙이는 구조라, .card img는 이미지를 고를 뿐 카드를 고르지 못했습니다. "부모 선택자"는 오래된 숙원이었고, 그래서 다들 JavaScript로 우회했습니다.
:has()는 이 방향을 뒤집습니다. 인자로 준 상대 선택자에 일치하는 요소가 하나라도 있으면 그 요소가 선택됩니다. 2023년 12월부터 모든 최신 브라우저에서 사용 가능(Baseline)합니다.
해결 방안
- 자식 존재 여부로 부모를 고릅니다. 위의 스크립트가 통째로 사라집니다.
.card:has(img) {
grid-template-columns: 200px 1fr;
}- 이전 형제도 고를 수 있습니다. 결합자를 인자 안에 넣으면 "뒤에 무엇이 오는가"로 앞 요소를 고를 수 있습니다.
/* 바로 뒤에 p가 오는 h1의 아래 여백만 없앤다 */
h1:has(+ p) { margin-bottom: 0; }- 폼 상태를 부모로 끌어올립니다. 유효성 표시를 위해 상태 클래스를 관리하던 코드가 대부분 여기서 정리됩니다.
.field:has(input:invalid) { border-color: var(--danger); }
.field:has(input:focus-visible) { outline: 2px solid var(--accent); }- 특이도를 계산에 넣습니다.
:has()는:is(),:not()과 같이 인자 중 가장 특이도가 높은 선택자의 특이도를 가져갑니다..card:has(#hero)는 id 특이도를 갖게 되므로, 나중에.card로 덮어쓰려다 실패할 수 있습니다. - 두 가지 제약을 기억합니다.
:has()안에:has()를 중첩할 수 없습니다.- 의사 요소(
::before등)는 인자로 쓸 수 없고, 의사 요소를 기준점으로 삼을 수도 없습니다. 조상 스타일에 따라 존재 여부가 달라지는 것들이라 순환 질의가 생기기 때문입니다.
- 미지원 브라우저를 신경 써야 한다면
:where()로 감쌉니다.:has()를 모르는 브라우저는 그 선택자 블록 전체를 버리는데,:is()/:where()같은 관대한 목록 안에 있으면 그 부분만 무시됩니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.