스크롤 상태 쿼리

Scroll-State Container Queries

요소가 sticky로 "붙었는지", 스냅으로 "고정됐는지" 같은 스크롤 상태를 CSS 컨테이너 쿼리로 직접 물어보는 기능.

다른 이름: @container scroll-state()stuck query
···
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="scroller" id="scroller">
    <div class="head" id="head">헤더 (스크롤하면 붙음)</div>
    <div class="filler">위 내용</div><div class="filler">위 내용</div><div class="filler">아래로 스크롤</div>
    <div class="filler">계속</div><div class="filler">계속</div><div class="filler">끝</div>
  </div>
</div>
css
.wrap{position:relative;width:min(260px,92%);height:100%;display:grid;place-items:center}
.scroller{width:100%;height:140px;overflow-y:auto;border:1px solid var(--line);border-radius:10px;container-type:scroll-state}
.head{position:sticky;top:0;padding:8px 10px;font-size:11px;font-weight:700;background:var(--surface);color:var(--fg);border-bottom:1px solid var(--line);transition:box-shadow .2s}
@container scroll-state(stuck: top){ .head{ box-shadow:0 4px 10px rgba(0,0,0,.16) } }
.head.fallback-stuck{box-shadow:0 4px 10px rgba(0,0,0,.16)}
.filler{padding:16px 10px;font-size:11px;color:var(--muted);border-bottom:1px dashed var(--line)}
.badge{position:absolute;top:0;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);z-index:2}
.badge.no{color:var(--accent-2)}
.badge svg{width:9px;height:9px}
js
const ok = CSS.supports('container-type', 'scroll-state');
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 ? '스크롤 상태 쿼리 지원됨' : '미지원 · IO 폴백';
const scroller = document.getElementById('scroller'), head = document.getElementById('head');
if (!ok) {
  const sentinel = document.createElement('div'); sentinel.style.height = '1px'; scroller.insertBefore(sentinel, head);
  new IntersectionObserver(([e]) => head.classList.toggle('fallback-stuck', !e.isIntersecting), { root: scroller, threshold: 1 }).observe(sentinel);
}
let dir = 1;
setInterval(() => { scroller.scrollTop += dir * 40; if (scroller.scrollTop <= 0 || scroller.scrollTop >= scroller.scrollHeight - scroller.clientHeight) dir *= -1; }, 900);

`position: sticky`인 헤더가 실제로 상단에 "붙은 순간"을 감지해서 그림자를 넣는 것 같은 연출은, CSS만으로는 붙었는지 여부를 알 방법이 없어서 항상 IntersectionObserver로 sentinel 엘리먼트를 관찰하는 JS가 필요했습니다.

`@container scroll-state(stuck: top)`은 그 상태를 컨테이너 쿼리 문법으로 직접 물어봅니다. 부모에 `container-type: scroll-state`를 주면, 자식이 스크롤 컨테이너 안에서 stuck·snapped·scrollable 같은 상태에 따라 스타일을 바꿀 수 있습니다. scroll-driven-animation이 "스크롤 진행률"을 다룬다면, 이건 "스크롤로 인한 이산적 상태 변화"를 다룹니다 — 서로 보완적입니다.

기본 지원은 caniuse/MDN baseline을 확인하세요. 미지원 브라우저에서는 지금처럼 IntersectionObserver로 sentinel 요소의 교차 여부를 감시해 클래스를 토글하는 것이 표준 대안입니다.

언제 쓰나

sticky 헤더가 붙었을 때 그림자를 넣거나, 스크롤 스냅 상태에 따라 버튼을 활성화할 때.