Back

CSS Anchor Positioning: JavaScript 툴팁 라이브러리의 종말 (완전 가이드)

프론트엔드 개발자라면 한 번쯤은 겪어봤을 거예요 — 툴팁 위치 잡다가 멘탈 탈탈 털리는 경험. 버튼에 달린 드롭다운, 트리거 따라다니는 툴팁, 특정 요소에 찰싹 붙어야 하는 팝오버. 해결법은 늘 같았어요. JavaScript 라이브러리 깔고, 스크롤/리사이즈마다 좌표 계산하고, 오버플로 감지 처리하고, z-index가 안 깨지길 기도하기.

Floating UI(Popper.js 후속)의 코어 패키지만 주간 npm 다운로드 2300만 건이 넘습니다. CSS가 네이티브로 못 하는 걸 보완하려고 수천만 프로젝트가 라이브러리에 의존하고 있었던 거죠. 근데 이제 그 시대가 끝났어요.

CSS Anchor Positioning API가 Baseline 2026으로 Chrome 125+, Firefox 147+, Safari 26 전부 지원합니다. 플래그 뒤에 숨은 실험 기능 아닙니다. 프로덕션 바로 투입 가능하고, 웹에서 위치 지정 UI를 만드는 방식 자체를 뒤집어놓는 스펙이에요.

이 글에서는 핵심 API, 고급 폴백 전략, Popover API 연동, JS 라이브러리에서의 마이그레이션 패턴, 그리고 모르면 반드시 물리는 엣지 케이스까지 몽땅 파헤쳐봅니다.


CSS Anchor Positioning이 해결하는 문제

이 API가 없던 시절엔, 다른 요소 기준으로 뭔가를 배치하려면 선택지가 딱 세 가지였습니다:

1. Absolute Positioning + 수동 오프셋 — 잘 깨지고, 스크롤하면 틀어지고, 오버플로? 꿈도 꾸지 마세요.

/* 클래식 꼼수 */ .tooltip-wrapper { position: relative; } .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); }

정적 레이아웃에선 돌아가죠. 근데 트리거가 뷰포트 끝자락에 있거나, 스크롤하거나, 컨테이너에 overflow: hidden이 걸려있으면? 바로 터집니다.

2. JavaScript 위치 계산 — 10년간의 업계 표준이었죠.

// Floating UI가 내부적으로 하는 일 (단순화) function updatePosition(anchor, floating) { const anchorRect = anchor.getBoundingClientRect(); const floatingRect = floating.getBoundingClientRect(); let top = anchorRect.bottom + 8; let left = anchorRect.left + (anchorRect.width - floatingRect.width) / 2; // 오버플로 감지 if (top + floatingRect.height > window.innerHeight) { top = anchorRect.top - floatingRect.height - 8; // 위로 뒤집기 } if (left < 0) left = 8; // 오른쪽으로 밀기 if (left + floatingRect.width > window.innerWidth) { left = window.innerWidth - floatingRect.width - 8; // 왼쪽으로 밀기 } floating.style.top = `${top}px`; floating.style.left = `${left}px`; } // 스크롤, 리사이즈, DOM 변경마다 실행해야 함 window.addEventListener('scroll', () => updatePosition(anchor, floating), { passive: true }); window.addEventListener('resize', () => updatePosition(anchor, floating)); const observer = new ResizeObserver(() => updatePosition(anchor, floating)); observer.observe(anchor);

동작은 합니다. 근데 매 스크롤 프레임마다 JS가 레이아웃 계산을 돌리고 있어요. 브라우저 렌더링 파이프라인한테 "내가 끼어들게!"하고 방해하는 셈이죠.

3. CSS Anchor Positioning — 네이티브 솔루션.

.trigger { anchor-name: --my-trigger; } .tooltip { position: fixed; position-anchor: --my-trigger; bottom: anchor(top); left: anchor(center); translate: -50% 0; /* 자동 오버플로 처리 */ position-try-fallbacks: flip-block; }

