CSS로 JavaScript를 대체하는 6가지 UI 패턴 (2026) — 브라우저 지원이 넓은 순서로 살펴보는 번들 절감법
솔직히 고백하자면, 저도 2년 전까지만 해도 툴팁 하나 만들려면 Popper.js를 npm으로 깔고, 스크롤 이벤트에 requestAnimationFrame 걸고, Intersection Observer 콜백 안에서 클래스 토글하는 게 당연한 루틴이었습니다. 그런데 어느 날 동료가 "그거 CSS로 되는데?"라고 한 마디 던졌고, 저는 그때부터 지금껏 쌓아온 JS 코드들을 다시 보게 됐습니다.
그 동료 말이 맞았습니다. Popper.js를 제거하면 번들에서 약 10KB(gzip 기준)가 사라지고, ScrollMagic을 걷어내면 35KB가 줄어듭니다. 스크롤 이벤트 리스너 없이도 진행 바가 더 부드럽게 동작하는 이유는 브라우저가 GPU에서 직접 처리하기 때문입니다. 현대 CSS가 어떤 UI 인터랙션을 흡수했는지, 그리고 실무에서 어떻게 적용할 수 있는지 브라우저 지원이 넓고 교체 효과가 큰 순서대로 코드와 함께 풀어봤습니다.
핵심 개념
"CSS는 UI 동작을, JS는 비즈니스 로직을"
이 흐름의 핵심 철학은 관심사 분리입니다. 지금까지 JavaScript는 UI 상태 관리 전반을 담당했습니다 — 스크롤 감지, 툴팁 위치 계산, 페이지 전환 애니메이션, 폼 필드 크기 조절까지요. 그런데 2023년부터 2026년 사이에 브라우저에 탑재된 신규 CSS 사양들이 이 역할을 조금씩 가져가기 시작했습니다.
왜 CSS 애니메이션이 더 부드러운가?
transform·opacity를 다루는 CSS 애니메이션은 GPU에서 직접 처리됩니다. JavaScript가 돌아가는 메인 스레드를 건드리지 않기 때문에, 무거운 JS 작업이 진행 중이더라도 애니메이션이 끊기지 않습니다. 60fps 유지가 훨씬 쉬워지는 이유가 여기에 있습니다.
아래 표에서 2026년 5월 기준 주요 기능들의 지원 현황을 확인할 수 있습니다. 이 글은 브라우저 커버리지가 넓고 교체 효과가 큰 순서대로 예시를 다룹니다.
| CSS 기능 | Chrome | Firefox | Safari | 브라우저 커버리지 |
|---|---|---|---|---|
:has() 선택자 |
105+ | 121+ | 15.4+ | 약 94% |
| Popover API | 114+ | 125+ | 17+ | 약 91% |
| View Transitions API | 111+ | 144+ | 18+ | 약 89% |
| Scroll-Driven Animations | 115+ | 미지원 | 미지원 | 약 65% |
| CSS Anchor Positioning | 125+ | 미지원 | 미지원 | 약 65% |
field-sizing: content |
123+ | 미지원 | 미지원 | 약 65% |
field-sizing: content—textarea나input에 적용하면 내용에 따라 필드 높이가 자동으로 조절됩니다. JavaScript의scrollHeight계산 코드를 한 줄의 CSS로 대체합니다. Firefox·Safari 지원이 아직 없으므로,@supports와 함께 사용하는 것을 권장합니다.
브라우저 지원이 아직 고르지 않은 기능들도 있습니다. Interop 2026 프로젝트가 뷰 트랜지션, 스크롤 기반 애니메이션, 앵커 포지셔닝의 크로스 브라우저 호환성을 최우선 목표로 삼고 있는 만큼, 2026년 하반기면 상황이 많이 달라질 가능성이 높습니다.
실전 적용
예시 1: 부모 상태 감지 — :has() 선택자 (커버리지 94%)
/* 폼 안에 에러가 있으면 폼 전체 테두리 변경 */
.form:has(.error-message) {
border: 2px solid #ef4444;
border-radius: 8px;
}
/* 체크박스가 체크되면 연결된 라벨 강조 */
.option-row:has(input[type="checkbox"]:checked) .label {
color: #6366f1;
font-weight: 600;
}
/* 폼에 유효하지 않은 입력이 없으면 제출 버튼 활성화 */
.form:not(:has(input:invalid)) .submit-button {
opacity: 1;
pointer-events: auto;
}예전이라면 모두 JavaScript로 이벤트 리스너를 걸고, 조건에 따라 부모 요소의 클래스를 토글해야 했던 패턴들입니다. :has()는 "이 요소 안에 ~가 있으면"이라는 조건을 CSS 선택자로 표현할 수 있게 해줍니다.
94% 커버리지라 지금 당장 프로덕션에서 써볼 수 있는 기능입니다. 실무에서 자주 맞닥뜨리는 상황인데, 처음 이걸 팀에 공유했을 때 "이게 CSS가 맞아?"라는 반응이 가장 많았습니다. 네, 맞습니다.
주의: 대규모 DOM에서
:has()선택자를 전역 범위에 복잡하게 중첩하면 브라우저가 DOM 변화마다 전체를 다시 계산해야 해서 성능이 떨어질 수 있습니다.body:has(.modal-open) .sidebar:has(.active-item) ul li같은 체인은 피하고, 선택자 범위는 최대한 좁게 유지하는 것이 좋습니다.
예시 2: 팝오버 — JavaScript 한 줄도 없이 (커버리지 91%)
<button popovertarget="user-menu">메뉴 열기</button>
<div id="user-menu" popover role="menu">
<ul>
<li>프로필</li>
<li>설정</li>
<li>로그아웃</li>
</ul>
</div>popover 속성 하나로 다음이 모두 자동으로 동작합니다.
| 동작 | 설명 |
|---|---|
| 열기/닫기 토글 | popovertarget으로 연결된 버튼이 자동 처리 |
| ESC 키로 닫기 | 브라우저가 기본 제공 |
| 바깥 클릭으로 닫기 | 라이트 디스미스(Light Dismiss) 기본 동작 |
| 포커스 관리 | 팝오버 열릴 때 포커스 이동, 닫힐 때 원래 위치로 복귀 |
접근성 얘기를 잠깐 하자면, popover 속성은 라이트 디스미스·ESC 키·포커스 복귀를 브라우저가 자동으로 처리합니다. 다만 role="menu"나 aria-label 같은 시맨틱 속성은 여전히 직접 붙여줘야 합니다. 커스텀 모달이나 드롭다운을 처음부터 구현하다가 접근성 이슈로 고생해본 적이 있다면, 브라우저가 이 부분을 대신 챙겨준다는 게 얼마나 반가운 일인지 공감될 겁니다.
예시 3: 페이지 전환 애니메이션 — View Transitions (커버리지 89%)
View Transitions는 조금 독특한 포지션입니다. JavaScript 트리거가 필요하지만 실제 애니메이션 연출은 CSS가 전담하는, JS+CSS 협업 패턴입니다. JS는 DOM이 바뀐다는 타이밍을 브라우저에 알리고, CSS는 그 전환을 꾸밉니다.
// JS가 하는 일: DOM 변경 타이밍을 브라우저에 알리기
document.startViewTransition(() => {
document.querySelector('.content').innerHTML = newContent;
});/* 나가는 페이지 */
::view-transition-old(root) {
animation: slide-out 300ms ease-in;
}
/* 들어오는 페이지 */
::view-transition-new(root) {
animation: slide-in 300ms ease-out;
}
@keyframes slide-out {
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slide-in {
from { transform: translateX(100%); opacity: 0; }
}
::view-transition-old와::view-transition-new— 뷰 트랜지션이 시작되면 브라우저는 현재 화면을 스냅샷으로 캡처해old, 새 화면을new로 관리합니다. CSS 슈도 엘리먼트로 각각의 애니메이션을 독립적으로 제어할 수 있습니다.
저도 처음에 "이게 SPA 전환 로직을 다 대체할 수 있다고?" 싶었는데, 2025년 10월에 Baseline Newly Available 상태에 진입하면서 Chrome, Edge, Firefox, Safari 모두에서 쓸 수 있게 됐습니다. SPA에서 수천 줄짜리 페이지 전환 로직을 관리하던 팀이라면 꽤 큰 변화입니다.
예시 4: 스크롤 진행률 표시바 — window.scroll 이벤트와 이별하기 (커버리지 65%)
가장 흔하게 마주치는 케이스입니다. 블로그 상단에 스크롤 진행률을 보여주는 바, 다들 한 번쯤 구현해봤을 겁니다. 예전 방식은 이랬죠.
// 스크롤할 때마다 메인 스레드에서 DOM을 건드립니다
window.addEventListener('scroll', () => {
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = (scrollTop / scrollHeight) * 100;
document.querySelector('.progress-bar').style.width = `${progress}%`;
});페이지가 무거우면 버벅임이 생기고, requestAnimationFrame으로 감싸도 근본적인 한계가 있습니다. 이제는 이렇게 할 수 있습니다.
@keyframes grow-bar {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: #6366f1;
transform-origin: left;
/* animation-duration 생략이 맞습니다 —
Scroll-Driven Animations에서는 스크롤 위치 자체가 타임라인을 대체합니다 */
animation: grow-bar linear;
animation-timeline: scroll(root);
}| 코드 요소 | 역할 |
|---|---|
animation-timeline: scroll(root) |
루트(페이지 전체) 스크롤 위치를 타임라인으로 사용 |
animation: grow-bar linear |
스크롤 0%에서 100%까지 애니메이션 선형 진행 |
transform-origin: left |
scaleX 변환 기준점을 왼쪽으로 고정 |
JavaScript 한 줄 없이, GPU에서 부드럽게 동작합니다.
브라우저 지원 주의: Chrome 115+ 전용입니다 (2026년 5월 기준). Firefox·Safari는 아직 미지원이므로,
@supports (animation-timeline: scroll()) { ... }블록 안에 작성하고 JS 폴백을 병행하는 것을 권장합니다.
예시 5: 스크롤 시 요소 등장 — Intersection Observer 대신 (커버리지 65%)
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
/* both: 애니메이션 시작 전은 from 상태, 끝난 후는 to 상태를 유지합니다 */
animation: fade-in-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 30%;
}animation-timeline: view()는 해당 요소가 뷰포트에 들어오고 나가는 것을 타임라인으로 삼습니다. animation-range: entry 0% entry 30%를 풀어서 설명하면 이렇습니다.
entry 0%: 요소의 맨 아래쪽이 뷰포트 하단에 처음 닿는 순간entry 30%: 요소의 30%가 뷰포트 안에 들어온 순간
이 두 시점 사이에서 애니메이션이 완료됩니다. 처음 이걸 팀 코드리뷰에 올렸을 때 "이거 JS 없는 거 맞아?"라는 댓글이 두 개 달렸습니다. 값을 직접 조정하면서 타이밍을 잡아보고 싶다면 Scroll-Driven Animations 데모 사이트가 좋습니다. 눈으로 확인하면서 맞추는 게 훨씬 빠릅니다.
브라우저 지원 주의: 예시 4와 동일하게 Chrome 115+ 전용입니다. 미지원 브라우저에서는 요소가 처음부터 보이도록 폴백을 설정해두는 것이 좋습니다.
예시 6: 툴팁 위치 계산 — Popper.js와 작별 (커버리지 65%)
저도 처음엔 앵커 포지셔닝이 "그거 CSS로 되는 게 맞아?" 싶었습니다. 툴팁 위치 계산은 스크롤, 뷰포트 경계, 오버플로우 등을 모두 고려해야 해서 항상 Floating UI나 Popper.js 같은 라이브러리에 의존했거든요.
/* 1. 기준이 되는 요소에 앵커 이름 부여 */
.anchor-button {
anchor-name: --my-btn;
}
/* 2. 툴팁이 앵커 기준으로 위치 잡기 */
.tooltip {
position: absolute;
position-anchor: --my-btn;
top: anchor(bottom);
left: anchor(center);
translate: -50% 8px;
/* 아래에 공간이 없으면 위로 자동 전환 */
position-try-fallbacks: --flip-up;
}
@position-try --flip-up {
top: auto;
bottom: anchor(top);
translate: -50% -8px;
}<button class="anchor-button">저장</button>
<div class="tooltip" role="tooltip">변경사항을 저장합니다</div>anchor() 함수는 기준 요소의 특정 엣지(top, bottom, left, right, center)를 참조합니다. "Popper.js처럼 뷰포트 경계도 처리할 수 있냐?"는 당연히 드는 의문인데, position-try-fallbacks가 그 역할을 합니다. 뷰포트 아래쪽에 공간이 부족하면 브라우저가 자동으로 --flip-up 위치를 시도합니다. Popper.js가 200줄 이상의 JS로 처리하던 것을 이 CSS 블록 하나가 대신합니다.
브라우저 지원 주의: Chrome 125+ 전용입니다. Firefox·Safari 지원이 아직 없으므로, Chrome/Edge 위주 사내 툴이나 어드민 페이지에 먼저 적용해보시면 좋습니다. Interop 2026 목표 기능이라 올 하반기에는 상황이 달라질 가능성이 높습니다.
장단점 분석
물론 장밋빛 얘기만 하면 안 되겠죠. 제가 실제로 밟았거나 팀에서 이슈가 됐던 것들을 솔직하게 정리해봤습니다.
장점
| 항목 | 내용 |
|---|---|
| 성능 | transform·opacity 기반 CSS 애니메이션은 GPU 처리로 메인 스레드 블로킹 없이 60fps 유지 가능 |
| 코드 감소 | 150줄 이상의 JS가 수 줄의 CSS로 대체되는 경우가 실제로 많음 |
| 접근성 내장 | Popover API는 포커스 복귀·ESC 키·라이트 디스미스를 브라우저가 자동 처리 |
| 번들 크기 절감 | Popper.js 제거 시 약 10KB gzip, ScrollMagic 제거 시 약 35KB gzip 절감 |
| 유지보수 용이 | UI 상태가 CSS에 집중되면 JS 코드의 복잡도가 눈에 띄게 줄어듦 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 브라우저 지원 격차 | 스크롤 기반 애니메이션·앵커 포지셔닝은 Firefox·Safari 미지원 (2026년 5월 기준) | @supports로 기능 감지 후 JS 폴백 병행 |
:has() 성능 함정 |
대규모 DOM에서 복잡한 :has() 셀렉터 남용 시 레이아웃 재계산 비용 증가 |
선택자 범위를 좁게 유지, DevTools로 레이아웃 비용 확인 |
| View Transition LCP 영향 | 모바일에서 LCP +70ms 증가가 보고됨 | 전환 duration을 200ms 이하로 유지, prefers-reduced-motion 적용 |
| JS 완전 대체 불가 | 데이터 페칭, 비즈니스 로직, 복잡한 상태 관리는 여전히 JS 영역 | 역할 분리를 명확히 — CSS는 표현, JS는 로직 |
| 학습 곡선 | 새 CSS 사양이 빠르게 추가되어 전체 파악에 꾸준한 노력 필요 | Baseline 지표와 Can I Use를 주기적으로 확인하는 습관 |
@supports활용법 —@supports (animation-timeline: scroll()) { /* 지원하는 경우 */ }형태로 작성하면, 지원하지 않는 브라우저는 이 블록을 무시합니다. 미지원 브라우저에서도 기본 UI가 깨지지 않도록 폴백을 항상 함께 준비하는 것이 좋습니다.
prefers-reduced-motion— 시스템 설정에서 모션 감소를 요청한 사용자에게는 애니메이션을 최소화하거나 제거하는 것이 접근성 원칙입니다.@media (prefers-reduced-motion: reduce) { .card { animation: none; } }형태로 적용할 수 있습니다.
실무에서 가장 흔한 실수
-
@supports없이 프로덕션에 바로 적용하기 — Scroll-Driven Animations나 앵커 포지셔닝은 Firefox·Safari에서 동작하지 않습니다. 폴백 없이 배포하면 특정 사용자에게 UI가 아예 깨질 수 있습니다. -
:has()선택자를 전역 범위에서 복잡하게 중첩하기 —body:has(.modal-open) .sidebar:has(.active-item) ul li처럼 복잡한 체인은 DOM 변화마다 전체를 다시 계산해야 합니다. 선택자 범위는 필요한 곳에서만, 최대한 좁게 유지하는 것이 좋습니다. -
View Transitions를 모든 화면 전환에 무분별하게 적용하기 — 전환 효과가 길거나 복잡할수록 모바일에서 LCP에 악영향을 줍니다. 핵심 경로의 페이지에는 duration을 200ms 이하로 짧게 유지하고, 반드시
prefers-reduced-motion에 대응하는 것을 권장합니다.
마치며
현대 CSS는 UI의 "움직임"을 전담하고, JavaScript는 "데이터와 로직"에 집중하는 방향으로 프론트엔드 개발의 무게중심이 옮겨가고 있습니다. 한꺼번에 모든 걸 바꿀 필요는 없습니다. 지금 프로젝트의 package.json에서 Popper.js, ScrollMagic, GSAP 같은 패키지가 보인다면, 이 글의 해당 예시로 교체를 먼저 검토해볼 만합니다. Popper.js 제거만으로 약 10KB, ScrollMagic 제거로 약 35KB의 번들 절감이 가능합니다.
지금 바로 시작해볼 수 있는 3단계:
-
가장 지원이 넓은 것부터 적용해볼 수 있습니다 —
:has()선택자는 94% 커버리지로 지금 당장 프로덕션에서 쓸 수 있습니다. 이벤트 리스너로 부모 클래스를 토글하는 패턴이 코드베이스에 있다면,:has()로 교체해보시면 좋습니다. -
새 컴포넌트 작성 시 Popover API를 먼저 고려해볼 수 있습니다 — 다음에 툴팁, 드롭다운, 모달을 새로 만들 때
popover어트리뷰트로 시작해보시면 좋습니다. 접근성 처리를 브라우저가 대신해주는 경험이 꽤 인상적일 겁니다. 지원 범위가 91%에 달해 대부분의 프로덕션 환경에서 안전하게 사용할 수 있습니다. -
Scroll-Driven Animations는 데모 사이트에서 먼저 눈으로 확인해보시면 좋습니다 — 스크롤 기반 애니메이션은 코드만으로는 체감이 잘 안 됩니다. 데모를 직접 조작해보고, Chrome DevTools의 애니메이션 패널로 타임라인을 확인하면서 적용하면 훨씬 빠르게 익힐 수 있습니다.
참고 자료
공식 문서
심화 학습
- What's New in View Transitions (2025 Update) | Chrome for Developers
- Scroll-Triggered Animations Coming | Chrome for Developers
- CSS in 2026 — New Features Reshaping Frontend Development | LogRocket
- What You Need to Know About Modern CSS (2025 Edition) | Frontend Masters
- Menus, Toasts and More with Popover API & Anchor Positioning | Frontend Masters
- Replace JavaScript Animations with View Transitions | Builder.io
- Anchor Positioning and Popover API for a JS-Free Site Menu | CSS-IRL
- 10 CSS Features That Replace JavaScript (2026) | BigDevSoon
- Scroll-Driven Animations | Josh W. Comeau
- Announcing Interop 2026 | WebKit
실습 도구
- Scroll-Driven Animations 데모 사이트
- Can I Use — 브라우저별 CSS 기능 지원 현황 확인