스크롤 애니메이션을 구현할 때 보통 IntersectionObserver나 scroll 이벤트 리스너를 쓴다. 근데 이 방식은 코드가 생각보다 많이 필요하고, 스로틀링이나 퍼포먼스도 신경 써야 한다. Framer Motion 같은 라이브러리를 붙이면 편하지만 번들 크기가 부담이다.
CSS Scroll-Driven Animations는 이 문제를 CSS만으로 해결한다. Chrome 115부터 정식 지원되고, 지금은 주요 브라우저 대부분에서 쓸 수 있다.
animation-timeline이 핵심
기존 CSS 애니메이션은 시간(time)을 기준으로 진행된다. Scroll-Driven Animations는 그 타임라인을 스크롤 위치로 바꾼다. animation-timeline 속성에 scroll()을 넘기면, 스크롤 진행도에 따라 애니메이션이 재생된다.
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.box {
animation: fade-in linear;
animation-timeline: scroll();
}
scroll()은 가장 가까운 스크롤 컨테이너의 진행도를 타임라인으로 쓴다. 스크롤을 맨 위로 올리면 애니메이션 0%, 맨 아래로 내리면 100% 지점이 된다. animation-duration을 지정할 필요가 없다는 게 포인트다. 시간이 아니라 스크롤이 진행을 결정하니까.
특정 요소가 화면에 들어올 때 — view()
전체 스크롤이 아니라 "이 요소가 뷰포트에 보일 때"를 기준으로 하고 싶을 때가 더 많다. 그럴 땐 view()를 쓴다.
.card {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
view()는 요소가 뷰포트에 들어오기 시작할 때부터 빠져나갈 때까지를 타임라인으로 잡는다. animation-range로 구간을 세밀하게 조절할 수 있다.
entry— 요소가 뷰포트에 진입하는 구간exit— 요소가 빠져나가는 구간cover— 요소가 뷰포트와 겹치는 전체 구간
위 예시는 요소가 진입하기 시작(entry 0%)해서 40% 지점(cover 40%)에 도달할 때까지 페이드인을 진행한다. 다 보이기 전에 애니메이션이 끝나서 자연스럽다.
스크롤 진행 바 만들기
가장 실용적인 예시. 페이지 상단의 읽기 진행 바를 JS 한 줄 없이 만들 수 있다.
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
width: 100%;
background: tomato;
transform-origin: left;
animation: grow-progress linear;
animation-timeline: scroll(root block);
}
@keyframes grow-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
scroll(root block)은 루트 스크롤러(문서 전체)의 세로 스크롤을 기준으로 삼는다. 예전엔 scroll 이벤트 + 진행률 계산 + DOM 업데이트가 필요했던 게 keyframes 하나로 끝난다.
미지원 브라우저 대응
Firefox는 아직 플래그 뒤에 있다. @supports로 분기하면 깔끔하다.
@supports (animation-timeline: scroll()) {
.box {
animation: fade-in linear both;
animation-timeline: view();
}
}
지원하지 않는 브라우저에서는 애니메이션 없이 요소가 그냥 보이면 되니까, progressive enhancement로 적용하기 딱 좋다.
핵심은 "애니메이션의 진행 기준을 시간에서 스크롤로 바꾼다"는 한 문장이다. scroll()과 view() 두 함수만 익혀두면, 예전에 JS로 끙끙대던 스크롤 효과 대부분을 CSS 몇 줄로 대체할 수 있다. 라이브러리도 이벤트 리스너도 없으니 성능 걱정도 덜하다.