JavaScript 없음. 스크롤 리스너 없음. 리사이즈 옵저버 없음. getBoundingClientRect() 없음. 브라우저가 렌더링 파이프라인 안에서 알아서 처리합니다. 원래 이랬어야 했는데, 이제야 된 거죠.


핵심 API: 세 가지 빌딩 블록

CSS Anchor Positioning은 세 가지 개념이 전부예요: 앵커 선언, 앵커 기준 위치 지정, 오버플로 처리.

1. 앵커 선언하기

어떤 요소든 anchor-name 속성으로 이름을 붙이면 앵커가 돼요:

.profile-avatar { anchor-name: --avatar; } .settings-gear { anchor-name: --settings-btn; }

규칙:

  • 이름은 CSS dashed-ident 문법(-- 접두사)을 써야 해요.
  • 요소 하나에 앵커 이름은 하나만 가능.
  • 앵커 이름은 포지셔닝된 요소와 같은 컨테이닝 블록 스코프에 존재해야 해요.

HTML 인라인으로도 선언 가능해요:

<button style="anchor-name: --menu-trigger">Menu</button>

2. 앵커 기준으로 위치 잡기

앵커를 기준으로 요소를 위치시키려면 세 가지가 필요해요:

  1. 위치 지정 대상은 position: absolute 또는 position: fixed여야 해요.
  2. position-anchor로 앵커에 연결.
  3. 인셋 속성(top, right, bottom, left)에서 anchor() 함수 사용.
.status-badge { position: fixed; position-anchor: --avatar; /* 아바타의 오른쪽 아래 모서리에 위치 */ top: anchor(bottom); left: anchor(right); }

anchor() 함수는 앵커의 기하 정보를 참조하는 사이드 키워드를 받아요:

anchor()의미
anchor(top)앵커의 위쪽 가장자리
anchor(bottom)앵커의 아래쪽 가장자리
anchor(left)앵커의 왼쪽 가장자리
anchor(right)앵커의 오른쪽 가장자리
anchor(center)앵커의 중심점 (해당 축 기준)
anchor(start)논리적 시작 (쓰기 방향 반영)
anchor(end)논리적 끝

퍼센트도 쓸 수 있어요:

.tooltip { position: fixed; position-anchor: --trigger; /* 앵커 왼쪽 가장자리에서 25% 지점에 위치 */ left: anchor(25%); bottom: anchor(top); }

position-area 단축 속성

흔한 배치에는 position-area가 훨씬 직관적이에요. 개별 사이드를 생각하는 대신, 앵커 주변의 3×3 그리드로 생각하면 돼요:

┌──────────┬──────────┬──────────┐
│ top left │ top      │ top right│
├──────────┼──────────┼──────────┤
│ left     │ center   │ right    │
├──────────┼──────────┼──────────┤
│ bottom   │ bottom   │ bottom   │
│ left     │          │ right    │
└──────────┴──────────┴──────────┘
/* 앵커 아래, 가운데 정렬 툴팁 */ .tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; } /* 앵커 오른쪽 사이드바 */ .sidebar { position: fixed; position-anchor: --panel; position-area: right; } /* 아이콘 오른쪽 위 알림 뱃지 */ .badge { position: absolute; position-anchor: --icon; position-area: top right; }

솔직히 대부분은 position-area만으로 충분합니다. anchor() 함수를 직접 쓸 일은 "앵커 왼쪽에서 20% 지점"처럼 픽셀 단위 정밀도가 필요한 경우뿐이에요.

3. position-try-fallbacks로 오버플로 처리

여기가 진짜 킥입니다. CSS Anchor Positioning이 JS 라이브러리를 압살하는 포인트예요. position-try-fallbacks는 배치된 요소가 화면 밖으로 삐져나갈 때 어떻게 할지 정해줍니다:

.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; /* 아래로 넘치면 위로 뒤집기 */ position-try-fallbacks: flip-block; }

내장 전략:

