블로그 목록
프론트엔드

CSS만으로 구현하는 스크롤 애니메이션 — animation-timeline: scroll()

스크롤 애니메이션을 구현할 때 보통 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 몇 줄로 대체할 수 있다. 라이브러리도 이벤트 리스너도 없으니 성능 걱정도 덜하다.