02 — Position (static · relative · absolute · fixed · sticky)
한 줄 답:
position은 containing block과 흐름 참여 여부를 동시에 결정한다 —absolute의 부모는 DOM 부모가 아니라 가장 가까운 positioned 조상이고,sticky는 가장 가까운 스크롤 가능한 조상 안에서만 작동한다.
Why — 왜 position이 헷갈리나
5개 값(static/relative/absolute/fixed/sticky)이 모두 다른 두 축을 조작한다:
| 축 | 영향 |
|---|---|
| 흐름 참여 여부 | 자기 자리를 유지(static/relative/sticky) vs 흐름에서 빠짐(absolute/fixed) |
| containing block 기준 | 부모 / 가장 가까운 positioned 조상 / 뷰포트 / 스크롤 컨테이너 |
이 두 축이 각자 다른 규칙으로 결정되기 때문에 “왜 모달이 엉뚱한 곳에 떴지”, “왜 sticky가 갑자기 안 붙지” 같은 사고가 반복된다.
/* 한 줄 요약 */
.modal { position: absolute; top: 0; left: 0; }
/* → 어떤 부모를 기준으로 0,0? 답: "가장 가까운 positioned 조상" */How — 어떻게 동작하나
1) 5개 값의 동작 비교
2) Containing block 결정 규칙 (★ 핵심)
top/right/bottom/left/inset이 어떤 박스를 기준으로인지를 정하는 것 = containing block.
| position | Containing block은 누구 |
|---|---|
static, relative | 가장 가까운 block container 조상 (보통 DOM 부모) |
absolute | 가장 가까운 positioned 조상(static이 아닌 조상) — 없으면 initial containing block(≈ 뷰포트) |
fixed | 뷰포트 — 단, 조상에 transform/filter/perspective/will-change/contain: paint가 있으면 그 조상이 containing block이 된다 (★ 흔한 함정) |
sticky | 가장 가까운 scroll container (overflow가 auto/scroll/hidden인 조상) — 안에서 top/left/... offset 도달 시 viewport-relative처럼 동작 |
3) 흔한 함정 ① — transform 박힌 부모
.modal-root {
transform: translateZ(0); /* GPU 가속 위해 박았다 */
}
.modal {
position: fixed; /* 뷰포트 기준이 ... */
top: 50%; left: 50%;
}
/* → fixed인데 .modal-root 기준이 되어버린다!
CSS Transforms 명세: transform이 있는 박스는 후손 fixed의 containing block */2026 BP: 모달/툴팁 부모에는
transform/filter/will-change를 함부로 박지 말 것. 필요하면<dialog>+ Popover API(top layer)로 도망.
4) 흔한 함정 ② — sticky와 부모 overflow: hidden
.section { overflow: hidden; } /* 잘림 방지로 박았다 */
.section .nav { position: sticky; top: 0; }
/* → sticky는 가장 가까운 스크롤 가능 조상 안에서 작동.
.section이 overflow: hidden이면 그 안에서 스크롤이 발생하지 않으므로
sticky는 .section을 스크롤 컨테이너로 간주하지만 스크롤이 없어 안 붙음.
또는 .section 자체가 짧으면 sticky 영역이 너무 작아 거의 안 보임. */진단 체크리스트:
- 부모 체인을 거슬러 올라가며
overflow값을 확인. - 처음 만나는
overflow != visible박스가 스크롤 컨테이너. - 그 박스가 스크롤되는 박스인가? 아니면
hidden이라 스크롤 자체가 안 일어나나? - 그 박스 안에서 sticky 요소가 충분히 위에 있는가 (도달할 공간 있나)?
5) Stacking context — z-index의 진짜 규칙
z-index는 모든 곳에서 작동하지 않는다. stacking context 안에서만 비교된다.
stacking context를 만드는 트리거:
position: absolute/relative+z-index ≠ autoposition: fixed/sticky(값 무관)opacity < 1transform/filter/perspective/clip-path/mask(값 ≠ none)will-change: transform/opacity등isolation: isolate(★ 깔끔한 트리거)contain: layout/paint/strict- Root 요소
/* z-index 9999인데 안 보이는 이유 */
.parent { opacity: 0.99; } /* 새 stacking context 형성 */
.child { position: absolute; z-index: 9999; }
/* → 9999는 .parent의 stacking context 안에서만 의미.
.parent의 sibling보다 위로 못 올라감 */
/* 해결: */
.parent { isolation: isolate; } /* 새 context를 *명시적으로* 만들고,
동시에 그 안의 z-index를 외부에서 격리 */What — 구체 사양
5개 값 전수표
| 값 | 흐름 | Containing block | Offset (top/...) | 용도 |
|---|---|---|---|---|
static | 참여 | 부모 | 무시 | 기본값 — 보통 위치 |
relative | 참여 | 부모 | 자기 자리에서 이동 (시각만 이동, 흐름은 원래 자리) | 부분 이동, absolute 자식의 기준 만들기 |
absolute | 이탈 | 가장 가까운 positioned 조상 | 그 조상 기준 | 모달, 툴팁, 뱃지 |
fixed | 이탈 | 뷰포트 (transform 조상 있으면 그 조상) | 뷰포트 기준 | 헤더, 플로팅 버튼 |
sticky | 참여 | 스크롤 컨테이너 | threshold 도달 시 컨테이너 끝까지 고정 | 헤더, 사이드 ToC |
inset (논리축 단축 속성)
.modal {
position: absolute;
inset: 0; /* top: 0; right: 0; bottom: 0; left: 0; */
/* 또는 */
inset: 0 auto auto 0; /* top: 0; right: auto; bottom: auto; left: 0; */
}→ Logical version: inset-block, inset-inline (05-logical-properties 참고).
Sticky의 정확한 알고리즘
1. 스크롤 컨테이너 안에서 sticky 요소는 normal flow에 있다.
2. 컨테이너를 스크롤하면서 sticky의 `top: 10px` 같은 threshold에 도달.
3. 도달 시점부터 sticky 요소는 컨테이너의 *그 지점에 고정*된다 (시각적으로).
4. 컨테이너의 *끝*에 sticky 박스의 *원래 자리*가 닿으면 다시 같이 밀린다.
→ "containing block 안에서만 sticky"→ sticky는 컨테이너 내부의 fixed처럼 보이지만, 컨테이너를 벗어날 수 없다.
Top Layer (참고)
<dialog>.showModal(), [popover], fullscreen API는 DOM 트리와 무관한 top layer에 그려진다 → containing block, stacking context, transform 조상 모두 무관. 모달 사고의 근본 해결책.
<dialog id="modal">
<p>모달</p>
<button onclick="modal.close()">닫기</button>
</dialog>
<script>document.getElementById('modal').showModal();</script>
<!-- → transform 조상이 뭐가 있든, z-index가 뭐든 *항상 최상위* -->What-if — 잘못 쓰면
1) position: absolute인데 relative 부모를 안 만든 경우
.card { /* position 없음 */ }
.card .badge { position: absolute; top: 0; right: 0; }
/* → badge가 .card가 아니라 더 위 조상(또는 viewport)에 붙음 */
/* 해결 */
.card { position: relative; }→ “absolute는 relative와 짝” 이라는 1996년부터의 BP.
2) fixed 모달이 부모 transform 때문에 잘못 위치
body { transform: translateY(0); } /* 누가 박았는지 모르겠음 */
.modal { position: fixed; inset: 0; }
/* → modal이 body 기준이 됨. 뷰포트와 같으면 다행이지만 body가 좁으면 깨짐 */→ DevTools “Computed → Containing block”으로 추적.
→ 근본: <dialog>.showModal() 또는 [popover]로 top layer 도망.
3) Sticky가 부모 overflow: hidden에 죽음
.list { overflow: hidden; } /* 잘림 방지 의도 */
.list .header { position: sticky; top: 0; }
/* → .list가 스크롤 컨테이너이지만 hidden이라 스크롤 안 됨 → sticky 무효 */→ overflow: clip (sticky를 차단하지 않음, 2024+ baseline)으로 교체 or 스크롤 컨테이너 재설계.
.list { overflow: clip; } /* 2026 권장: hidden이지만 sticky/스크롤 영향 X */4) z-index 9999가 안 먹힘
.tooltip { position: absolute; z-index: 9999; }
/* → 부모 어딘가에 opacity < 1, transform, filter 등이 있어
stacking context가 끊겼다.
부모의 z-index가 0이면 9999는 그 안에서만 9999일 뿐. */→ DevTools “Layers” 탭으로 stacking context 트리 확인.
→ 부모에 isolation: isolate로 의식적으로 격리.
5) relative만 박고 offset 없이 끝
.x { position: relative; } /* offset 없음. 시각적으로 변화 없음 */→ 보통 “absolute 자식의 containing block 만들기” 목적. 이건 유효한 패턴이지만, 의도를 주석으로 남길 것.
.card {
position: relative; /* absolute .badge의 기준 */
}Insight — 흥미로운 이야기
position: sticky는 왜 늦게 표준이 됐나
2012년 Edward O’Connor(Apple)가 Safari에 iOS Safari 헤더처럼 붙는 동작을 위해 제안했다. 하지만 “sticky가 어떤 박스를 기준으로 작동해야 하는가” 가 격렬한 논쟁을 불렀다 — 부모? 가장 가까운 스크롤? 명시 지정?
결국 가장 가까운 스크롤 컨테이너로 결정됐는데, 이게 “왜 sticky가 작동 안 하지” 1위 사고의 원인이 됐다. 명세가 직관과 살짝 어긋난다.
2024년에 등장한 overflow: clip은 이 문제의 일부를 푼다 — 시각적으로 자르지만 스크롤 컨테이너를 만들지 않는 새 값.
position: absolute의 1996년 디자인
CSS1(1996)에는 position이 없었다. CSS2(1998)에서 전자책·인쇄 매체를 위해 absolute가 추가됐다 — “이 단어 옆에 각주를 띄워라” 같은 용도. 그래서 containing block이 가장 가까운 positioned 조상이라는 규칙은 책의 *주(註)·각주(脚註)*에서 유래한 아이디어다.
그게 25년 뒤에 모달·툴팁·드롭다운의 표준이 됐다. 그래서 헷갈린다 — 도구의 원래 용도가 지금 용도와 다르다.
Top Layer의 등장이 모든 걸 뒤집는다
2023년 Chrome 114부터 <dialog>와 [popover]가 top layer에 그려진다. 이는 “position: fixed로 모달 만들기” 라는 25년 묵은 패턴을 과거의 일로 만든다. transform 조상, z-index 경합, stacking context 사고 — 모두 발생할 수 없다.
2026년의 결정: 모달/툴팁/메뉴는
<dialog>또는[popover]+ Anchor Positioning (06-anchor-positioning)으로 만들 것.position: fixed는 헤더·플로팅버튼 같은 영구 고정 UI에만 남긴다.
요약 + 다이어그램
position은 흐름 참여와 containing block을 동시에 바꾼다. absolute는 가장 가까운 positioned 조상, fixed는 뷰포트(또는 transform 조상), sticky는 가장 가까운 스크롤 컨테이너가 기준이다. z-index는 stacking context 안에서만 비교되므로isolation: isolate로 명시적 격리하는 것이 모던 BP. 모달/툴팁의 미래는 top layer(<dialog>,[popover])와 anchor positioning이다.
다음 문서:
03-float-and-clear.md— Flexbox 이전 시대의 레이아웃 도구가 2026년에 어떤 본업으로 살아남았는가.