전략동작
flip-block블록 축에서 반대편으로 뒤집기 (위 ↔ 아래)
flip-inline인라인 축에서 뒤집기 (왼쪽 ↔ 오른쪽)
flip-block flip-inline양쪽 축 모두 뒤집기 시도

더 세밀한 제어가 필요하면 @position-try로 커스텀 폴백 위치를 정의할 수 있어요:

@position-try --above { position-area: top center; margin-bottom: 8px; } @position-try --left-side { position-area: left; margin-right: 8px; } @position-try --right-side { position-area: right; margin-left: 8px; } .tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; /* 순서대로 시도해서 맞는 걸 선택 */ position-try-fallbacks: --above, --left-side, --right-side; }

브라우저가 각 폴백을 순서대로 확인해서, 안 넘치는 첫 번째 걸로 자동 선택합니다. 레이아웃 타이밍에 알아서 처리되니까 JavaScript도 requestAnimationFrame도 리사이즈 옵저버도 필요 없어요.


Popover API와의 결합

CSS Anchor Positioning은 HTML Popover API(popover 속성)와 조합하면 진가가 드러납니다. 이 둘이 합쳐지면 JS 없는 완전체 툴팁/드롭다운이 탄생하거든요:

<button popovertarget="user-menu" style="anchor-name: --menu-btn"> 설정 ⚙️ </button> <div id="user-menu" popover anchor="menu-btn"> <nav> <a href="/profile">프로필</a> <a href="/settings">설정</a> <a href="/logout">로그아웃</a> </nav> </div>
[popover] { position: fixed; position-anchor: --menu-btn; position-area: bottom left; margin-top: 4px; position-try-fallbacks: flip-block; }

공짜로 얻는 것들:

  • 최상위 레이어 렌더링 — z-index 전쟁 끝. 팝오버는 브라우저의 최상위 레이어에서 렌더링돼요.
  • 라이트 디스미스 — 바깥 클릭하면 자동으로 닫혀요.
  • 기본 접근성 — 키보드 네비게이션과 포커스 관리를 브라우저가 처리해요.
  • 앵커 포지셔닝 — 메뉴가 트리거에 붙어있고, 오버플로를 처리하고, 스크롤에 따라 조정돼요.

JavaScript 제로. 전부요.

툴팁 패턴 (Popover 활용)

<button popovertarget="tip" popovertargetaction="toggle" style="anchor-name: --help-btn"> 도움말? </button> <div id="tip" popover="hint"> 이 작업은 되돌릴 수 없습니다. </div>
#tip { position: fixed; position-anchor: --help-btn; position-area: top center; margin-bottom: 8px; position-try-fallbacks: flip-block, flip-inline; /* 툴팁 스타일링 */ background: var(--surface-inverse); color: var(--text-inverse); padding: 6px 12px; border-radius: 6px; font-size: 0.85rem; max-width: 240px; }

popover="hint" 변형은 비모달이고 포커스를 뺏지 않아요. 툴팁에 딱이죠.

참고: popover="hint"는 Interop 2026 중점 영역이고 브라우저 지원이 아직 확대 중이에요. 크로스 브라우저 호환이 중요하다면 popover="manual"에 JavaScript show/hide 로직을 폴백으로 쓰는 게 안전해요.


실전 패턴

패턴 1: 서브메뉴가 있는 드롭다운 메뉴

/* 메인 메뉴 트리거 */ .nav-item { anchor-name: --nav-item; } /* 드롭다운 */ .dropdown { position: fixed; position-anchor: --nav-item; position-area: bottom span-right; margin-top: 4px; position-try-fallbacks: flip-block; } /* 서브메뉴 아이템이 자신의 서브메뉴 앵커 역할 */ .dropdown-item { anchor-name: --sub-trigger; } .submenu { position: fixed; position-anchor: --sub-trigger; position-area: right; margin-left: 2px; position-try-fallbacks: flip-inline; }

position-areaspan-right 키워드에 주목하세요. 앵커 위치에서 오른쪽으로 펼쳐지는데, 드롭다운 메뉴의 정확한 동작이에요. 서브메뉴는 flip-inline으로 뷰포트를 넘칠 때 왼쪽으로 뒤집혀요.

