CSS 강좌 / z-index와 쌓임 맥락 이해하기
z-index는 요소의 겹침 순서를 조절하지만, 값이 크다고 페이지의 모든 요소 위로 올라가는 것은 아닙니다. 요소는 자신의 쌓임 맥락(stacking context) 안에서 순서를 비교하며, 다른 부모 맥락과의 앞뒤 관계는 자식 숫자보다 부모 맥락의 순서에 좌우됩니다.
겹침 순서와 위치를 구분하기
겹침 순서를 비교하려면 두 요소가 실제로 같은 화면 영역을 차지해야 합니다. 왼쪽과 오른쪽에 완전히 떨어져 있거나 위아래로 떨어져 있으면 z-index를 바꿔도 차이가 보이지 않습니다. 먼저 개발자 도구에서 요소의 사각형 위치를 확인하고, 겹치는 구역이 존재하는지부터 판단하세요.
position은 좌표 기준과 흐름 참여를 정하고 z-index는 겹칠 때의 앞뒤를 정합니다. top으로 좌표를 바꾸는 것과 z-index로 순서를 바꾸는 것은 다른 작업입니다. 아래 예제는 배지와 카드의 위치를 고정한 뒤 부모의 z-index만 바꾸므로 두 기능을 섞지 않고 비교할 수 있습니다.
z-index는 relative·absolute 같은 positioned 요소에 적용할 수 있으며 Flex 아이템과 Grid 아이템은 position: static이어도 사용할 수 있습니다. 보통의 일반 흐름 블록에 숫자만 지정하고 효과를 기대하면 적용 조건이 맞지 않을 수 있습니다. MDN의 z-index 설명에서 대상과 값의 의미를 확인할 수 있습니다.
쌓임 맥락은 부모 그룹 단위로 비교됨
쌓임 맥락은 내부 요소들의 앞뒤 순서를 하나의 그룹 안에서 계산하는 범위입니다. 부모 맥락은 바깥의 비교에서 하나의 단위처럼 취급됩니다. 왼쪽 부모가 1, 오른쪽 카드가 2인 같은 바깥 맥락에서 왼쪽 부모의 자식이 9999여도 그 자식은 부모 그룹 밖에서 오른쪽 카드의 2와 직접 경쟁하지 않습니다.
왼쪽 부모 맥락: z-index 1 └ 자식 배지: z-index 9999 오른쪽 카드 맥락: z-index 2 바깥에서는 부모 1과 카드 2를 비교합니다.
반대로 왼쪽 부모를 3으로 높이면 부모 그룹 전체가 오른쪽 카드 2보다 앞에 놓입니다. 자식 배지의 숫자는 그대로여도 겹치는 부분에서 배지가 보입니다. 내부 숫자와 외부 숫자를 더해 10000과 2를 비교하는 방식도 아닙니다. 실제 그룹 생성 조건과 중첩 관계는 MDN의 쌓임 맥락 안내에서 확인할 수 있습니다.
어떤 선언이 새 쌓임 맥락을 만드는가
| 대표 조건 | 새 맥락 생성 | 확인할 점 |
|---|---|---|
| relative 또는 absolute + z-index가 auto가 아님 | 생성 | 0도 숫자이며 auto와 다름 |
| fixed 또는 sticky | 생성 | z-index가 auto여도 맥락을 만듦 |
| Flex 또는 Grid 아이템 + non-auto z-index | 생성 | position 없이도 가능 |
| opacity가 1보다 작음 | 생성 | 보이는 정도뿐 아니라 그룹 관계에 영향 |
| transform이 none이 아님 | 생성 | 이동량이 0이어도 none과 다름 |
| isolation: isolate | 생성 | 의도적으로 별도 맥락을 만드는 방법 |
위 표는 자주 만나는 조건이며 모든 생성 조건의 목록은 아닙니다. 단순히 relative만 지정하고 z-index가 auto인 경우와 relative에 z-index: 0을 지정한 경우를 구분해야 합니다. 겹침이 달라졌다면 숫자뿐 아니라 조상에 새로 추가된 opacity나 transform도 확인하세요.
같은 맥락 안에서는 적용 조건이 맞는 요소의 z-index 숫자로 앞뒤를 비교할 수 있습니다. 다만 전체 CSS 그리기 순서는 음수 레벨, 일반 흐름, positioned 요소 등 여러 단계가 있으므로 모든 요소의 auto를 숫자 0과 동일하다고 취급하거나 숫자만으로 전체 페이지 순서를 계산하지 마세요. 학습 실습은 양수 숫자와 명시적인 부모 맥락으로 조건을 제한합니다.
전체 예제로 자식과 부모의 숫자 비교하기
아래 코드를 stacking-context.html로 저장해 엽니다. 배지는 위쪽 80~144px, 오른쪽 카드는 110~230px 영역에 놓여 세로로 34px 겹칩니다. 좌우 위치는 부모 너비에 대한 비율을 사용해 모바일에서도 겹침을 유지합니다. 부모와 아이템은 border-box로 계산하며 컨테이너 밖으로 불필요하게 넘치지 않도록 구성했습니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>stacking-context.html</title>
<style>
* { box-sizing: border-box; }
body { margin: 16px; font: 16px/1.6 sans-serif; color: #243447; }
h1 { font-size: 24px; } h2 { font-size: 20px; margin-top: 32px; }
a { color: #145f8f; } a:focus-visible { outline: 3px solid #b4470b; outline-offset: 3px; }
.scene { position: relative; width: min(100%, 520px); height: 300px; background: #f1f4f7; }
.left { position: absolute; z-index: 1; left: 0; top: 20px; width: 60%; height: 180px;
padding: 16px; background: #ffe3b5; }
.badge { position: absolute; z-index: 9999; left: 70%; top: 60px; width: 50%; height: 64px;
padding: 8px; background: #f5a7cc; }
.right { position: absolute; z-index: 2; left: 50%; top: 110px; width: 45%; height: 120px;
padding: 12px; background: #9cd4f2; }
.raised .left { z-index: 3; }
</style>
</head>
<body>
<h1>자식 숫자와 부모 숫자</h1><h2>부모 1, 오른쪽 카드 2</h2><div class="scene original"><div class="left">왼쪽 부모<div class="badge">자식 배지<br>9999</div></div><div class="right">오른쪽 카드<br>z-index: 2</div></div><h2>부모 3, 오른쪽 카드 2</h2><div class="scene raised"><div class="left">왼쪽 부모<div class="badge">자식 배지<br>9999</div></div><div class="right">오른쪽 카드<br>z-index: 2</div></div>
</body>
</html>
첫 구역에서는 겹치는 부분의 분홍 배지가 파란 카드에 가려지고, 둘째 구역에서는 분홍 배지가 파란 카드 위에 보입니다. 두 구역의 차이는 왼쪽 부모의 1과 3뿐입니다. 아래 화면은 동일한 예제의 두 상태를 비교한 것입니다.
- 첫 구역의 .badge 숫자만 99999로 높입니다. 부모 1과 카드 2의 관계가 유지되어 겹치는 부분은 계속 카드에 가립니다. 숫자를 9999로 복원합니다.
- 첫 구역에만 .original .left { z-index: 3; }을 추가합니다. 좌표를 바꾸지 않아도 배지가 카드 위로 올라옵니다. 확인 후 추가한 규칙을 제거합니다.
- 원래 첫 구역에서 .left의 z-index만 auto로 바꿉니다. 다른 생성 속성이 없는 이 예제에서는 왼쪽 부모가 별도 맥락을 만들지 않아 배지의 맥락이 바깥 카드와 비교됩니다. 배지가 앞에 보이는지 확인한 뒤 1로 복원합니다.
- 화면을 좁히거나 넓혀 배지와 카드가 계속 겹치는지 확인합니다. 좌표가 떨어진 상태에서는 숫자 변화가 눈에 보이지 않으므로 겹침 구역을 유지하면서 비교해야 합니다.
overflow로 잘린 내용은 별개의 문제
조상이 overflow: hidden으로 콘텐츠를 자르면 그 경계 밖의 부분은 높은 z-index만으로 다시 보이게 할 수 없습니다. 순서는 앞인데도 보이지 않는 경우에는 다른 요소에 덮였는지, 조상 경계에서 잘렸는지를 구분해야 합니다. 아래 코드는 stacking-clip.html로 저장해 실행합니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>stacking-clip.html</title>
<style>
* { box-sizing: border-box; }
body { margin: 16px; font: 16px/1.6 sans-serif; color: #243447; }
h1 { font-size: 24px; } h2 { font-size: 20px; margin-top: 32px; }
a { color: #145f8f; } a:focus-visible { outline: 3px solid #b4470b; outline-offset: 3px; }
.frame { position: relative; width: min(100%, 260px); height: 100px; overflow: hidden;
border: 2px solid #789; background: #e8f5de; }
.popup { position: absolute; z-index: 9999; left: 80%; top: 24px; width: 90px; height: 60px;
padding: 8px; background: #f5a7cc; }
</style>
</head>
<body>
<h1>숫자를 높여도 잘리는 상자</h1><div class="frame">부모 경계<div class="popup">배지<br>9999</div></div><p>배지의 오른쪽 부분은 부모의 경계 밖에 있습니다.</p>
</body>
</html>
- 배지의 오른쪽이 부모 경계에서 잘리는지 확인합니다. z-index만 99999로 바꿔도 잘린 부분은 나타나지 않습니다. 숫자를 9999로 복원합니다.
- .frame의 overflow만 visible로 바꿉니다. 다른 좌표와 숫자는 그대로인데 경계 밖의 부분이 보입니다. 비교 후 hidden으로 되돌립니다.
실제 화면에서는 overflow를 제거하면 스크롤과 둥근 모서리 등 다른 목적에 영향을 줄 수 있습니다. 먼저 잘림이 의도된 것인지 판단하고, 팝업이 경계 밖에 있어야 한다면 잘림 조상 밖으로 구조를 옮기거나 적절한 별도 표시 방식을 검토하세요. 메뉴가 가려진다는 이유만으로 모든 부모의 overflow를 일괄 변경하지 마세요.
가려진 요소를 확인하는 순서
- 요소가 실제로 존재하고 display·visibility·opacity 때문에 숨겨진 것은 아닌지 확인합니다.
- 두 요소의 사각형 위치를 확인하여 실제 겹침 구역이 있는지 확인합니다.
- 대상 요소와 조상을 따라 올라가 쌓임 맥락을 만드는 선언을 찾습니다. 경쟁하는 맥락이 어느 공통 부모에서 비교되는지 확인합니다.
- 바꿀 숫자는 자식인지 부모 맥락인지 판단합니다. 영향을 받는 형제 요소도 함께 확인하고 필요한 범위에서만 값을 조절합니다.
- 순서를 바꿔도 안 보이면 overflow·clip 같은 잘림과 다른 좌표 기준을 점검합니다. 비교를 끝내면 임시 수정 값을 원래대로 복원합니다.
일반 요소의 z-index는 top layer에 올라간 dialog 같은 별도 표시 계층을 숫자만 높여 넘는 수단으로 생각해서는 안 됩니다. 기본 카드와 배지의 문제를 해결할 때는 먼저 실제 부모 맥락과 잘림을 이해하는 것이 우선입니다. 숫자를 계속 키우는 대신 어떤 그룹끼리 비교되는지 확인하면 작은 값으로도 의도한 순서를 구성할 수 있습니다.











