CSS Anchor Positioning으로 JavaScript 없이 툴팁·드롭다운 위치 계산하기
getBoundingClientRect() 를 반복 호출하며 툴팁 위치를 계산해본 적 있으신가요? 저도 몇 년 전 온보딩 코치마크 UI를 만들면서 Floating UI(JS 기반 위치 계산 라이브러리)를 도입하고, requestAnimationFrame 루프로 좌표를 갱신하고, 스크롤할 때마다 위치가 튀는 버그를 잡느라 며칠을 보낸 기억이 있습니다. 그때 "이게 CSS에서 될 수 있으면 얼마나 좋을까" 싶었는데, 이제 정말 됩니다.
CSS Anchor Positioning은 한 요소를 다른 요소에 선언적으로 연결해서 위치를 결정하는 CSS 기능입니다. 좌표 계산은 브라우저 레이아웃 엔진이 맡고, 뷰포트 경계 처리도 CSS 한 줄로 끝납니다. 2024년 Chrome이 먼저 지원을 시작했고, 2026년 1월 Firefox 147, 같은 해 Safari 26까지 합류하면서 Baseline 2026에 도달했습니다. Baseline이란 모든 주요 브라우저가 해당 기능을 구현해 상호 운용성이 확보됐음을 의미하는 지표로, Can I Use 기준 전 세계 지원율은 약 76%입니다. 슬슬 프로덕션에서도 진지하게 고민해볼 시점이 됐습니다.
이 글에서는 CSS Anchor Positioning의 핵심 개념부터 실전 패턴, 폴리필 전략, 그리고 아직 JS가 필요한 예외 케이스까지 한 번에 정리합니다.
핵심 개념
기존 방식에서 위치 계산이 어려웠던 이유
먼저 기존 방식의 한계를 짚겠습니다. 툴팁이나 드롭다운을 만들 때 가장 골치 아팠던 건 overflow: hidden 이나 z-index 스태킹 컨텍스트 문제였을 겁니다. 스태킹 컨텍스트란 브라우저가 요소를 Z축으로 쌓는 순서를 결정하는 단위인데, transform 이나 opacity 같은 속성이 새 컨텍스트를 만들면 자식 요소가 부모 경계 밖으로 시각적으로 나올 수 없게 됩니다. 포지셔닝 요소를 부모 안에 두면 overflow 에 잘리고, 그렇다고 <body> 아래로 빼면 DOM 구조가 지저분해지고 스크롤 동기화를 별도로 처리해야 했죠.
CSS Anchor Positioning은 이 문제를 근본부터 다르게 접근합니다. 앵커 요소와 포지셔닝 요소가 DOM 트리 어디에 있든 관계없이 CSS 이름만으로 연결할 수 있고, 좌표 계산은 브라우저 레이아웃 엔진이 처리합니다.
동작에 필요한 세 가지 요소
앵커 포지셔닝이 동작하려면 세 가지가 반드시 갖춰져야 합니다.
/* 1. 앵커로 등록할 요소 */
.trigger {
anchor-name: --my-trigger; /* dashed ident 형태 필수 */
}
/* 2. 앵커에 연결되는 포지셔닝 요소 */
.tooltip {
position: absolute; /* 포지셔닝 컨텍스트 */
position-anchor: --my-trigger; /* 참조할 앵커 지정 */
position-area: top center; /* 앵커 위쪽 중앙에 배치 */
}- 연결(Association):
anchor-name으로 요소를 앵커로 등록하고position-anchor로 참조 - 포지셔닝 컨텍스트: 포지셔닝 요소에
position: absolute또는position: fixed지정 - 위치 지정(Location):
position-area또는anchor()함수로 앵커 기준 위치 결정
anchor-name값은 반드시--로 시작하는 dashed ident 형태여야 합니다. CSS 커스텀 프로퍼티와 같은 네이밍 규칙으로,--my-anchor,--dropdown-trigger같은 형태가 맞습니다.my-anchor처럼 작성하면 아무 에러 없이 그냥 동작하지 않아서 디버깅이 상당히 까다롭습니다.
핵심 속성 한눈에 보기
직접 써보면서 자주 찾게 되는 속성들만 모았습니다.
| 속성/함수 | 역할 |
|---|---|
anchor-name |
요소를 앵커로 등록 |
position-anchor |
참조할 앵커 지정 |
position-area |
9-cell 그리드 기반 위치 지정 |
anchor() |
앵커 특정 엣지를 inset 계산에 활용 |
anchor-size() |
앵커 너비·높이 기반으로 포지셔닝 요소 크기 설정 |
position-try-fallbacks |
뷰포트 초과 시 자동으로 시도할 위치 후보 목록 |
anchor-scope |
앵커 이름의 유효 범위를 하위 트리로 제한 |
anchor-scope 는 리스트 아이템마다 툴팁이 달리는 반복 컴포넌트에서 특히 중요합니다. 같은 anchor-name 을 여러 컴포넌트에 재사용할 때, anchor-scope 를 지정하지 않으면 페이지 전체에서 마지막에 선언된 앵커 하나만 참조하게 됩니다. 각 컴포넌트 루트에 anchor-scope: --my-tooltip 을 선언하면 해당 앵커 이름이 그 하위 트리 안에서만 유효해집니다.
position-area: 9칸 그리드로 위치 결정
position-area 는 앵커를 중심에 둔 3×3 그리드를 상상하면 이해하기 쉽습니다. 가로축과 세로축 키워드를 조합해서 원하는 셀을 지정합니다.
position-area: top center; /* 앵커 위쪽 중앙 */
position-area: bottom span-left; /* 앵커 아래쪽, 왼쪽으로 펼쳐짐 */
position-area: right span-all; /* 앵커 오른쪽, 위아래 전체 */span- 키워드는 인접한 셀까지 영역을 확장할 때 씁니다. inline-start, self-start 같은 논리적 키워드도 있는데, 이건 RTL(오른쪽에서 왼쪽으로 읽는) 레이아웃이나 다국어 쓰기 방향에 대응할 때 유용합니다. 아랍어나 히브리어처럼 쓰기 방향이 반대인 환경에서 left/right 대신 논리적 키워드를 쓰면 레이아웃이 자동으로 뒤집힙니다.
뷰포트 오버플로우 자동 처리
제가 개인적으로 가장 좋아하는 기능입니다. 예전엔 스크롤 이벤트마다 뷰포트 경계를 수동으로 계산해서 위치를 전환했는데, 이제는 position-try-fallbacks 에 후보 위치를 나열해두면 브라우저가 순서대로 시도해서 최적 위치를 자동으로 선택합니다.
.tooltip {
position: absolute;
position-anchor: --trigger;
position-area: top center;
position-try-fallbacks: bottom center, right span-top, left span-top;
/* 위쪽이 안 되면 아래쪽, 그것도 안 되면 오른쪽, 왼쪽 순서로 시도 */
}실전 적용
예시 1: 현재 바로 동작하는 :hover 툴팁
저도 처음엔 interestfor 어트리뷰트를 쓴 예시부터 보여주려 했는데, 대부분의 브라우저에서 아직 실험 단계라 복붙하면 동작하지 않습니다. 지금 당장 쓸 수 있는 버전부터 보여드리는 게 맞겠다 싶어서 순서를 바꿨습니다.
@supports 로 감싸면 앵커 포지셔닝을 지원하지 않는 브라우저에서는 툴팁이 단순히 숨겨지고, 지원하는 브라우저에서만 위치가 잡힙니다.
<div class="tooltip-wrapper">
<button class="trigger">도움말 보기</button>
<div class="tooltip" role="tooltip">파일을 드래그해서 업로드할 수 있습니다.</div>
</div>.trigger {
anchor-name: --btn-trigger;
}
.tooltip {
display: none; /* 기본값: 숨김 */
}
/* 앵커 포지셔닝 지원 브라우저에서만 위치 지정 */
@supports (anchor-name: --x) {
.tooltip {
display: block;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
position: absolute;
position-anchor: --btn-trigger;
position-area: top center;
position-try-fallbacks: bottom center, right span-top, left span-top;
background: #1a1a2e;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.875rem;
max-width: 200px;
}
.trigger:hover + .tooltip,
.trigger:focus-visible + .tooltip {
opacity: 1;
}
}| 코드 | 역할 |
|---|---|
@supports (anchor-name: --x) |
점진적 향상 — 미지원 브라우저에서는 툴팁 숨김 |
position-area: top center |
버튼 위쪽 중앙에 배치 |
position-try-fallbacks |
상단 공간 부족 시 순서대로 대체 위치 시도 |
.trigger:hover + .tooltip |
CSS 인접 선택자로 hover 상태 제어 |
interestfor어트리뷰트를 쓰면 hover/focus 트리거를 HTML에 선언적으로 처리할 수 있어 CSS도 더 깔끔해집니다. 다만 현재 Chrome Canary 수준의 실험적 기능이라 지금 당장 쓰기보다는 Interop 2026 진행 상황을 지켜보시는 것을 권장합니다.
예시 2: 드롭다운 메뉴 (버튼 너비에 자동 맞춤)
드롭다운 메뉴는 버튼과 같은 너비를 유지해야 자연스럽죠. anchor-size() 함수로 이것도 CSS에서 처리됩니다.
<div class="menu-wrapper">
<button class="menu-button" popovertarget="dropdown">메뉴 열기</button>
<ul id="dropdown" popover class="dropdown-panel">
<li>프로필 설정</li>
<li>알림 관리</li>
<li>로그아웃</li>
</ul>
</div>.menu-button {
anchor-name: --menu-trigger;
}
.dropdown-panel {
position: absolute;
position-anchor: --menu-trigger;
position-area: bottom span-right;
min-width: anchor-size(width); /* 버튼 너비 이상으로 자동 설정 */
position-try-fallbacks: top span-right; /* 아래 공간 부족 시 위로 */
}anchor-size(width) 는 앵커 요소의 현재 너비를 실시간으로 참조합니다. 반응형 레이아웃에서 버튼 너비가 바뀌어도 드롭다운이 자동으로 따라갑니다. 기존엔 JavaScript로 버튼 너비를 읽어서 드롭다운에 직접 설정해줬던 부분이죠.
예시 3: 온보딩 코치마크 (기존 200줄 JS를 CSS 몇 줄로)
솔직히 이 케이스를 처음 봤을 때 좀 충격이었습니다. 기존에 하이라이트 대상 요소의 좌표를 계산하고, 스크롤 위치를 반영하고, 화면 크기 변경에 대응하는 로직만 해도 100줄은 거뜬히 넘었는데요.
<button class="feature-button">새 기능</button>
<div class="coach-mark">이 버튼을 눌러보세요!</div>.feature-button {
anchor-name: --feature-highlight;
}
.coach-mark {
position: absolute;
position-anchor: --feature-highlight;
position-area: top center;
position-try-fallbacks: bottom center, right span-top, left span-top;
/* 말풍선 스타일링 */
background: #fff;
border: 2px solid #6366f1;
border-radius: 8px;
padding: 10px 14px;
max-width: 220px;
font-size: 0.875rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* 말풍선 꼬리 — 코치마크가 앵커 위에 올 때 아래쪽 화살표 */
.coach-mark::after {
content: "";
position: absolute;
top: 100%; /* 말풍선 아래쪽에 꼬리 */
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #6366f1;
}뷰포트 경계 처리, 스크롤 대응, 리사이즈 대응 모두 브라우저가 처리합니다. 말풍선 꼬리(::after) 정도만 직접 스타일링하면 됩니다. position-try-fallbacks 로 위아래가 전환될 때 꼬리 방향도 함께 바꾸고 싶다면 @position-try 규칙을 활용해볼 수 있습니다.
예시 4: 자동완성 드롭다운 (입력창 아래 딱 붙이기)
검색창이나 주소 입력 필드 아래에 제안 목록을 붙이는 패턴입니다.
.search-input {
anchor-name: --search-anchor;
width: 100%;
}
.suggestions-list {
position: absolute;
position-anchor: --search-anchor;
position-area: bottom span-all; /* 입력창 전체 너비에 맞춰 아래에 배치 */
max-height: 300px;
overflow-y: auto;
}span-all 키워드 덕분에 입력창 너비와 정확히 맞아떨어지는 제안 목록이 만들어집니다. 기존엔 JavaScript로 width 를 동기화해야 했던 부분이죠.
장단점 분석
장점
직접 써보면서 가장 크게 체감한 장점 순서로 정리했습니다.
| 항목 | 내용 |
|---|---|
| 성능 | 브라우저 레이아웃 엔진이 좌표 계산을 처리. requestAnimationFrame 루프와 getBoundingClientRect() 호출이 사라짐 |
| DOM 구조 자유도 | 앵커와 포지셔닝 요소가 DOM 어디에 있든 참조 가능. z-index/overflow 스태킹 컨텍스트 문제 해소 |
| 뷰포트 자동 대응 | position-try-fallbacks 선언만으로 브라우저가 최적 위치 자동 선택 |
| 번들 사이즈 절감 | Floating UI, Popper.js 등 JS 기반 위치 계산 라이브러리 의존성 제거 가능 |
| 코드 간결성 | 위치 관련 로직을 CSS에 집중시켜 유지보수성 향상 |
단점 및 주의사항
**점진적 향상(Progressive Enhancement)**이란, 모든 브라우저에서 기본 기능이 동작하게 만든 뒤, 최신 기능을 지원하는 환경에서 더 나은 경험을 제공하는 전략입니다.
@supports (anchor-name: --x) { }쿼리로 지원 여부를 확인하고 CSS를 분기하는 방식이 대표적으로, 예시 1의 코드처럼 폴백을 선언해두면 미지원 브라우저에서도 기본 기능은 동작합니다.
실무에서 직접 마주친 한계와 대응 방안을 솔직하게 정리했습니다.
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 레거시 브라우저 미지원 | 전 세계 지원율 약 76%, 오래된 브라우저 제외 | OddBird 폴리필 적용 또는 점진적 향상 전략 |
| 폴리필 한계 | position-area 래핑 엘리먼트 추가로 CSS 선택자 깨질 수 있음 |
~, +, >, :nth-child 선택자 의존 UI는 별도 확인 필요 |
| 외부 스타일시트 미파싱 | 폴리필이 import된 외부 CSS의 position-area를 파싱하지 못함 |
핵심 포지셔닝 CSS는 인라인 또는 메인 번들에 포함 |
| 복잡한 케이스 한계 | Shadow DOM 크로스 오리진, 가상화 리스트, 다단계 중첩 메뉴 | 해당 케이스는 Floating UI 등 JS 라이브러리 유지 권장 |
position-area 키워드 복잡도 |
span-all, self-start, inline-start 등 논리적 키워드 다수 |
MDN 레퍼런스와 브라우저 DevTools 활용 |
실무에서 가장 흔한 실수
anchor-name값을 dashed ident 없이 작성하는 경우 —my-anchor가 아니라--my-anchor형태여야 합니다. 이 규칙을 놓치면 아무 에러 없이 그냥 동작하지 않아서 디버깅이 상당히 까다롭습니다.- 포지셔닝 요소에
position: absolute또는position: fixed를 빠뜨리는 경우 —position-anchor와position-area는 포지셔닝 컨텍스트가 있어야 동작합니다. 없으면 역시 조용히 무시됩니다. - 폴리필 환경에서 인접 선택자가 이상하게 동작할 때 원인을 못 찾는 경우 — OddBird 폴리필이
position-area처리를 위해 래핑 엘리먼트를 삽입하는 부작용이 있습니다.~나:nth-child선택자가 예상과 다르게 동작한다면 폴리필 이슈를 먼저 의심해보시는 것이 좋습니다.
마치며
CSS Anchor Positioning은 오랫동안 JavaScript가 담당해온 위치 계산 로직을 브라우저 레이아웃 엔진으로 가져오는 구조적 변화입니다. 툴팁·드롭다운·팝오버처럼 "하나의 요소가 다른 요소 옆에 붙어야 하는" 패턴에서 JS 코드 양과 번들 사이즈가 동시에 줄어드는 경험을 할 수 있습니다.
아직 지원율이 76%이기 때문에 무조건 전환하기보다는, 새로 만드는 컴포넌트부터 점진적으로 도입해보시는 것을 권장합니다. 기존 프로젝트의 Floating UI나 Popper.js를 당장 걷어낼 필요는 없지만, 신규 개발 시 CSS Anchor Positioning으로 먼저 구현해보고 코드 절감 효과를 직접 느껴보시면 좋겠습니다.
지금 바로 시작해볼 수 있는 3단계:
- 브라우저 지원 확인부터 — Chrome 125+, Firefox 147+, Safari 26+ 중 하나를 쓰고 있다면 바로 시험해볼 수 있습니다. MDN의 CSS anchor positioning 가이드에 있는 라이브 데모부터 열어보시면 감이 빠르게 잡힙니다.
- 기존 툴팁 하나 골라서 포팅해보기 — 프로젝트에서 가장 단순한 툴팁 컴포넌트 하나를 선택해서
anchor-name+position-anchor+position-area세 가지만으로 재구현해보시기를 권장합니다. JavaScript 코드가 얼마나 줄어드는지 바로 체감됩니다. - 폴리필 준비 — 레거시 브라우저 대응이 필요하다면
@oddbird/css-anchor-positioning패키지를 설치하고@supports쿼리로 점진적 향상 구조를 잡아두시면 됩니다. 폴리필의 선택자 제약은 GitHub 이슈에서 최신 상태를 확인해두시는 것이 좋습니다.
참고 자료
- CSS anchor positioning | MDN Web Docs
- Anchor positioning | web.dev
- CSS Anchor Positioning Guide | CSS-Tricks
- Introducing the CSS anchor positioning API | Chrome for Developers
- CSS Anchor Positioning Module Level 1 | W3C
- First Public Working Draft: CSS Anchor Positioning Module Level 2 | W3C
- Anchor Positioning Updates for Fall 2025 | OddBird
- OddBird CSS Anchor Positioning Polyfill | GitHub
- CSS Anchor Positioning | Can I use
- Updates to Popover and CSS Anchor Positioning Polyfills | OddBird