패턴 2: 폼 필드 밸리데이션 툴팁

<div class="field"> <label for="email">이메일</label> <input id="email" type="email" style="anchor-name: --email-input" required placeholder="[email protected]"> <div class="validation-msg" popover="hint" id="email-error"> 유효한 이메일 주소를 입력해 주세요 </div> </div>
#email-error { position: fixed; position-anchor: --email-input; position-area: right; margin-left: 12px; position-try-fallbacks: --below-field; background: var(--color-danger-surface); color: var(--color-danger-text); border: 1px solid var(--color-danger-border); padding: 8px 12px; border-radius: 6px; font-size: 0.85rem; max-width: 200px; } @position-try --below-field { position-area: bottom span-right; margin-top: 4px; margin-left: 0; }

데스크톱에서는 인풋 오른쪽에 밸리데이션 메시지가 나타나요. 모바일이나 좁은 뷰포트에서 공간이 부족하면 자동으로 필드 아래로 폴백하죠. 미디어 쿼리가 필요 없어요.

패턴 3: 구글 독스 스타일 맥락적 주석

/* 각 코멘트 마커가 앵커 */ .comment-marker { anchor-name: --comment; background: var(--highlight-yellow); } .comment-thread { position: fixed; position-anchor: --comment; position-area: right; margin-left: 24px; width: 280px; position-try-fallbacks: --left-side; } @position-try --left-side { position-area: left; margin-right: 24px; }

JavaScript 라이브러리에서 마이그레이션

Floating UI나 Tippy.js 쓰고 계신 분들, 개념 대응표부터 볼까요:

Floating UI → CSS Anchor Positioning

Floating UI 개념CSS 동등 기능
computePosition()position-anchor + anchor()
placement: 'bottom'position-area: bottom center
flip() 미들웨어position-try-fallbacks: flip-block
shift() 미들웨어커스텀 @position-try가 있는 position-try-fallbacks
offset(8)margin-top: 8px (또는 해당 방향 마진)
autoUpdate()내장 (자동)
arrow 미들웨어::before 의사 요소 + anchor()

마이그레이션 예제

Before (Floating UI):

import { computePosition, flip, shift, offset } from '@floating-ui/dom'; const button = document.querySelector('#trigger'); const tooltip = document.querySelector('#tooltip'); function update() { computePosition(button, tooltip, { placement: 'bottom', middleware: [offset(8), flip(), shift({ padding: 5 })], }).then(({ x, y }) => { Object.assign(tooltip.style, { left: `${x}px`, top: `${y}px`, }); }); } const cleanup = autoUpdate(button, tooltip, update);

After (CSS Anchor Positioning):

<button style="anchor-name: --trigger">여기에 호버</button> <div class="tooltip">툴팁 내용</div>
.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; position-try-fallbacks: flip-block, flip-inline; }

JS 버전은 라이브러리 임포트(Floating UI 코어 ~3KB min+gzip), auto-update 리스너 설정, 언마운트 시 cleanup까지 챙겨야 합니다. CSS 버전? 속성 세 개. JavaScript 제로.

화살표(Arrow)는 어떻게?

JavaScript 툴팁 라이브러리는 보통 arrow 미들웨어를 제공하죠. CSS Anchor Positioning에서는 의사 요소를 써요:

.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 12px; position-try-fallbacks: flip-block; } .tooltip::before { content: ''; position: absolute; bottom: 100%; left: anchor(--trigger center); translate: -50% 0; border: 6px solid transparent; border-bottom-color: var(--tooltip-bg); }

핵심은 이겁니다: 화살표 의사 요소가 anchor()로 트리거 중심에 계속 정렬됩니다. 오버플로 때문에 툴팁이 위치를 바꿔도 화살표는 꿋꿋이 제자리. JS 라이브러리에선 전용 미들웨어가 필요했던 걸, CSS 속성 하나로 끝이에요.


