:has() 관계형 선택자

:has() Relational Selector

자식이나 형제의 상태를 보고 부모(또는 자기 자신)를 선택하는 CSS 선택자. "부모 선택자"라 불리기도 합니다.

다른 이름: Parent selectorCSS qualifier
···
html
<div class="wrap">
  <div class="badge" id="badge"><svg viewBox="0 0 10 10"><path id="bpath" d="M1.5 5.2l2.6 2.6L8.5 2.4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg><span id="btext">확인 중</span></div>
  <div class="rows">
    <label class="row"><input type="checkbox" checked><span>결제 알림</span></label>
    <label class="row"><input type="checkbox"><span>마케팅 수신</span></label>
    <label class="row"><input type="checkbox" checked><span>보안 경고</span></label>
  </div>
</div>
css
.wrap{position:relative;width:min(320px,92%)}
.rows{display:grid;gap:8px}
.row{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--line);border-radius:10px;background:var(--surface);font-size:13px;color:var(--muted);transition:background .2s,border-color .2s,color .2s}
.row:has(input:checked){border-color:var(--accent);color:var(--fg);background:color-mix(in srgb, var(--accent) 10%, var(--surface))}
.row.fallback-checked{border-color:var(--accent);color:var(--fg);background:color-mix(in srgb, var(--accent) 10%, var(--surface))}
.row input{accent-color:var(--accent)}
.badge{position:absolute;top:-30px;right:0;display:flex;align-items:center;gap:5px;padding:4px 9px;border-radius:999px;font-size:10px;font-weight:700;background:var(--bg);border:1px solid var(--line);color:var(--accent-3)}
.badge.no{color:var(--accent-2)}
.badge svg{width:9px;height:9px}
js
const ok = CSS.supports('selector(:has(a))');
const b=document.getElementById('badge'),p=document.getElementById('bpath'),t=document.getElementById('btext');
b.classList.toggle('no', !ok);
p.setAttribute('d', ok ? 'M1.5 5.2l2.6 2.6L8.5 2.4' : 'M2 2l6 6M8 2l-6 6');
t.textContent = ok ? ':has() 지원됨' : '미지원 · JS 폴백';
// :has() 미지원 브라우저용 폴백: 체크 상태가 바뀔 때 부모에 직접 클래스를 토글한다.
const rows = [...document.querySelectorAll('.row')];
function sync(row){ row.classList.toggle('fallback-checked', row.querySelector('input').checked); }
rows.forEach((row) => { sync(row); row.querySelector('input').addEventListener('change', () => sync(row)); });
let i = 0;
setInterval(() => { const inp = rows[i % rows.length].querySelector('input'); inp.checked = !inp.checked; inp.dispatchEvent(new Event('change')); i++; }, 1800);

:has() 이전에는 "이 안에 이런 자식이 있으면 부모 스타일을 바꾼다"를 CSS만으로 할 수 없었습니다. 체크박스가 체크된 행을 강조하거나, 이미지가 없는 카드에 다른 레이아웃을 주는 것 같은 작업은 전부 JS로 클래스를 붙이거나 떼는 수밖에 없었습니다. :has()는 이걸 순수 CSS 선택자로 옮깁니다 — `.row:has(input:checked)`, `.card:not(:has(img))` 처럼.

괄호 안에는 거의 모든 선택자가 들어갈 수 있어서 자식뿐 아니라 형제 관계(`.a:has(+ .b)`)도 표현할 수 있고, `:has()`가 폼 유효성 전달(`.field:has(:invalid)`)에도 자주 쓰입니다. 다만 조건이 많아지면 브라우저가 매 렌더마다 하위 트리를 다시 훑어야 해서 큰 리스트에서는 성능을 눈여겨봐야 합니다.

기본 브라우저 지원은 baseline 상태를 caniuse.com/mdn 에서 확인하세요. 지원 전 브라우저에서는 JS로 상태가 바뀔 때 부모에 직접 클래스를 토글하는 것이 유일한 대안이었고, 지금도 안전한 폴백으로 남아 있습니다.

언제 쓰나

폼 유효성 스타일링, "자식 유무에 따른" 레이아웃 분기, JS 없이 상태를 부모까지 전파하고 싶을 때.