CSS @scope와 Anchor Positioning: 2026년 레이아웃을 바꾸는 두 가지 혁신
핵심 요약
이 글에서 확인할 내용
CSS가 스스로 경계를 그을 수 있다면 2026년 프론트엔드 생태계에서 두 가지 CSS 기능이 조용하지만 확실하게 실무의 판도를 바꾸고 있습니다. @scope 와 Anchor Positioning입니다. 두 기능은 서로 다른 문제를 풀지만, 공통적으로 "자바스크립트가 처리하던 일을 CSS가 직접 처리한다"는 흐름 위에 있습니다. @scope 는 스타일 격리 문제를 해결합니다.
- 1. @scope의 탄생 배경: CSS 격리의 새 접근
- 2. :scope 가상 클래스와 @scope At-rule의 차이
- 3. 스코프 프록시미티: 명시도 다음에 오는 새 카스케이드 축
CSS가 스스로 경계를 그을 수 있다면
2026년 프론트엔드 생태계에서 두 가지 CSS 기능이 조용하지만 확실하게 실무의 판도를 바꾸고 있습니다. @scope와 Anchor Positioning입니다. 두 기능은 서로 다른 문제를 풀지만, 공통적으로 "자바스크립트가 처리하던 일을 CSS가 직접 처리한다"는 흐름 위에 있습니다.
@scope는 스타일 격리 문제를 해결합니다. Anchor Positioning은 팝오버, 툴팁, 드롭다운의 위치 계산 문제를 해결합니다. 두 기능 모두 2026년 기준 주요 브라우저에서 안정적으로 동작하며, 점진적 적용 전략도 충분히 실현 가능합니다.
이 흐름을 조금 더 넓게 보면 지난 몇 년간 CSS는 예전이라면 반드시 자바스크립트가 담당했던 영역을 하나씩 흡수해 왔습니다. 부모 요소 선택을 가능하게 한 :has() 선택자, 컨테이너 크기에 반응하는 Container Query, 명시도 전쟁을 정리하는 캐스케이드 레이어가 모두 같은 계열의 진화입니다. @scope와 Anchor Positioning은 여기에 "스타일의 경계"와 "요소의 위치"라는 두 축을 더합니다. 우리는 이 글에서 두 기능의 배경, 내부 동작, 실전 패턴, 그리고 실무에서 자주 밟는 함정까지 차례로 짚어 보려 합니다. 단순히 "이렇게 쓰면 된다"를 넘어서, "왜 이렇게 설계됐고 언제 쓰지 말아야 하는가"까지 이해하는 것이 목표입니다.
1. @scope의 탄생 배경: CSS 격리의 새 접근
스타일 격리는 CSS의 가장 오래된 숙제입니다. 전역 네임스페이스 하나에 모든 규칙이 쏟아지는 구조에서, 한 컴포넌트의 .title이 다른 컴포넌트의 .title을 덮어쓰는 사고는 규모가 커질수록 필연적으로 발생합니다. 지난 십수 년간 우리는 이 문제를 우회하기 위해 다양한 도구를 만들어 왔습니다.
| 방법 | 격리 수준 | 빌드 도구 의존 | JS 런타임 비용 | 단점 |
|---|---|---|---|---|
| CSS Modules | 높음 | 필요 | 없음 | 빌드 설정 필요 |
| Shadow DOM | 완전 격리 | 불필요 | 있음 | 외부 스타일 차단 |
| BEM 네이밍 | 규약 의존 | 불필요 | 없음 | 사람 실수 의존 |
| styled-components | 높음 | 필요 | 있음 | SSR hydration 비용 |
| @scope | 명시적 범위 | 불필요 | 없음 | 브라우저 지원 제한 |
이 표를 조금 더 풀어 보겠습니다. BEM은 .card__title--active 같은 규약으로 충돌을 피합니다. 도구는 필요 없지만 순전히 사람의 규율에 의존하기 때문에, 팀이 커지고 마감이 급해지면 규약은 서서히 무너집니다. CSS Modules는 클래스 이름을 빌드 시점에 해시로 바꿔 지역화합니다. 확실하지만 번들러 설정이 필요하고, 클래스 이름을 문자열로 조합하는 순간(예: 동적 클래스) 격리가 새어 나갑니다. CSS-in-JS(styled-components, Emotion 등)는 컴포넌트와 스타일을 한 파일에 묶어 개발 경험은 좋지만, 런타임에 스타일을 생성·주입하므로 서버 렌더링 이후 hydration 비용과 초기 페인트 지연이라는 대가를 치릅니다. 최근 Tailwind v4처럼 빌드 타임에 모든 것을 정적으로 뽑아내는 방향이 다시 힘을 얻는 것도, 런타임 스타일 계산 비용을 줄이려는 같은 문제의식에서 출발합니다. Shadow DOM은 브라우저가 보장하는 완전 격리를 제공하지만, 그 격리가 너무 강력해서 디자인 토큰이나 전역 폰트조차 경계를 넘기 어렵고, 스타일링 API(::part, ::slotted)를 따로 배워야 합니다.
@scope는 이 계보에서 "빌드 도구도, 런타임도, 완전 격리의 부작용도 없이" 스타일 범위를 CSS 문법 자체로 선언하는 접근입니다. CSS Cascade 6 스펙에서 정의된 @scope는 스타일을 "어디에 적용할지"가 아니라 "어디서부터 어디까지 영향을 미칠지"를 CSS 선언 안에서 명시합니다. 즉 선택자의 대상만 좁히는 것이 아니라, 규칙이 살아 있는 영역 자체를 문서 트리 위에 그려 넣는 것입니다.
그 대가로 @scope는 "Proximity(근접성)"라는 새로운 카스케이드 기준을 도입합니다. 같은 명시도의 규칙이 충돌할 때, 타겟 요소와 더 가까운 스코프에 속한 규칙이 이깁니다. 이 개념이 기존의 명시도·출처 순서와 어떻게 상호작용하는지는 CSS @layer 가이드에서 다룬 카스케이드 순서와 함께 이해하면 더 명확합니다. 정리하면 @scope가 푸는 문제는 단순한 "이름 충돌 방지"가 아니라, "컴포넌트가 자기 스타일의 유효 범위를 선언적으로 소유하게 만드는 것"입니다.
2. :scope 가상 클래스와 @scope At-rule의 차이
이름이 비슷해서 자주 혼동하지만, :scope와 @scope는 완전히 다른 층위의 기능입니다. :scope는 CSS 선택자 안에서 현재 스타일 컨텍스트의 루트 요소를 가리키는 가상 클래스입니다. 반면 @scope는 At-rule, 즉 규칙 블록 전체의 유효 범위를 정의하는 구문입니다.
:scope {
--color-brand: #0070f3;
}
@scope (.card) {
img {
border-radius: 0.5rem;
object-fit: cover;
}
p {
color: var(--color-neutral-700);
line-height: 1.6;
}
}
MDN @scope 문서는 이 차이를 명확히 설명합니다. :scope는 선택자 레벨의 가상 클래스이고, @scope는 카스케이드 알고리즘에 근접성 축을 추가하는 At-rule입니다.
여기서 반드시 이해하고 넘어가야 할 세부 규칙이 하나 있습니다. @scope 블록 안에서 그냥 img, p처럼 쓴 맨선택자(bare selector)와 중첩 선택자 &는, 내부적으로 :where(:scope)가 앞에 붙은 것처럼 동작합니다. 그런데 :where()는 명시도가 0이므로, 스코프 루트 선택자(.card)가 아무리 구체적이어도 내부 규칙의 명시도에는 한 톨도 더해지지 않습니다. 즉 @scope (.card) { img { … } }에서 img 규칙의 명시도는 여전히 0-0-1입니다. 반대로 블록 안에서 :scope를 명시적으로 쓰면 그때는 가상 클래스로서 0-1-0의 명시도가 정직하게 계산됩니다. 이 비대칭을 모르면 "스코프를 걸었더니 규칙이 안 이긴다" 혹은 "왜 이렇게 쉽게 뒤집히지"라는 혼란을 겪게 됩니다.
정리하면, @scope는 명시도를 부풀리지 않으면서 규칙의 유효 범위만 좁히는 도구입니다. 우선순위는 명시도가 아니라 근접성이라는 별도 축으로 결정하며, 이 축은 명시도가 동률일 때만 작동합니다.
3. 스코프 프록시미티: 명시도 다음에 오는 새 카스케이드 축
@scope가 도입한 근접성(scope proximity)은 실무에서 가장 오해가 많은 부분입니다. "가까운 스코프가 이긴다"를 잘못 이해하면 "명시도보다 근접성이 우선한다"고 착각하기 쉽습니다. 사실은 정반대입니다. 근접성은 카스케이드에서 명시도 다음, 소스 순서 앞에 위치하는 아주 늦은 단계의 타이브레이커입니다.
MDN이 정리한 카스케이드 우선순위 순서는 다음과 같습니다. 위에서부터 먼저 판정합니다.
- 중요도(
!important) - 캐스케이드 레이어(
@layer) - 명시도(specificity)
- 스코프 근접성(scope proximity)
- 소스 순서(order of appearance)
즉 근접성은 앞 단계에서 승부가 나지 않고 명시도까지 완전히 동률일 때 비로소 작동합니다. 더 구체적인 선택자가 먼 스코프에 있다면, 그 규칙은 가까운 스코프의 덜 구체적인 규칙을 여전히 이깁니다. 근접성이 뒤집는 것은 오직 "명시도가 같은데 소스 순서로만 갈리던" 상황입니다.
근접성이 실제로 의미 있는 대표적 장면은 테마 중첩입니다. 라이트 테마 안에 다크 테마가 있고, 그 안에 다시 라이트 테마가 중첩되는 구조를 떠올려 보세요.
@scope (.light-theme) {
p { color: black; }
}
@scope (.dark-theme) {
p { color: white; }
}
가장 안쪽 라이트 테마 블록 안의 문단은 검정색이 됩니다. .dark-theme 규칙이 소스상 나중에 선언되었더라도, 해당 문단에서 스코프 루트까지의 "홉 수"를 세어 보면 안쪽 .light-theme가 한 단계, 바깥 .dark-theme가 두 단계 떨어져 있기 때문입니다. 명시도가 동률이므로 근접성이 승부를 가르고, 더 가까운 스코프가 이깁니다. 이 규칙 덕분에 우리는 테마를 아무리 깊게 중첩해도 "가장 가까운 테마 선언이 이긴다"는 직관적인 결과를 자바스크립트 없이 얻습니다. 예전이라면 선택자 순서와 명시도를 손으로 맞추며 씨름했을 문제입니다.
4. 범위 제한과 Donut Scope (from/to 구문)
@scope의 가장 독특한 기능 중 하나는 "구멍이 뚫린 스코프(Donut Scope)"입니다. 스코프 루트에서 시작하되, 특정 하위 영역은 스코프에서 도려내는 것입니다.
@scope (.card) {
h2 {
font-size: 1.25rem;
font-weight: 700;
}
}
/* Donut Scope: .card 안이지만 .card__footer 안은 제외 */
@scope (.card) to (.card__footer) {
p {
color: var(--color-text-primary);
font-size: 0.9375rem;
}
a {
color: var(--color-brand);
text-decoration: underline;
}
}
@scope (.card__footer) {
a {
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.875rem;
}
}
여기서 정확히 알아야 할 경계 규칙이 있습니다. @scope (root) to (limit)에서 상단 경계(root)는 포함(inclusive), 하단 경계(limit)는 제외(exclusive)입니다. 즉 to (.card__footer)라고 쓰면 .card__footer 요소 자신과 그 자손 전부가 스코프 바깥으로 빠집니다. 이 "하단 경계가 자기 자신까지 제외한다"는 점이 실무에서 자주 발을 거는 지점입니다. 만약 경계 요소 자신에는 스타일을 주고 그 안쪽만 도려내고 싶다면 to (.card__footer > *)처럼 자식 결합자를 써서 경계를 한 단계 안으로 밀어 넣어야 합니다. 반대로 루트 자신을 제외하고 싶다면 @scope (.card > *)로 시작합니다. 이 조합을 알아 두면 도넛의 안쪽 반경과 바깥쪽 반경을 세밀하게 조정할 수 있습니다.
도넛 스코프가 빛나는 전형적인 사례는 "부모 컴포넌트가 자식 컴포넌트의 슬롯 영역까지 스타일을 흘려보내면 안 되는" 상황입니다. 예를 들어 카드 레이아웃이 본문 링크에는 밑줄 스타일을 주고 싶지만, 그 카드 안에 꽂히는 사용자 콘텐츠나 별도 푸터 위젯에는 손대고 싶지 않을 때입니다. to 구문에 사용하는 선택자는 스코프 안에 포함되어야 한다는 제약만 지키면 됩니다. CSS Container Query와 @scope를 조합하면 컴포넌트가 자신의 공간과 스타일 경계를 모두 자율적으로 관리하는 구조가 완성됩니다. 하나는 "얼마나 넓은 공간에 놓였는가"에 반응하고, 다른 하나는 "내 스타일이 어디까지 새어 나가는가"를 통제합니다.
5. 컴포넌트 스타일 격리 실전 사례
이제 실제 피드 카드와 추천 카드를 예로 들어 보겠습니다. 두 카드는 같은 img, h3 태그를 쓰지만 완전히 다른 레이아웃을 가집니다.
@scope (.feed-card) {
:scope {
display: grid;
grid-template-rows: auto 1fr auto;
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
}
img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
h3 {
font-size: 1rem;
font-weight: 600;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
@scope (.featured-card) {
:scope {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
padding: 1.5rem;
background: var(--color-surface-elevated);
border-radius: 1rem;
}
img {
width: 100%;
aspect-ratio: 4 / 3;
}
h3 {
font-size: 1.5rem;
font-weight: 700;
}
}
이 패턴의 핵심은 .feed-card img와 .featured-card img가 충돌하지 않는다는 점입니다. @scope를 사용하면 근접성이 우선하므로, 어떤 순서로 선언되든 타겟 요소와 더 가까운 스코프의 규칙이 이깁니다. BEM이었다면 .feed-card__image, .featured-card__image처럼 이름을 늘려 격리했을 것을, 여기서는 순수하게 태그 선택자만으로 안전하게 격리했습니다. 마크업이 훨씬 담백해지고, HTML만 봐도 구조가 읽힙니다.
한 가지 운영 팁을 덧붙이면, @scope는 @layer와 함께 쓸 때 진가를 발휘합니다. 컴포넌트 레이어 안에 @scope를 중첩하면, 레이어가 큰 층위의 우선순위를 잡고 그 안에서 근접성이 세부 승부를 가르는 이층 구조가 됩니다. 레이어로 "디자인 시스템 vs 컴포넌트 vs 유틸리티"의 큰 서열을 정하고, 스코프로 "컴포넌트 내부의 규칙 유효 범위"를 정하는 식으로 역할을 나누면 명시도 인플레이션 없이 예측 가능한 스타일 시트를 유지할 수 있습니다.
6. @scope를 언제 쓰지 말아야 하나
새 기능은 항상 "언제 쓰지 말아야 하는가"까지 알아야 실무에서 무기가 됩니다. @scope도 예외가 아닙니다.
첫째, 이미 CSS Modules나 스코핑이 잘 동작하는 빌드 파이프라인이 있다면 전면 교체를 서두를 이유가 없습니다. @scope는 빌드 도구 없이도 격리를 준다는 장점이 있지만, 이미 해시 기반 격리가 자리 잡은 프로젝트에서는 두 방식이 겹치면서 오히려 추적이 어려워질 수 있습니다. 새로 시작하는 컴포넌트나 디자인 토큰 계층부터 점진적으로 도입하는 편이 안전합니다.
둘째, 깊은 중첩 스코프의 근접성 계산은 사람의 직관을 벗어나기 쉽습니다. 테마가 서너 겹 중첩되고 각 층이 서로 다른 규칙을 얹기 시작하면, "왜 이 색이 이겼는지"를 설명하기 위해 홉 수와 명시도를 동시에 추적해야 합니다. 디버깅 난이도가 올라가므로, 스코프 중첩은 의미가 분명한 경우(테마, 위젯 슬롯)로 제한하는 것이 좋습니다.
셋째, 성능 관점의 오해도 짚어야 합니다. @scope는 런타임 비용이 없지만, 매우 많은 스코프 블록을 만들면 스타일 매칭 시 고려해야 할 경계가 늘어납니다. 실무에서 문제가 될 만한 수준은 드물지만, 수천 개의 미세 스코프를 남발하기보다 컴포넌트 단위의 굵은 스코프를 유지하는 편이 유지보수와 성능 모두에 유리합니다. @scope는 만능 격리 장치가 아니라, "선언적으로 범위를 소유"하는 도구라는 위치를 기억하면 판단이 쉬워집니다.
7. Anchor Positioning 개념: position-anchor와 anchor()
이제 두 번째 축으로 넘어갑니다. Anchor Positioning은 한 요소의 위치를 다른 요소(앵커)의 위치에 상대적으로 묶는 CSS 기능입니다. MDN CSS Anchor Positioning 문서는 이를 "하나의 요소가 다른 요소를 위치 기준점으로 삼을 수 있는 메커니즘"이라고 정의합니다. 그동안 우리는 툴팁이나 드롭다운이 트리거 요소를 따라다니게 하려고, 자바스크립트로 getBoundingClientRect()를 읽고 스크롤·리사이즈 이벤트마다 좌표를 다시 계산했습니다. Anchor Positioning은 이 계산을 브라우저의 레이아웃 엔진에 위임합니다.
/* 버튼이 앵커 역할: 이름 부여 */
.dropdown-trigger {
anchor-name: --dropdown-btn;
}
/* 드롭다운 패널: 버튼 아래에 붙도록 위치 */
.dropdown-panel {
position: absolute;
position-anchor: --dropdown-btn;
top: anchor(bottom);
left: anchor(left);
min-width: anchor-size(width);
max-width: 320px;
}
동작 방식을 뜯어보면 세 조각으로 나뉩니다. 앵커가 될 요소에 anchor-name으로 이름을 붙이고, 위치를 잡을 요소에 position-anchor로 그 이름을 연결한 뒤, 인셋 속성(top, left 등)에서 anchor() 함수로 앵커의 특정 변(邊)을 참조합니다. anchor(bottom)은 "앵커의 아래 변"을 뜻하고, 여기에 calc()로 여백을 더할 수 있습니다. 이 함수는 anchor(<side>, <fallback>) 형태로 두 번째 인자에 폴백 길이를 받을 수 있는데, 앵커가 존재하지 않거나 해당 변이 그 인셋 속성과 호환되지 않을 때 폴백 값이 대신 쓰입니다. 예를 들어 top: anchor(bottom, 50px)은 앵커를 못 찾으면 위에서 50px 지점으로 떨어집니다.
크기 역시 앵커에 묶을 수 있습니다. anchor-size() 함수는 앵커의 너비·높이를 읽어 위치 요소의 크기를 계산합니다. min-width: anchor-size(width)는 드롭다운 패널이 최소한 버튼만큼은 넓어지도록 만드는 전형적인 패턴입니다. Chrome Developers 블로그의 Anchor Positioning API 소개에 따르면, 이 기능은 position: absolute 또는 position: fixed를 사용하는 요소에서만 동작합니다. 위치를 잡는 요소가 정적 흐름(static flow)에서 벗어나 있어야 앵커에 붙일 수 있기 때문입니다.
8. position-area: 3×3 그리드로 위치를 선언한다
anchor()로 변을 하나하나 지정하는 방식은 강력하지만, 단순히 "앵커 위쪽 가운데" 같은 흔한 배치에는 장황합니다. 이를 위해 스펙은 position-area라는 상위 수준 속성을 제공합니다. 이 속성은 앵커를 중심에 둔 3×3 그리드를 상상하고, 위치 요소를 그중 어느 칸에 놓을지 한 줄로 선언합니다.
.tooltip {
position: absolute;
position-anchor: --tooltip-anchor;
position-area: top center;
}
position-area: top center는 앵커 위쪽 가운데, bottom right는 앵커 아래 오른쪽에 요소를 배치합니다. top span-left처럼 span- 접두사를 쓰면 여러 칸에 걸치게 할 수도 있습니다. 그리드에서 가운데 칸은 앵커 요소의 컨테이닝 블록 크기에 해당하고, 바깥 칸까지의 거리는 위치 요소의 컨테이닝 블록에 의해 결정됩니다. 별도 크기 지정이 없으면 위치 요소는 max-content 너비를 기본으로 가지며, 가운데 정렬 위치에서는 앵커 폭에 맞춰 정렬됩니다.
여기서 한 가지 역사적 맥락을 알아 두면 좋습니다. 이 속성은 원래 inset-area라는 이름이었으나, CSS 워킹 그룹의 결정으로 position-area로 이름이 바뀌었고 Chrome 129에서 반영되었습니다. 한 버전 앞선 Chrome 128에서는 position-try-options가 position-try-fallbacks로 개명되었습니다. 2026년 현재 프로덕션 코드에서는 반드시 새 이름인 position-area와 position-try-fallbacks를 써야 합니다. 오래된 튜토리얼에서 inset-area를 마주치면 개명 전 자료라는 신호로 받아들이면 됩니다. 정식 정의는 W3C CSS Anchor Positioning Module Level 1에서 확인할 수 있습니다.

9. 툴팁·드롭다운·팝오버 구현 패턴
실전에서 가장 자주 만드는 세 가지가 툴팁, 드롭다운, 팝오버입니다. 먼저 순수 CSS 툴팁입니다.
.tooltip-btn {
anchor-name: --tooltip-anchor;
position: relative;
}
.tooltip {
position: absolute;
position-anchor: --tooltip-anchor;
bottom: calc(anchor(top) + 8px);
left: anchor(center);
transform: translateX(-50%);
background: var(--color-neutral-900);
color: white;
padding: 0.375rem 0.625rem;
border-radius: 0.375rem;
font-size: 0.8125rem;
white-space: nowrap;
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity 150ms ease, visibility 150ms ease;
}
.tooltip-btn:hover + .tooltip,
.tooltip-btn:focus-visible + .tooltip {
opacity: 1;
visibility: visible;
}
여기서 눈여겨볼 점은 :hover뿐 아니라 :focus-visible도 트리거로 넣었다는 것입니다. 마우스가 없는 키보드 사용자를 위한 최소한의 배려이며, 뒤에서 다룰 접근성 함정과 직접 연결됩니다. pointer-events: none은 툴팁이 마우스 이벤트를 가로채 깜빡임을 일으키지 않도록 막습니다.
다음은 popover API와 결합한 사용자 메뉴입니다.
<button popovertarget="user-menu" class="user-btn">계정 메뉴</button>
<div id="user-menu" popover class="user-popover">
<ul>
<li><a href="/profile">프로필 설정</a></li>
<li><a href="/billing">결제 관리</a></li>
<li><button>로그아웃</button></li>
</ul>
</div>
.user-btn {
anchor-name: --user-menu-anchor;
}
.user-popover {
position: absolute;
position-anchor: --user-menu-anchor;
top: anchor(bottom);
right: anchor(right);
margin-top: 0.5rem;
min-width: 200px;
background: var(--color-surface);
border-radius: 0.5rem;
box-shadow: var(--shadow-md);
}
popover API와 함께 사용하면 몇 가지 UX 기본기를 브라우저가 대신 처리합니다. 팝오버가 열려 있을 때 바깥을 클릭하면 자동으로 닫히는 라이트 디스미스(light dismiss), Escape 키로 닫기, 그리고 위쪽 레이어(top layer) 렌더링으로 z-index 지옥에서 벗어나는 것 등입니다. 특히 팝오버를 top layer로 띄우면 부모의 overflow: hidden이나 transform에 잘려 나가는 고전적인 문제가 사라집니다. 다만 여기에는 중요한 단서가 붙습니다. popover가 자동으로 주는 것은 어디까지나 "레이어와 열고 닫기 동작"이지, 메뉴로서의 의미론이 아닙니다. 이 구분이 접근성의 핵심이라 다음 절에서 따로 다룹니다.
10. @position-try fallback 전략
앵커에 딱 붙인 팝오버는 화면 가장자리에서 잘려 나갈 수 있습니다. 트리거가 화면 맨 아래에 있으면 아래로 펼쳐진 드롭다운이 뷰포트를 벗어납니다. @position-try는 이럴 때 대체 위치를 순서대로 시도하게 하는 장치입니다.
.tooltip {
position: absolute;
position-anchor: --my-anchor;
top: calc(anchor(bottom) + 8px);
left: anchor(center);
transform: translateX(-50%);
position-try-fallbacks:
--tooltip-above,
--tooltip-left,
--tooltip-right;
}
@position-try --tooltip-above {
top: auto;
bottom: calc(anchor(top) + 8px);
}
@position-try --tooltip-left {
top: anchor(center);
transform: translateY(-50%);
left: auto;
right: calc(anchor(left) + 8px);
}
@position-try --tooltip-right {
top: anchor(center);
transform: translateY(-50%);
left: calc(anchor(right) + 8px);
}
브라우저는 기본 위치를 먼저 시도합니다. 해당 위치로 렌더링했을 때 팝오버가 viewport를 벗어나면, position-try-fallbacks 목록을 순서대로 시도하고 넘치지 않는 첫 번째 위치를 채택합니다. 이는 Floating UI의 flip, shift 미들웨어가 자바스크립트로 하던 계산을 CSS가 선언적으로 대체하는 지점입니다. 실무에서는 위·아래·좌·우 네 방향을 모두 폴백으로 준비해 두는 것이 안전합니다. 또한 어떤 폴백도 완전히 들어맞지 않을 때 요소가 어떻게 보일지도 고려해야 하며, 필요하다면 넘칠 때 요소를 숨기는 조건부 처리까지 설계 범위에 넣습니다. 폴백을 하나도 작성하지 않으면 앵커 포지셔닝의 가장 큰 장점 하나를 스스로 버리는 셈입니다.
11. 접근성 함정: 시각적 위치는 의미가 아니다
Anchor Positioning을 도입할 때 가장 많이 저지르는 실수는 "시각적으로 붙였으니 관계도 맺어졌다"고 믿는 것입니다. 스펙과 MDN이 반복해서 강조하듯, 앵커 포지셔닝은 순수하게 시각적·공간적 결합일 뿐, DOM이나 접근성 트리에 어떤 의미론적 관계도 만들지 않습니다. 화면상으로는 툴팁이 버튼 바로 위에 떠 있어도, 스크린 리더에게 그 둘은 아무 연결이 없는 별개의 요소입니다.
그래서 우리가 손으로 채워야 하는 것이 있습니다. 첫째, 의미 연결은 ARIA로 만듭니다. 도움말 툴팁이라면 트리거에 aria-describedby를 걸고 툴팁 요소에 role="tooltip"과 일치하는 id를 부여해야 보조기술이 "이 버튼에는 이런 설명이 딸려 있다"를 전달할 수 있습니다. 이 연결이 없으면 순수 CSS 툴팁의 내용은 시각 사용자에게만 존재하는 유령이 됩니다.
둘째, DOM 순서와 시각 순서의 불일치를 경계해야 합니다. 앵커 포지셔닝은 요소를 DOM상 위치와 무관하게 화면 어디든 배치할 수 있게 해 줍니다. 그러나 스크린 리더의 읽기 순서와 키보드 탭 순서는 여전히 DOM 순서를 따릅니다. 시각적으로는 버튼 옆에 있는 안내 문구가, 마크업상 문서 맨 끝에 있다면 키보드 사용자는 한참 뒤에야 그 문구를 만나게 됩니다. 위치는 CSS로 옮기더라도, 의미상 인접해야 하는 요소는 마크업에서도 가까이 두는 것이 원칙입니다.
셋째, popover가 접근성까지 완성해 준다는 착각입니다. popover=auto는 열고 닫기와 라이트 디스미스, top layer 렌더링을 주지만, 그 콘텐츠에 "메뉴"라는 의미를 부여하지는 않습니다. 진짜 메뉴로 동작하려면 role="menu", role="menuitem", 그리고 위·아래 화살표 키 이동 같은 키보드 상호작용을 직접 구현하거나 검증된 컴포넌트 라이브러리에 맡겨야 합니다. 트리거 버튼에는 aria-haspopup과 aria-expanded로 "여기 눌러 펼치는 메뉴가 있고 지금 열려 있다"를 알려야 합니다.
넷째, 호버 전용 인터랙션의 사각지대입니다. :hover만으로 여닫는 툴팁은 키보드 사용자와 터치 기기 사용자를 배제합니다. 터치 화면에는 호버 상태가 없으므로, :focus-visible 트리거를 함께 넣고 터치로도 접근할 경로를 마련해야 합니다. 여기에 애니메이션을 넣는다면 prefers-reduced-motion을 존중해 전환 효과를 줄이거나 끄는 배려까지 더하면 좋습니다. 요약하면, 앵커 포지셔닝은 "어디에 그릴지"를 우아하게 풀어 주지만 "무엇을 의미하는지"는 여전히 우리 몫입니다.
12. 브라우저 지원 현황과 폴백
여기가 원문 대비 가장 크게 갱신해야 할 지점입니다. 2024~2025년 초까지만 해도 "Safari 미지원"이 정설이었지만, 2026년 현재 상황은 완전히 달라졌습니다.
@scope 지원 현황: Chrome 118+, Edge 118+, Firefox 128+, Safari 17.4+에서 지원합니다. 2023년 하반기부터 2024년 중반 사이에 모든 주요 엔진이 합류해, 오랜 기간 Baseline Newly Available 상태를 유지해 왔고 이제는 사실상 안심하고 쓸 수 있는 기능이 되었습니다.
Anchor Positioning 지원 현황(2026년 기준): Chromium 계열이 가장 먼저 나섰고(Chrome·Edge 125+ 기본 활성화), Safari는 18.x대에서 anchor() 등 핵심 배치 기능을 단계적으로 도입하다가 position-area를 포함한 전체 기능 집합이 Safari 26에서 완성된 것으로 알려져 있습니다(Safari 18.x 세부 버전별 지원 범위는 WebKit 공식 릴리스 노트에서 재확인을 권장합니다). Firefox는 가장 마지막 주자로, 145~146에서는 layout.css.anchor-positioning.enabled 플래그 뒤에 있다가 Firefox 147부터 기본 활성화되었습니다. 그 결과 Anchor Positioning은 2026년에 Baseline 2026 등급에 올라섰고, caniuse 기준 전 세계 커버리지는 약 82%입니다(변동 가능하니 caniuse.com/css-anchor-positioning에서 최신 수치 확인 권장). 다만 Safari의 초기 버전대처럼 부분 지원 구간에 머무른 브라우저나 사내 단말에 남아 있는 구형 브라우저를 감안하면, 폴백은 여전히 프로덕션의 필수 요소입니다.
가장 현실적인 전략은 @supports 기반 기능 감지입니다.
.tooltip {
position: fixed;
top: var(--tooltip-top, 0);
left: var(--tooltip-left, 0);
}
@supports (anchor-name: --test) {
.tooltip {
position: absolute;
position-anchor: --tooltip-anchor;
top: calc(anchor(bottom) + 8px);
left: anchor(center);
transform: translateX(-50%);
}
}
기능을 지원하지 않는 브라우저는 @supports 블록 전체를 무시하므로, 바깥의 기본 스타일이 안전망이 됩니다. 더 넓은 하위 호환이 필요하다면 OddBird가 유지하는 CSS Anchor Positioning 폴리필이 구형 Chrome·Firefox·Safari까지 지원 범위를 넓혀 주지만, 폴리필은 자바스크립트 비용과 완벽하지 않은 재현이라는 대가가 있으므로 정말 필요한 구간에만 신중히 적용하는 것이 좋습니다.
13. 기존 JavaScript 포지셔닝(Floating UI) 대체 가능성
이제 실무의 단골 질문으로 돌아옵니다. "Floating UI를 걷어내도 되는가?" 2026년의 답은 "상당 부분 가능하지만, 세부 기능을 확인하고 결정하라"입니다.
| 기능 | Floating UI | CSS Anchor Positioning |
|---|---|---|
| 기본 위치 지정 | 지원 | 지원 |
| Viewport 감지 fallback | 지원 | @position-try로 지원 |
| 화살표(꼬리) 위치 계산 | 지원 | 수동 계산 필요 |
| 가상 요소 앵커 | 지원 | 미지원 |
| Safari 지원 | 지원 | Safari 26+ 지원 |
| 번들 크기 | ~10KB | 0KB |
| 스크롤 추적 자동화 | JS 이벤트 | 브라우저 내장 |
원문 표에서 "Safari 미지원"으로 적혀 있던 항목은 이제 "Safari 26+ 지원"으로 갱신되어야 합니다. 이 한 줄의 변화가 의사결정을 크게 바꿉니다. 예전에는 Safari 때문에 CSS 방식을 보조로만 쓸 수 있었지만, 이제는 최신 브라우저 매트릭스를 쓰는 서비스라면 CSS를 기본 경로로 삼는 선택이 충분히 합리적입니다.
그럼에도 즉시 라이브러리를 제거하기 전에 확인할 것이 있습니다. 툴팁 꼬리(화살표)의 위치를 앵커에 맞춰 미세 조정하는 일은 아직 손이 갑니다. 커서를 따라다니는 툴팁처럼 실제 DOM 요소가 아닌 가상 좌표를 앵커로 삼는 시나리오는 CSS만으로는 표현하기 어렵습니다. 이런 특수 케이스가 많은 제품이라면 Floating UI를 남겨 두는 편이 낫습니다. 반대로 트리거 버튼에 붙는 평범한 드롭다운·팝오버가 대부분이라면, @supports 감지를 통해 지원 브라우저에서는 CSS로 처리하고 나머지에서만 라이브러리로 폴백하는 점진적 대체가 가장 안전합니다. 한 번에 걷어내기보다, 새로 만드는 컴포넌트부터 CSS 우선으로 전환하며 번들에서 라이브러리 의존을 서서히 줄이는 방향을 권합니다.
14. 디자인 시스템 적용 체크리스트
@scope 도입 체크리스트
- 컴포넌트 경계를 어떤 선택자를 스코프 루트로 사용할지 결정했는가
@layer와 함께 사용할 때 레이어 안에@scope를 중첩하는 방식으로 우선순위를 관리하고 있는가- 스코프 근접성이 명시도 다음에 오는 타이브레이커임을 팀이 이해하고 있는가(명시도가 근접성보다 앞선다)
- Donut Scope(
to구문)가 필요한 중첩 컴포넌트를 식별했고, 하단 경계가 제외(exclusive)라는 점을 반영했는가 :scope를 명시적으로 쓰면 명시도가 더해진다는 점을 인지했는가
Anchor Positioning 도입 체크리스트
- 대상 컴포넌트(툴팁, 드롭다운, 팝오버, 컨텍스트 메뉴)를 목록화했는가
@supports (anchor-name: --test)감지로 미지원·부분지원 브라우저에 폴백(Floating UI 또는 정적 위치)을 준비했는가@position-tryfallback 전략이 4방향(상, 하, 좌, 우)을 모두 커버하는가position-area와anchor()중 상황에 맞는 API를 선택했고, 개명 전 이름(inset-area)을 쓰고 있지 않은가popoverAPI와 조합하되,role·aria-*로 의미론을 별도로 부여했는가- 시각적 위치와 DOM 순서가 어긋나 키보드·스크린 리더 사용자를 혼란스럽게 하지 않는가
자주 묻는 질문
Q. @scope를 쓰면 CSS Modules나 Tailwind를 버려야 하나요?
아닙니다. 세 도구는 층위가 다릅니다. @scope는 규칙의 유효 범위를, CSS Modules는 클래스 이름의 지역화를, 유틸리티 퍼스트는 조합 가능한 원자 클래스를 다룹니다. 새 컴포넌트의 격리에 @scope를 얹으면서 기존 파이프라인은 유지하는 공존이 가능합니다.
Q. 근접성 때문에 예상과 다른 색이 나옵니다. 왜인가요?
대부분은 명시도를 먼저 의심해야 합니다. 근접성은 명시도가 동률일 때만 작동하므로, 한쪽 선택자가 더 구체적이면 근접성과 무관하게 그 규칙이 이깁니다. 명시도가 같은데도 결과가 이상하다면 스코프 홉 수를 세어 보세요.
Q. 앵커 포지셔닝만으로 접근성 있는 메뉴를 만들 수 있나요?
위치 계산은 CSS가 해 주지만 의미론은 그렇지 않습니다. role, aria-haspopup, aria-expanded, 화살표 키 이동은 여전히 직접 구현하거나 검증된 라이브러리에 맡겨야 합니다.
Q. Safari를 이제 신경 쓰지 않아도 되나요?
전체 기능은 Safari 26부터입니다. 그보다 낮은 버전은 부분 지원이거나 미지원이므로, 지원 매트릭스에 구형 Safari가 남아 있다면 @supports 폴백을 반드시 유지해야 합니다.
다시 정리하면
@scope와 Anchor Positioning은 CSS가 "스타일 언어"에서 "UI 레이아웃 로직 언어"로 진화하는 흐름의 두 축입니다. 하나는 스타일이 어디까지 영향을 미칠지를 선언하게 하고, 다른 하나는 요소가 어디에 놓일지를 브라우저에 위임하게 합니다. 실무 적용의 원칙을 압축하면 다음과 같습니다.
- 팀의 브라우저 지원 매트릭스를 실제 분석 데이터로 확인하고,
@scope와 Anchor Positioning 각각의 도입 시점을 별도로 결정한다. @scope는@layer구조와 함께 설계하고, 근접성이 명시도 다음에 오는 타이브레이커임을 기억한다.- Anchor Positioning은
anchor()와position-area를 상황에 맞게 골라 쓰고,popoverAPI와 조합하되 접근성 의미론은role·aria-*로 직접 채운다. @position-tryfallback을 4방향으로 반드시 작성한다.- Floating UI 같은 기존 JS 포지셔닝 라이브러리를 즉시 제거하지 말고,
@supports감지를 통한 점진적 대체 경로를 설계한다. 새 컴포넌트부터 CSS 우선으로 전환하며 의존을 줄여 나간다.
시각적으로 붙였다고 의미까지 연결된 것은 아니라는 접근성의 원칙, 그리고 "가까운 스코프가 이기되 명시도를 이기지는 못한다"는 카스케이드의 원칙, 이 두 가지만 몸에 익히면 두 기능은 2026년 프론트엔드의 든든한 기본기가 됩니다.