엣지 케이스와 주의점

1. 앵커 스코프와 가시성

앵커는 위치 지정된 요소와 같은 컨테이닝 블록 스코프에 있어야 해요. 앵커가 position: relative이면서 overflow: hidden인 컨테이너 안에 있으면, 위치 지정된 요소가 그 컨테이너 밖을 "볼" 수 없어요.

해결: 위치 지정 대상에 position: fixed를 쓰고, 앵커가 fixed 포지셔닝 컨텍스트에서 접근 가능한지 확인하세요.

2. 같은 앵커 이름을 가진 여러 요소

여러 요소가 같은 anchor-name을 가지면 DOM 순서상 마지막 요소만 활성 앵커가 돼요. 의도된 동작이지만 헷갈릴 수 있어요.

/* 이러면 안 돼요: 두 버튼이 같은 앵커 이름 */ .btn { anchor-name: --btn; } /* 툴팁은 DOM에서 마지막 .btn에 앵커링됨 */ .tooltip { position-anchor: --btn; }

해결: 고유한 앵커 이름을 쓰거나, CSS 커스텀 프로퍼티로 동적으로 관리하세요.

3. HTML anchor 속성으로 암시적 앵커링

HTML anchor 속성은 CSS에 position-anchor 없이도 암시적 앵커링을 제공해요:

<button id="my-btn">클릭</button> <div anchor="my-btn" class="popup">콘텐츠</div>
.popup { position: fixed; /* position-anchor 불필요 — HTML anchor 속성이 처리 */ position-area: bottom center; }

주의: HTML anchor 속성은 요소의 ID(-- 접두사 없이)를 쓰고, CSS anchor-name 속성은 CSS dashed-ident(-- 접두사 포함)를 써요. 섞어 쓰면 안 돼요.

4. 애니메이션 주의사항

앵커 포지셔닝된 요소에 애니메이션을 줄 때는, 앵커 위치 속성이 아니라 위치 지정된 요소 자체에 트랜지션을 걸어야 해요:

.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; /* 위치가 아니라 툴팁에 애니메이션 */ opacity: 0; transform: translateY(-4px); transition: opacity 0.2s, transform 0.2s; } .tooltip:popover-open { opacity: 1; transform: translateY(0); } /* display: none에서 보이는 상태로 전환할 때의 진입 애니메이션 */ @starting-style { .tooltip:popover-open { opacity: 0; transform: translateY(-4px); } }

@starting-style이 핵심이에요. display: none에서 보이는 상태로 전환될 때(팝오버가 열릴 때) CSS 트랜지션의 시작 상태를 정의해주거든요.

5. 동적 앵커 전환

어떤 앵커에 위치시킬지 동적으로 바꿀 수도 있어요:

.contextual-toolbar { position: fixed; position-anchor: --active-selection; position-area: top center; margin-bottom: 8px; }
// 사용자가 텍스트를 선택하면 앵커 업데이트 document.addEventListener('selectionchange', () => { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const marker = document.getElementById('selection-marker'); const rect = range.getBoundingClientRect(); marker.style.cssText = ` anchor-name: --active-selection; position: fixed; top: ${rect.top}px; left: ${rect.left}px; width: ${rect.width}px; height: ${rect.height}px; pointer-events: none; `; } });

Medium의 플로팅 포맷 툴바처럼, 사용자 상호작용에 따라 앵커 위치가 바뀌는 컨텍스트 툴바에 유용한 패턴이에요.


성능: 네이티브가 JS를 박살내는 이유

성능 차이가 "약간 빠르다" 수준이 아닙니다. 구조 자체가 달라요.

JavaScript 포지셔닝 파이프라인

사용자 스크롤
  → 스크롤 이벤트 발생
    → JavaScript가 getBoundingClientRect() 호출 (레이아웃 강제)
      → JavaScript가 새 위치 계산
        → JavaScript가 element.style 업데이트 (레이아웃 트리거)
          → 브라우저 리렌더

