02 — Position (static · relative · absolute · fixed · sticky)

한 줄 답: positioncontaining 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.

positionContaining 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 영역이 너무 작아 거의 안 보임. */

진단 체크리스트:

  1. 부모 체인을 거슬러 올라가며 overflow 값을 확인.
  2. 처음 만나는 overflow != visible 박스가 스크롤 컨테이너.
  3. 그 박스가 스크롤되는 박스인가? 아니면 hidden이라 스크롤 자체가 안 일어나나?
  4. 그 박스 안에서 sticky 요소가 충분히 위에 있는가 (도달할 공간 있나)?

5) Stacking context — z-index의 진짜 규칙

z-index는 모든 곳에서 작동하지 않는다. stacking context 안에서만 비교된다.

stacking context를 만드는 트리거:

  • position: absolute/relative + z-index ≠ auto
  • position: fixed/sticky (값 무관)
  • opacity < 1
  • transform/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 blockOffset (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년에 어떤 본업으로 살아남았는가.