CSS 앵커 포지셔닝 position-try-fallbacks — JavaScript 없이 드롭다운 위치를 4방향으로 자동 조정하는 방법
솔직히 말하면, 저도 꽤 오랫동안 드롭다운 위치 계산을 Floating UI나 Popper.js에 맡겼습니다. 뷰포트 가장자리에서 패널이 잘리는 문제를 getBoundingClientRect()로 읽고, requestAnimationFrame으로 스크롤마다 재조정하는 루프를 짜는 게 당연한 수순처럼 느껴졌거든요. 번들 크기 분석을 하다가 Floating UI 하나에 30KB 넘게 실리는 걸 보고서야 "이걸 꼭 JavaScript로 해야 하나?"라는 의문이 들었습니다.
CSS 앵커 포지셔닝의 position-try-fallbacks를 활용하면, 뷰포트 경계 감지부터 4방향 자동 반전, 드롭다운 토글까지 JavaScript 한 줄 없이 선언적으로 처리할 수 있습니다. 2026년 1월 Firefox 147이 합류하면서 Baseline 2026 지위를 얻었고, 91% 이상의 브라우저 트래픽에서 이미 네이티브로 동작합니다. 제가 직접 겪은 시행착오와 함께, 지금 당장 컴포넌트 하나부터 교체해볼 수 있는 패턴을 코드로 풀어봤습니다.
핵심 개념
CSS 앵커 포지셔닝의 세 가지 레이어
앵커 포지셔닝은 크게 세 단계로 구성됩니다. 처음 접했을 때 속성 이름들이 비슷비슷해서 저도 꽤 헷갈렸는데, 역할 중심으로 보면 이해하기 훨씬 수월합니다.
| 속성 | 역할 | 적용 대상 |
|---|---|---|
anchor-name |
기준점 요소에 식별자 부여 | 앵커(기준) 요소 |
position-anchor + position-area |
어느 앵커를, 어느 방향에 붙일지 선언 | 포지셔닝 대상 요소 |
position-try-fallbacks |
넘쳐날 때 브라우저가 순서대로 시도할 대체 위치 목록 | 포지셔닝 대상 요소 |
position-area는 그리드 기반의 방향 키워드 조합으로 배치 방향을 선언하는 고수준 추상화입니다. top, bottom, left, right 같은 기본 방향부터 span-inline, span-block 같은 조합까지 다양한 값이 있고, anchor() 함수를 직접 쓰는 것보다 훨씬 직관적이라 실무에서는 position-area를 먼저 손이 가게 됩니다.
position-try-fallbacks — 브라우저에게 "이 순서로 시도해봐"
대상 요소가 뷰포트나 지정된 컨테이너 밖으로 넘쳐날 때, 브라우저가 대체 위치를 순서대로 하나씩 시도하고 처음으로 넘치지 않는 값을 채택합니다. 값을 지정하는 방법은 두 가지입니다.
① 내장 <try-tactic> 키워드
.tooltip {
position: fixed;
position-anchor: --btn;
position-area: top;
/* 상단 → 하단 → 좌우 반전 → 상하+좌우 동시 반전 순으로 시도 */
position-try-fallbacks:
flip-block,
flip-inline,
flip-block flip-inline;
}| 키워드 | 동작 |
|---|---|
flip-block |
블록 축(상↔하) 물리적 반전 |
flip-inline |
인라인 축(좌↔우) 물리적 반전 |
flip-start |
쓰기 방향 기준 논리적 start↔end 반전 |
flip-block flip-inline |
공백으로 묶으면 동시에 반전 (사분면 전환) |
flip-start는 flip-block/flip-inline과 달리 물리적 방향이 아니라 쓰기 방향 기준의 "시작점"을 반전합니다. 아랍어처럼 RTL 언어에서는 인라인 start가 오른쪽이 되므로, 다국어 레이아웃을 다룰 때 유용합니다. RTL이 없는 프로젝트라면 flip-block/flip-inline 조합으로 대부분 해결됩니다.
RTL/LTR 자동 처리:
flip-block과flip-inline은 논리적 속성 기반입니다. 블록 방향은 텍스트가 줄 바꿈되는 방향, 인라인 방향은 텍스트가 흐르는 방향을 가리키며, 쓰기 모드에 따라 결정됩니다. 덕분에 아랍어 같은 RTL 레이아웃에서도 별도 처리 없이 올바르게 반전됩니다.
② @position-try 커스텀 블록
내장 키워드로 표현하기 어려운 복잡한 위치(크기 변경, 마진 조정 등)는 이름 있는 블록으로 등록할 수 있습니다.
@position-try --left-side {
position-area: left;
width: 200px;
margin-inline-end: 8px;
}
@position-try --right-side {
position-area: right;
width: 200px;
margin-inline-start: 8px;
}
.dropdown {
position-try-fallbacks: --left-side, --right-side, flip-block;
}position-try-order — 순서가 아닌 '여유 공간' 기준 선택
기본적으로 position-try-fallbacks는 목록 순서대로 시도합니다. 하지만 position-try-order를 함께 쓰면 "가장 여유 공간이 많은" 위치를 우선 선택하도록 알고리즘을 바꿀 수 있습니다.
.panel {
position-try-order: most-height; /* 수직 여유가 가장 많은 위치 우선 */
position-try-fallbacks: flip-block, flip-inline;
}주의:
most-height,most-width같은 값은 스크롤할 때마다 재계산될 수 있습니다. 복잡한 레이아웃에서 이 속성을 쓴다면 성능 프로파일링을 먼저 확인해보는 것을 권장합니다.
개념을 파악했으면, 실제 상황에서 어떻게 동작하는지 살펴볼 차례입니다. 제가 실무에서 자주 맞닥뜨린 네 가지 시나리오를 순서대로 풀어봤습니다.
실전 적용
예시 1: 뷰포트 어느 모서리에서든 잘리지 않는 드롭다운 패널
헤더 우측 끝에 있는 메뉴 버튼을 누르면 드롭다운이 오른쪽으로 벗어나고, 화면 하단 버튼은 아래로 잘리는 상황 — 다들 한 번쯤 겪어보셨을 겁니다. 예전엔 이걸 JS로 좌표 계산해서 클래스를 동적으로 바꿨는데, 이제는 이렇게 됩니다.
CSS-only 토글이 필요하다면 <details>/<summary> 조합이 가장 깔끔합니다. 브라우저가 열고 닫기를 담당하고, 앵커 포지셔닝은 위치만 책임집니다.
<details class="nav-menu">
<summary class="menu-trigger">메뉴 ▾</summary>
<div class="dropdown-panel">
<ul>
<li>프로필</li>
<li>설정</li>
<li>로그아웃</li>
</ul>
</div>
</details>.menu-trigger {
anchor-name: --nav-menu;
cursor: pointer;
list-style: none; /* details 기본 삼각형 제거 */
}
.dropdown-panel {
position: fixed;
position-anchor: --nav-menu;
position-area: bottom span-right; /* 기본: 버튼 아래, 오른쪽 정렬 */
position-try-fallbacks:
flip-block, /* ① 화면 하단 → 버튼 위로 올림 */
flip-inline, /* ② 화면 우측 → 왼쪽 정렬로 전환 */
flip-block flip-inline; /* ③ 하단+우측 모서리 → 왼쪽 위로 배치 */
width: max-content;
margin-block-start: 4px;
}| 시도 순서 | 조건 | 결과 |
|---|---|---|
기본 (bottom span-right) |
충분한 공간 | 버튼 아래, 오른쪽 정렬 |
flip-block |
하단 공간 부족 | 버튼 위로 이동 |
flip-inline |
우측 공간 부족 | 왼쪽 정렬로 전환 |
flip-block flip-inline |
하단+우측 모두 부족 | 버튼 왼쪽 위로 배치 |
참고:
<details>는 바깥 영역을 클릭해도 자동으로 닫히지 않습니다 (Light Dismiss 없음). 이 동작이 필요하다면 예시 2의 Popover API 패턴이 더 적합합니다.
저는 처음에 flip-block과 flip-inline 순서를 반대로 뒀다가, 모바일 세로 화면에서 좌우 반전이 먼저 시도되어 패널이 엉뚱하게 왼쪽에 붙는 걸 경험했습니다. 모바일 세로 화면에서는 하단 공간 부족이 좌우 공간 부족보다 훨씬 자주 발생하므로, flip-block을 앞에 두는 게 사용자 경험상 유리합니다.
예시 2: Popover API와 결합한 완전한 JavaScript-free 드롭다운
제가 올해 가장 많이 쓴 패턴입니다. HTML의 popover 속성이 토글·레이어·키보드를 담당하고, CSS 앵커 포지셔닝이 위치를 담당하는 역할 분리가 아주 깔끔합니다.
popovertarget과 id로 버튼과 팝오버를 연결하면, 브라우저가 버튼을 해당 팝오버의 암묵적 앵커로 자동 등록합니다. 이 덕분에 CSS에서 anchor-name이나 position-anchor를 직접 선언하지 않아도 위치 연산이 정상 동작합니다.
<button id="menu-btn" popovertarget="nav-dropdown">
메뉴 ▾
</button>
<ul id="nav-dropdown" popover>
<li>항목 1</li>
<li>항목 2</li>
<li>항목 3</li>
</ul>#nav-dropdown {
/* popovertarget ↔ id 연결만으로 앵커 참조 자동 생성 — anchor-name 불필요 */
position: fixed;
position-area: bottom span-right;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
margin: 0;
padding: 8px 0;
list-style: none;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: white;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}Popover API가 제공하는 것들:
- Esc 키로 닫기 — 브라우저 기본 동작
- 라이트 디스미스(Light Dismiss) — 바깥 클릭 시 자동 닫힘
- top-layer 스태킹 —
z-index전쟁 종료 - ARIA 역할 자동 관리 — 접근성 처리 내장
top-layer: 브라우저가 관리하는 특별한 렌더링 레이어로,
z-index스태킹 컨텍스트 위에 항상 올라옵니다.dialog,popover요소가 여기서 렌더링됩니다.
예시 3: overflow: hidden 컨테이너 탈출
스크롤 가능한 테이블이나 카드 리스트 안에 드롭다운을 넣으면 컨테이너 밖으로 잘리는 문제, 저도 꽤 오래 JS로 해결했습니다. 예전 방식은 getBoundingClientRect()로 좌표를 계산한 뒤 position: fixed에 직접 수치를 박는 것이었는데, 이제는 이렇게 됩니다.
/* 스크롤 컨테이너 */
.table-wrapper {
overflow: auto;
height: 400px;
}
/* 행 내부 액션 버튼 */
.row-action-btn {
anchor-name: --row-action;
}
/* 드롭다운 — position: fixed로 overflow 컨테이너를 벗어나 렌더링 */
.row-dropdown {
position: fixed;
position-anchor: --row-action;
position-area: bottom span-right;
position-try-fallbacks: flip-block, flip-inline;
/* 앵커가 뷰포트 밖으로 스크롤되면 자동 숨김 */
position-visibility: anchors-visible;
}
position-visibility: anchors-visible: 앵커 요소가 뷰포트 밖으로 스크롤되어 보이지 않게 되면, 대상 요소도 자동으로 숨겨집니다. 스크롤하다가 드롭다운이 허공에 떠있는 문제를 CSS 한 줄로 해결할 수 있습니다.
예시 4: 커스텀 다방향 툴팁
::after 가상 요소로 툴팁을 만들 때 주의할 점이 있습니다. 여러 요소가 같은 anchor-name 값을 공유하면 마지막으로 선언된 요소만 앵커로 참조됩니다. 즉 두 번째 이후 툴팁은 엉뚱한 위치에 붙습니다. 각 요소에 반드시 고유한 anchor-name을 부여하고, ::after의 position-anchor가 그 이름을 참조하도록 짝을 맞춰줘야 합니다.
@position-try --tooltip-left {
position-area: left;
margin-inline-end: 8px;
}
@position-try --tooltip-right {
position-area: right;
margin-inline-start: 8px;
}
/* 각 버튼에 고유한 anchor-name을 부여 */
.btn-save { anchor-name: --btn-save; }
.btn-delete { anchor-name: --btn-delete; }
/* 공통 툴팁 스타일 */
.btn-save::after,
.btn-delete::after {
content: attr(data-tooltip);
position: fixed;
position-area: top;
margin-block-end: 8px;
/* 위 → 왼쪽 → 오른쪽 → 아래 순서로 시도 */
position-try-fallbacks:
--tooltip-left,
--tooltip-right,
flip-block;
background: #1f2937;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.875rem;
white-space: nowrap;
pointer-events: none;
}
/* 각 ::after가 자신의 앵커만 참조하도록 */
.btn-save::after { position-anchor: --btn-save; }
.btn-delete::after { position-anchor: --btn-delete; }<button class="btn-save" data-tooltip="파일을 저장합니다">저장</button>
<button class="btn-delete" data-tooltip="되돌릴 수 없습니다">삭제</button>반복 컴포넌트의 경우: 리스트 아이템처럼 동적으로 렌더링되는 요소에는 JS로 인라인
style="anchor-name: --tooltip-N"을 부여하거나, Popover API의 암묵적 앵커 참조를 활용하는 것을 권장합니다.
장단점 분석
장점
- 성능: 브라우저 레이아웃 엔진이 처리하므로
requestAnimationFrame루프와getBoundingClientRect()리플로우가 없습니다. - 코드 간소화: Floating UI / Popper.js 의존성을 제거하면 번들 크기가 눈에 띄게 줄어듭니다.
- 접근성: Popover API와 결합하면 포커스 트랩, 키보드 닫기, ARIA 역할을 브라우저가 기본으로 제공합니다.
- 논리적 속성:
flip-block/flip-inline이 RTL/LTR 쓰기 방향을 자동으로 고려합니다. - 선언적: CSS만으로 배치 의도를 명시하므로 나중에 코드를 읽을 때 훨씬 파악하기 쉽습니다.
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 레거시 브라우저 | Baseline 2026이지만 프로젝트 지원 범위에 따라 폴리필 필요 | @supports (anchor-name: --x) { } 조건부 분기 + 폴리필 병행 |
| Shadow DOM 한계 | 경계를 넘는 앵커 참조가 현재 스펙상 불완전 | 컴포넌트 내부에 앵커와 대상 요소를 함께 배치 |
| 가상화 목록 | 앵커 요소가 DOM에서 언마운트되면 참조 깨짐 | JS 솔루션 병행 또는 position-visibility 활용 |
| 복잡한 중첩 메뉴 | 동적으로 로드되는 서브메뉴 등 고복잡도 케이스 | 여전히 JS가 유리한 영역 |
position-try-order 성능 |
most-height 등은 스크롤마다 재계산 가능 |
성능 프로파일링 후 필요 시에만 사용 |
| 구버전 속성명 | Chrome 125~128은 구 명칭 position-try-options 사용 |
Chrome 129+ 기준으로 position-try-fallbacks 사용 |
Baseline: W3C와 브라우저 벤더가 공동 운영하는 호환성 지표로, Chrome/Edge/Firefox/Safari 모두에서 안정적으로 지원되는 시점을 'Baseline'으로 정의합니다. CSS 앵커 포지셔닝은 2026년 1월 Firefox 합류로 Baseline 2026이 되었습니다.
실무에서 가장 흔한 실수
-
position: fixed없이 앵커 포지셔닝만 쓰는 경우 —overflow: hidden컨테이너를 탈출하려면 반드시position: fixed와 함께 써야 합니다.position: absolute로는 부모 컨테이너를 벗어나지 못합니다. -
position-try-fallbacks값 순서를 감으로 정하는 경우 — 브라우저는 목록을 순서대로 시도하므로, 가장 자주 발생하는 오버플로우 방향을 첫 번째에 두는 게 사용자 경험상 유리합니다. 모바일 세로 화면에서는flip-block이flip-inline보다 먼저 시도되어야 합니다. -
anchor-name값을 여러 요소가 공유하는 경우 — 같은anchor-name이 여러 요소에 선언되면 마지막으로 선언된 요소만 참조됩니다. 반복되는 컴포넌트에서는 각 인스턴스에 고유한 식별자가 필요합니다.
마치며
드롭다운 위치 조정만이 아니라, CSS 앵커 포지셔닝은 "이 요소가 저 요소 옆에 있어야 한다"는 관계를 CSS가 직접 표현하는 새로운 방식입니다. 지금까지 JavaScript에 의존했던 레이아웃 관계의 일부를 브라우저 레이아웃 엔진이 맡기 시작했다는 신호로 읽힙니다. Floating UI가 "필수"였던 영역이 점차 "선택"이 되는 흐름입니다.
지금 바로 시작해볼 수 있는 3단계:
-
브라우저 지원 여부 확인 —
@supports (anchor-name: --x) { }블록 안에 새 코드를 작성하면 구형 브라우저에서 안전하게 격리됩니다. Chrome DevTools Elements 패널에서 앵커 포지셔닝 관계선 시각화 기능(Chrome 141+)도 켜보시면 디버깅이 훨씬 수월합니다. -
가장 단순한 케이스부터 교체 — 프로젝트에서
getBoundingClientRect()+ 클래스 토글로 구현된 툴팁이 있다면, 위 예시 4의 패턴으로 먼저 교체해볼 수 있습니다. JavaScript 없이data-tooltip속성과 CSS만으로 완성됩니다. -
폴리필 전략 설정 — 구형 브라우저 지원이 필요하다면 OddBird의
css-anchor-positioning폴리필이나 Floating UI 공식 폴리필을@supports조건 없이 추가할 수 있습니다. 모던 브라우저에서는 네이티브 CSS가 동작하고, 구형에서만 JS 폴리필이 개입하는 방식입니다.
참고 자료
처음 시작한다면 아래 세 곳을 먼저 보시면 좋습니다: MDN(속성 레퍼런스), CSS-Tricks(패턴 예시), Ahmad Shadeed(시각적 설명).
- position-try-fallbacks — MDN Web Docs
- @position-try — MDN Web Docs
- Fallback options and conditional hiding for overflow — MDN
- CSS Anchor Positioning Guide — CSS-Tricks
- position-try-fallbacks — CSS-Tricks Almanac
- Anchor Positioning — web.dev Learn CSS
- Introducing the CSS Anchor Positioning API — Chrome for Developers
- Anchor Positioning Updates for Fall 2025 — OddBird
- The Basics of Anchor Positioning — Ahmad Shadeed
- CSS Anchor Positioning Module Level 1 — W3C
- Anchor position fallbacks with position-try-fallbacks — Fractaled Mind
- The convenience of CSS's new @position-try — DEV Community
- CSS Anchor Positioning: Replace Floating UI With CSS — Botmonster Tech
- Understanding CSS Anchor Positioning and Fallback Detection — JavaScript in Plain English