매 스크롤 프레임: 레이아웃 → JS → 레이아웃 → 페인트 → 컴포짓

레이아웃 쓰래싱이 발생해요. getBoundingClientRect()가 레이아웃을 강제 계산하게 만들고, style.top 설정이 또다른 레이아웃 패스를 유발하는 거예요. 최악의 경우 초당 60번 이상 이런 일이 벌어져요.

CSS Anchor Positioning 파이프라인

사용자 스크롤
  → 브라우저가 정상 레이아웃 패스에서 위치 업데이트
    → 페인트 → 컴포짓

매 스크롤 프레임: 레이아웃 (포지셔닝 포함) → 페인트 → 컴포짓

브라우저가 정상 레이아웃 단계에서 앵커 포지셔닝을 처리해요. JavaScript 개입 없음, 강제 레이아웃 계산 없음, 이중 레이아웃 패스 없음. 앵커 위치는 일반 CSS 레이아웃과 같은 패스에서 해결돼요.

실제로 어떤 차이가 나나

20개 툴팁/팝오버가 있는 페이지에서:

  • JavaScript 방식: 20개 스크롤 리스너가 각각 레이아웃 재계산을 강제. 빠른 스크롤 시 프레임 드롭이 눈에 띄어요.
  • CSS 방식: 스크롤 리스너 제로. 20개 위치가 단일 레이아웃 패스에서 해결. 스크롤 중 JavaScript 실행 제로.

CPU가 한정된 모바일 디바이스에서, 메인 스레드의 1밀리초도 중요한 환경에서, 이 차이가 부드러운 60fps 스크롤과 눈에 보이는 버벅임의 갈림길이에요.


브라우저 지원과 점진적 향상

2026년 3월 기준, CSS Anchor Positioning은 다음에서 완전 지원돼요:

  • Chrome/Edge 125+ (2024년 6월부터)
  • Firefox 147+ (2026년 초부터)
  • Safari 26+ (2026년 초부터)

구형 브라우저용으로는 @supports로 폴백을 제공하세요:

/* 폴백: 기본 absolute 포지셔닝 */ .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); } /* 점진적 향상: 앵커 포지셔닝 지원 시 사용 */ @supports (anchor-name: --a) { .tooltip { position: fixed; position-anchor: --trigger; position-area: top center; margin-bottom: 8px; translate: none; inset: auto; position-try-fallbacks: flip-block; } }

2026년 기준 모던 브라우저 사용자 비율을 고려하면, 폴백 없이 바로 써도 되는 프로젝트가 많을 거예요.


마무리

CSS Anchor Positioning은 Flexbox, Grid 이후 CSS에 추가된 가장 임팩트 있는 레이아웃 프리미티브입니다. 10년 넘게 JS가 담당하던 문제를, 오버플로 처리까지 포함해서 브라우저가 네이티브로 해결해줘요.

API는 허무할 정도로 단순합니다. 속성 세 개(anchor-name, position-anchor, position-area)로 80% 커버하고, @position-try 폴백이 나머지 20%를 챙겨줘요. Popover API까지 조합하면 JS 한 줄 없이 완전한 툴팁·드롭다운·팝오버를 만들 수 있습니다.

2026년에 새 프로젝트 시작하면서 앵커 포지셔닝 때문에 Floating UI 깔 이유? 없습니다. 브라우저가 네이티브로 해줍니다. 더 빠르고, 코드도 적고요.

기존 프로젝트도 마이그레이션 어렵지 않아요. computePosition() 호출을 CSS로 바꾸고, 스크롤/리사이즈 리스너 날리고, 포지셔닝 라이브러리를 dependencies에서 삭제하면 됩니다. 유저는 더 부드러운 스크롤을 체감하고, 번들은 가벼워지고, 코드는 깔끔해지죠.

툴팁 포지셔닝 전쟁, 끝났습니다. CSS의 승리예요.

CSSFrontendWeb DevelopmentPerformanceBrowser APIUI

관련 도구 둘러보기

Pockit의 무료 개발자 도구를 사용해 보세요