:has() Relational Selector

:has() 관계형 선택자

A CSS selector that matches a parent (or itself) based on the state of its descendants or siblings — often called the "parent selector".

Also known as: 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);

Before :has(), CSS had no way to say "restyle the parent because it contains this kind of child" — highlighting a row whose checkbox is checked, or giving image-less cards a different layout, meant toggling a class from JS. :has() moves that into a pure CSS selector, like `.row:has(input:checked)` or `.card:not(:has(img))`.

Almost any selector can go inside the parentheses, so it also expresses sibling relationships (`.a:has(+ .b)`) and is popular for form validation styling (`.field:has(:invalid)`). With many conditions, the browser has to re-scan subtrees on every render, so it's worth watching performance on large lists.

Check caniuse/MDN baseline for current support. In browsers without it, the only option is toggling a class on the parent from JS when child state changes — and that remains a solid fallback today.

When to use

Form validation styling, layout branches based on "does this contain X", or propagating state up to a parent without JS.