CSS — 나중에 쓴 규칙이 이긴다 · sticky 라고 썼는데 안 붙는다 · 이론
원인은 언제나 조상에 있다
한 줄 요약
fixed 가 화면에 안 붙고 sticky 가 안 따라다닐 때, 원인은 그 요소가 아니라 조상에 있다. 담음 블록과 스크롤 상자를 누가 만들었는지 찾으면 답이 나온다.
왜 이게 필요했나
이 두 버그는 증상이 원인을 전혀 가리키지 않는다.
- 모달 덮개에
position: fixed; inset: 0을 줬는데 화면을 다 덮지 않고 카드 안에만 깔린다 - 목차에
position: sticky; top: 64px을 줬는데 스크롤하면 그냥 올라가 버린다
둘 다 그 요소의 CSS 는 완벽하게 맞다. 개발자 도구로 그 요소만 보면 아무 문제가 없다. 그래서 사람들은 값을 바꿔 가며 몇 시간을 쓴다. 볼 곳은 조상이다.
어떻게 동작하나 — 담음 블록
top, left, width: 50% 같은 값은 전부 담음 블록(containing block) 을 기준으로 잰다. 그 담음 블록이 무엇인지는 position 값이 정한다. [MDN 의 담음 블록 문서](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Containing_block)가 규칙을 정리한다.
| position | 담음 블록 |
|---|---|
| static · relative | 가장 가까운 블록 조상의 content 영역 |
| absolute | 가장 가까운 위치 지정된(static 이 아닌) 조상의 padding 영역 |
| fixed | 뷰포트 |
| sticky | 흐름 기준은 부모, 붙는 기준은 가장 가까운 스크롤 상자 |
여기까지는 대부분 안다. 문제는 그다음이다.
fixed 를 가두는 속성들
조상 중 어느 하나라도 아래 속성을 가지면, 그 조상이 fixed 와 absolute 의 새 담음 블록이 된다.
transform (none 이 아닌 값)filterbackdrop-filterperspectivewill-change: transform | filter | perspectivecontain: layout | paint | strict | content그래서 이런 일이 난다. 카드에 마우스를 올리면 살짝 떠오르는 효과를 주려고 transform: translateY(-2px) 을 넣는다. 또는 성능을 위해 will-change: transform 을 넣는다. 그 순간 카드 안에 있던 모달 덮개가 카드 안에 갇힌다. 애니메이션과 모달은 아무 관계가 없어 보이므로 원인을 찾기까지 오래 걸린다.
[CSS Positioned Layout 3](https://www.w3.org/TR/css-position-3/)과 [CSS Containment 3](https://www.w3.org/TR/css-contain-3/)이 각각의 규정을 담고 있다.
sticky 가 안 붙는 두 가지 이유
position: sticky 는 두 조건이 모두 맞아야 동작한다.
첫째, 임계값이 있어야 한다. top, right, bottom, left 중 하나 이상이 auto 가 아니어야 한다. 하나도 없으면 붙을 선이 없으므로 그냥 흘러간다. 오류도 경고도 없다.
둘째, 스크롤 상자가 실제로 스크롤되어야 한다. sticky 는 가장 가까운 스크롤 상자 안에서만 붙는다. 스크롤 상자는 overflow 가 visible 이 아닌 가장 가까운 조상이다.
여기서 overflow: hidden 이 사고를 낸다. 가로 스크롤을 막으려고 중간 컨테이너에 overflow-x: hidden 을 넣으면, overflow-y 도 함께 auto 가 되어 그 상자가 스크롤 상자가 된다. 사용자는 그 상자를 스크롤하지 않으므로(페이지를 스크롤한다) sticky 가 붙을 일이 영영 생기지 않는다.
그리고 sticky 의 붙는 범위는 부모 상자 안이다. 부모가 자기 높이만큼만 있으면 그만큼만 따라온다. "조금 따라오다 만다" 는 증상은 거의 항상 이것이다.
z-index 는 어느 상자 안에서 겨루는가
z-index: 9999 를 줬는데도 뒤에 깔리는 일이 있다. 숫자가 작아서가 아니라 다른 쌓임 맥락 안에 있어서다.
쌓임 맥락은 이런 것들이 만든다. [MDN 의 쌓임 맥락 문서](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context)가 전체 목록을 준다.
position이 static 이 아니면서z-index가auto가 아닌 요소position: fixed와position: sticky(z-index 와 무관하게 언제나)opacity가 1 미만transform,filter,perspective,isolation: isolate,contain: paint
조상이 쌓임 맥락을 만들면 그 안의 z-index 는 그 맥락 안에서만 통한다. 부모가 z-index: 1 이고 형제가 z-index: 2 면, 부모 안의 자식이 9999 여도 그 형제 아래다. opacity: 0.99 같은 무심한 한 줄이 이것을 만든다.
현장에서 만나는 모습
세 가지가 반복해서 나온다.
첫째, 드롭다운이 부모에 잘린다. 조상에 overflow: hidden 이 있고 드롭다운이 absolute 다. fixed 로 바꾸면 되는데, 그 조상에 transform 이 있으면 fixed 도 갇힌다. 근본 해결은 메뉴를 문서 최상단으로 옮기는 것(포털)이다.
둘째, 모바일에서만 헤더가 안 붙는다. 좁은 화면에서 가로 넘침을 막으려고 overflow-x: hidden 을 넣은 미디어 쿼리가 있었던 것이다.
셋째, iOS 에서만 이상하다. 이건 대개 -webkit-overflow-scrolling 이나 안전 영역(env(safe-area-inset-*)) 문제인데, 여기서 다루는 규칙과는 다른 축이라 실제 기기에서 확인해야 한다. 추측으로 고치지 않는 편이 낫다.
진단 순서를 외워 두면 대부분 5분 안에 끝난다.
> 1. 이 요소의 position 이 무엇인가
> 2. 담음 블록이 무엇인가 — 조상을 따라 올라가며 transform 계열을 찾는다
> 3. sticky 라면 임계값이 있는가, 스크롤 상자가 어느 것인가
> 4. z-index 라면 조상 중 누가 쌓임 맥락을 만들었는가
다음 실습에서 할 것
정해진 문서 구조에 스타일시트를 쓰고, 담음 블록과 스크롤 상자를 계산기로 직접 찾는다. 카드에 transform 을 넣으면 덮개가 어떻게 갇히는지, 조상의 overflow: hidden 이 sticky 를 어떻게 죽이는지를 눈으로 확인한다.