z-index를 올려도 위로 오지 않는 문제
드롭다운이나 모달에 z-index: 9999를 줘도 다른 요소 아래에 그려질 때가 있습니다. 원인이 쌓임 맥락이라면 z-index 값을 더 키워도 해결되지 않습니다.
브라우저는 같은 쌓임 맥락(stacking context) 안의 요소끼리만 z-index 값을 비교하기 때문입니다. 이 글에서는 이 현상을 렌더링 결과로 재현하고, 쌓임 맥락이 생기는 조건과 해결 방법을 정리합니다.
같은 쌓임 맥락 안의 z-index 비교
먼저 z-index가 기대대로 동작하는 경우입니다. 회색 상자와 빨강 상자를 겹쳐 두고, 회색 상자에만 z-index: 10을 줬습니다.
<div class="stage">
<div class="wrap-gray">
<div class="box gray">회색 (z-index: 10)</div>
</div>
<div class="wrap-red">
<div class="box red">빨강 (z-index: auto)</div>
</div>
</div>.stage {
position: relative;
}
.box {
position: absolute;
}
.gray {
z-index: 10;
}
/* .red에는 z-index를 주지 않았다(auto) */
"회색 상자(z-index 10)가 빨강 상자 위에 그려진 화면"
z-index가 큰 회색 상자가 빨강 상자 위에 그려집니다. 두 상자가 같은 쌓임 맥락에 있어서 브라우저가 z-index 값을 바로 비교하기 때문입니다.
부모에 opacity를 준 경우
이번에는 회색 상자를 감싼 부모(.wrap-gray)에 opacity: 0.6만 추가합니다. 회색 상자 자체와 z-index는 그대로 둡니다.
.wrap-gray {
opacity: 0.6; /* 추가한 속성은 이것 하나다 */
}
"부모에 opacity 0.6을 주자 회색 상자가 흐려지고 빨강 상자 아래로 내려간 화면"
z-index: 10인 회색 상자가 z-index를 주지 않은 빨강 상자 아래로 내려갔습니다. 부모의 opacity 때문에 회색 상자는 반투명하게 흐려졌습니다.
opacity: 0.6을 준 부모가 새 쌓임 맥락의 루트가 됐기 때문입니다. 이제 회색 상자의 z-index: 10은 부모가 만든 맥락 안에서만 비교되고, 바깥의 빨강 상자와는 비교되지 않습니다. 도입부에서 z-index를 키워도 해결되지 않는다고 한 이유가 이것입니다.
쌓임 맥락과 z-index 비교 규칙
쌓임 맥락은 요소를 z축(화면에서 사용자 쪽으로 나오는 방향)으로 쌓을 때 쓰는 독립된 그룹입니다. 페이지에는 루트 요소(<html>)가 만드는 최상위 맥락이 하나 있고, 특정 조건을 만족하는 요소가 그 안에 자식 맥락을 새로 만듭니다.
브라우저는 아래 두 규칙으로 z-index를 비교합니다.
z-index는 같은 쌓임 맥락에 속한 형제끼리만 비교된다.- 서로 다른 맥락에 속한 요소의 순서는 각 맥락 루트가 상위 맥락에서 쌓이는 순서로 정해진다. 자식의
z-index가 아무리 커도 맥락 밖의 요소와는 비교되지 않는다.
앞의 예제에 이 규칙을 적용하면 이렇습니다. 회색 상자의 z-index: 10은 부모 맥락 안에서만 유효합니다. 부모(.wrap-gray)와 빨강 상자 쪽(.wrap-red)은 둘 다 z-index가 없으므로, 상위 맥락에서는 DOM 순서로 쌓입니다. 그래서 DOM에서 나중에 나오는 빨강 상자가 위에 그려집니다.
쌓임 맥락을 만드는 속성
opacity처럼 z-index와 관련 없어 보이는 속성도 쌓임 맥락을 만듭니다. 자주 만나는 조건은 다음과 같습니다.
- 루트 요소
<html>(기본으로 존재하는 최상위 맥락) position: relative또는absolute이면서z-index가auto가 아닐 때position: fixed또는sticky(이 둘은z-index없이도 항상 만든다)opacity가 1보다 작을 때transform,filter,backdrop-filter,perspective,clip-path,mask가none이 아닐 때mix-blend-mode가normal이 아닐 때isolation: isolatewill-change에 위 속성 중 하나를 지정했을 때contain: layout,paint,strict,contentcontainer-type이size또는inline-size일 때 (컨테이너 쿼리)- flex/grid 컨테이너의 자식이면서
z-index가auto가 아닐 때
이 중 transform, opacity, filter는 애니메이션, 페이드, 그림자 효과에 흔히 쓰는 속성입니다. 그래서 z-index만 바꿨는데 쌓이는 순서가 기대와 다르다면, 조상 요소 중 하나에 이런 속성이 있는 경우가 많습니다.
isolation: isolate로 쌓임 맥락 만들기
쌓임 맥락은 필요할 때 직접 만들 수도 있습니다. 이때 쓰는 속성이 isolation: isolate입니다. 앞 예제의 opacity: 0.6 대신 부모에 isolation: isolate를 줘 보겠습니다.
.wrap-gray {
isolation: isolate; /* 시각 효과는 바꾸지 않고 쌓임 맥락만 만든다 */
}
"부모에 isolation: isolate를 주자 회색 상자가 흐려지지 않은 채 빨강 상자 아래로 내려간 화면"
쌓이는 순서는 opacity 예제와 같습니다. 회색 상자의 z-index: 10이 부모 맥락 안에서만 비교되므로 빨강 상자가 위에 그려집니다. 다만 이번에는 회색 상자가 흐려지지 않습니다. isolation: isolate는 쌓임 맥락만 새로 만들고 색이나 투명도는 바꾸지 않기 때문입니다.
이 속성은 컴포넌트 안의 z-index가 컴포넌트 밖의 요소와 비교되지 않게 할 때 씁니다. 컴포넌트 루트에 isolation: isolate를 주면, 안쪽 요소의 z-index는 그 컴포넌트 안에서만 비교됩니다.
opacity: 0.999나 transform: translateZ(0)으로 쌓임 맥락을 만드는 코드도 있습니다. 이런 코드는 쌓임 맥락을 만들려는 의도가 드러나지 않습니다. isolation: isolate는 속성 이름에 그 의도가 드러나고, 다른 시각 효과를 바꾸지 않습니다.
원인이 되는 조상 요소 찾기
요소가 기대한 순서로 쌓이지 않으면 아래 단계를 따라 원인을 찾습니다.
- 문제의 요소에서 부모 방향으로 올라가며, 앞 목록의 속성(
opacity,transform,filter,position: fixed등)을 가진 조상을 찾는다. - 그 조상이 새 쌓임 맥락을 만든 요소다. 문제가 되는 요소의
z-index는 그 맥락 안에서만 비교된다. - 요소를 그 조상 밖으로 옮겨(예:
portal로body아래에 렌더링) 비교할 요소와 같은 맥락에 두거나, 맥락을 만든 조상의 쌓임 순서를 올린다.
브라우저 개발자 도구에서 조상 요소를 선택하고 Computed 탭에서 opacity, transform 같은 속성을 확인하면 원인이 되는 요소를 빨리 찾을 수 있습니다. z-index 값을 키우기 전에 요소가 어느 쌓임 맥락에 속하는지부터 확인하는 것이 좋습니다.
정리
| 상황 | 결과 | 대응 |
|---|---|---|
| 같은 맥락 안의 요소 | z-index 값을 그대로 비교한다 | 값만 조정한다 |
| 조상이 새 맥락을 만든 요소 | z-index가 그 맥락 안에서만 비교된다 | 요소를 조상 밖으로 옮기거나 조상의 순서를 올린다 |
안쪽 z-index를 바깥과 분리할 컴포넌트 | 컴포넌트 루트에 새 맥락이 필요하다 | 루트에 isolation: isolate를 준다 |
z-index는 같은 쌓임 맥락 안의 형제끼리만 비교된다.opacity < 1,transform,filter,position: fixed/sticky같은 흔한 속성도 새 쌓임 맥락을 만든다.- 자식의
z-index는 맥락 밖의 요소와 비교되지 않는다. 그래서9999를 줘도 위로 오지 않을 수 있다. - 쌓임 맥락을 직접 만들 때는 다른 시각 효과가 없는
isolation: isolate를 쓴다.
마치며
z-index가 적용되지 않을 때 값을 키워서 해결되는 경우는 드뭅니다. 요소가 어느 쌓임 맥락에 속하는지 먼저 확인하면, 모달이 사이드바 뒤로 가거나 드롭다운이 헤더에 가려지는 문제의 원인을 빨리 찾을 수 있습니다. 긴 글 읽어주셔서 감사합니다.