스크롤에 맞춰 움직이는 걸, 이제 JS 없이 — 스크롤 기반 애니메이션
스크롤바를 따라 진행되는 애니메이션이나 "화면에 들어오면 나타나는" 연출을, 스크롤 이벤트를 듣지 않고 CSS만으로 만든다. animation-timeline을 퍼블리셔의 시선으로 정리한다.
스크롤에 반응하는 연출은 늘 자바스크립트의 일이었어요.
맨 위에 읽기 진행 바를 그리거나, 요소가 화면에 들어올 때 스르륵 나타나게 하거나.
scroll 이벤트를 듣고 위치를 계산하거나, IntersectionObserver로 “지금 보이나”를 감시하거나.
어느 쪽이든, 스크롤할 때마다 자바스크립트가 깨어나 일을 했죠.
이제 그 연결을 CSS가 직접 해요.
핵심은 애니메이션의 시간축을 시계가 아니라 스크롤 위치로 바꾸는 거예요.
animation-timeline에 스크롤을 물리면, 시간이 흘러 진행되던 애니메이션이 스크롤을 따라 진행돼요.
위로 올리면 되감기고, 내리면 앞으로 감기고요.
가장 흔한 게 읽기 진행 바예요.
문서 전체를 스크롤 축으로 삼아, 맨 위에서 0%, 맨 아래에서 100%가 되게 폭을 늘려요.
.progress {
animation: grow linear;
animation-timeline: scroll(root block);
}
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
scroll()은 “가장 가까운 스크롤 컨테이너의 진행률”을 축으로 주는 함수예요.
이 몇 줄이, 예전에 스크롤 이벤트를 듣고 scrollY / scrollHeight를 계산해 폭을 바꾸던 코드를 통째로 대신해요.
한 발 더 나가면 “요소가 화면에 들어오는 동안”을 축으로 삼을 수도 있어요.
view()는 그 요소가 뷰포트를 가로지르는 구간을 0~100%로 잡아줘요.
.card {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
IntersectionObserver를 걸어 클래스를 붙였다 떼던 그 패턴이, 선언 한 덩이로 줄어요.
animation-range로 “언제 시작해서 언제 끝날지”를 화면 진입 구간에 맞춰 말로 고르는 거예요.
지원은 아직 고르지 않아요.
크롬·엣지는 115부터 되고, 사파리도 18부터 들어왔어요.
파이어폭스는 아직 플래그 뒤에 있어서, 기본값으론 안 돌아가요.
그래서 @supports로 감싸 “되면 스크롤에 맞춰, 안 되면 그냥 보이게” 두는 게 지금은 안전해요.
@supports (animation-timeline: scroll()) {
/* 스크롤 기반 애니메이션 경로 */
}
여기엔 다행인 성질이 하나 있어요.
이 애니메이션은 스크롤 이벤트가 아니라 브라우저 합성 단계에서 도는 쪽으로 설계돼서, 메인 스레드를 붙잡지 않아요.
INP 같은 반응성 지표를 신경 쓸 때, 스크롤마다 자바스크립트가 깨어나 계산하던 부담이 사라지는 거예요.
움직임은 늘리면서 버벅임은 줄이는, 흔치 않은 조합이죠.
퍼블리셔의 눈으로 보면 이건 반가운 회귀예요.
”화면에 들어오면 살짝 떠오르는” 정도의 절제된 연출은 원래 CSS의 몫이어야 했는데, 그동안 자바스크립트를 빌려 쓰고 있었던 거니까요.
플레인 CSS로 결을 심는다고 했던 그 방향에, 움직임 하나가 더 얹히는 셈이에요.
다만 절제는 여전히 내 몫이에요.
스크롤마다 화면 전체가 요란하게 반응하면 그건 안심이 아니라 멀미가 되니까요.
prefers-reduced-motion을 존중하고, 움직임은 “어디쯤 왔는지”를 알려주는 정도로만.
도구가 쉬워질수록, 덜어내는 판단이 더 중요해져요.
댓글