왜 지금 animation-trigger를 봐야 할까요?

스크롤에 반응하는 애니메이션을 만들 때마다 우리는 습관처럼 Intersection Observer를 꺼내 들었죠. 물론 잘 동작합니다. 그런데 문제는 애니메이션 로직과 관찰 로직이 분리되어 있다는 점이에요. observer 콜백에서 클래스를 붙이고, CSS에서 그 클래스에 애니메이션을 걸고... 솔직히 말하면 "스크롤되면 페이드인" 같은 단순한 요구사항치고는 배선이 너무 많아요.

animation-trigger는 이 배선을 CSS 안으로 완전히 끌어들입니다. 트리거 선언, 활성 범위, 재생/역재생 액션까지 전부 CSS 속성으로 표현되죠. 다만 중요한 전제가 있어요.

⚠️ 실험적 기능입니다. 글 작성 시점 기준 **Chrome 145+**에서만 동작하며, 스펙은 아직 Editor's Draft 단계예요. 프로덕션에 그대로 넣기 전에 반드시 브라우저 지원을 확인하세요.

이 글에서는 timeline-trigger와 animation-trigger를 조합해 스크롤 트리거 애니메이션을 만드는 방법을 실전 코드로 정리해 드릴게요. (근거자료: CSS-Tricks - animation-trigger)

한 줄 요약: 스크롤 트리거 vs 스크롤 드리븐

많이 헷갈리는 부분이라 먼저 짚고 갈게요.

  • Scroll-driven (스크롤 드리븐): 스크롤 위치에 애니메이션 진행률이 1:1로 묶여 있음. 스크롤을 멈추면 애니메이션도 멈춤. "시작 시점"이라는 개념 자체가 없어요.
  • Scroll-triggered (스크롤 트리거): 트리거가 on/off 이진 상태를 가지고, 조건이 충족되는 순간 애니메이션이 **발사(fire)**됨. 이후로는 일반 CSS 애니메이션처럼 독립적으로 재생돼요.

animation-trigger는 후자, 즉 트리거 방식을 위한 속성입니다.

Developer inspecting CSS animation-trigger property in Chrome DevTools on a web layout Dev Environment Setup

핵심 문법과 실전 코드

1) timeline-trigger로 트리거 정의하기

먼저 "언제 발동할지"를 정의하는 트리거를 만들어야 해요. timeline-trigger 숏핸드로 한 줄에 정리하는 게 일반적입니다.

/* 트리거 요소: 뷰포트에 완전히 들어오면 활성화 */
.trigger {
  /* name / source / activation-range / active-range */
  timeline-trigger: --trigger scroll() contain / cover;
}
  • --trigger: animation-trigger에서 참조할 커스텀 대시 식별자(dashed ident)
  • scroll(): 스크롤 진행 타임라인을 소스로 사용
  • contain: 요소가 완전히 보일 때 트리거 ON
  • cover: 요소의 일부라도 보이는 동안 트리거 유지

💡 순서를 바꿀 수 없습니다. background나 border 숏핸드와 달리 timeline-trigger는 값의 순서가 의미를 가집니다.

2) animation-trigger로 애니메이션 연결하기

이제 실제로 애니메이션을 재생할 요소에 트리거를 붙입니다.

.text {
  /* --trigger가 활성화되면 애니메이션 재생 */
  animation-trigger: --trigger play;
  animation: fade 0.6s ease-out;
}

@keyframes fade {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

3) 액션 조합으로 완전히 다른 동작 만들기

같은 트리거를 써도 액션만 바꾸면 전혀 다른 UX가 나옵니다. 이게 이 속성의 진짜 매력이에요.

/* 진입 시 정방향, 이탈 시 역방향 */
.card-a { animation-trigger: --trigger play-forwards play-backwards; }

/* 한 번만 재생, 이후 트리거 무시 */
.card-b { animation-trigger: --trigger play-once; }

/* 진입 시 리셋 후 재생 (스크롤 반복 시 매번 등장) */
.card-c { animation-trigger: --trigger replay; }

/* 진입 시 정지, 이탈 시 원위치 */
.card-d { animation-trigger: --trigger pause reset; }

주요 액션을 정리하면 다음과 같아요.

액션동작
play현재 재생 속도로 재생
play-forwards정방향 재생
play-backwards역방향 재생
play-once초기/일시정지 상태에서만 1회 재생
pause현재 위치에서 정지
reset진행률을 0으로 되돌리고 정지
replay진행률을 0으로 되돌리고 즉시 재생
none아무것도 하지 않음

4) 트리거와 애니메이션은 다른 요소에 둬도 됩니다

실무에서 진짜 유용한 포인트예요. 부모에 트리거를 정의하고, 여러 자식에 animation-trigger를 걸면 부모가 뷰포트에 들어오는 순간 자식들이 한꺼번에 애니메이션됩니다.

.section {
  timeline-trigger: --section scroll() contain / cover;
}

.section .item {
  animation-trigger: --section play;
  animation: fade 0.5s ease-out both;
}

리스트 형태의 카드 UI를 순차적으로 등장시킬 때 Intersection Observer를 여러 개 만들 필요가 없어지는 거죠.

Laptop screen showing scroll-triggered fade-in CSS demo with timeline-trigger code Programming Illustration

주의사항과 실무 판단

1) 트리거 스코프는 기본적으로 전역입니다

같은 이름의 트리거를 여러 요소에서 정의하면, 캐스케이드에서 나중에 오는 요소가 이깁니다. 이걸 원치 않으면 trigger-scope로 특정 DOM 서브트리에 트리거를 가둬야 해요.

.section {
  trigger-scope: --section; /* 이 서브트리 안에서만 유효 */
}

2) active-range는 activation-range를 포함해야 합니다

activation-range가 트리거가 켜지는 조건, active-range가 트리거가 유지되는 조건인데요. active-range가 activation-range를 포함하지 않으면 트리거가 켜지지 않습니다. 이 부분에서 삽질하는 분들이 많아요.

/* ❌ 잘못된 예: active가 activation을 포함하지 않음 */
timeline-trigger: --bad scroll() cover / contain;

/* ✅ 올바른 예 */
timeline-trigger: --good scroll() contain / cover;

3) 한국 SI/자사몰 환경에서의 현실적인 적용

솔직히 말씀드리면, 국내 SI 프로젝트나 자사몰에서는 당장 프로덕션 적용이 어렵습니다. 이유는 명확해요.

  • 크롬 145+ 한정 지원이라 사파리(iOS), 파이어폭스, 국내에서 여전히 비중이 높은 구형 크롬에서 동작하지 않습니다.
  • 국내 이커머스는 여전히 iOS Safari 비중이 20~30% 수준이라, 순수 CSS로만 가면 iOS에서 애니메이션이 아예 안 뜹니다.
  • 스펙이 Editor's Draft라서 문법이 바뀔 가능성이 있습니다.

실무에서 쓸 수 있는 절충안은 이렇습니다.

/* 기본은 정적 상태, 지원 브라우저에서만 트리거 활성화 */
.text {
  opacity: 1; /* 기본은 보이게 */
}

@supports (animation-trigger: none) {
  .text {
    opacity: 0;
    animation-trigger: --trigger play;
    animation: fade 0.6s ease-out forwards;
  }
}

@supports로 감싸면 미지원 브라우저에서는 그냥 콘텐츠가 보이고, 지원 브라우저에서만 애니메이션이 붙습니다. 폴백 관점에서 이게 가장 안전한 패턴이에요.

4) 접근성 관점에서 반드시 챙겨야 할 것

스크롤 트리거 애니메이션은 모션 민감 사용자에게 부담이 됩니다. prefers-reduced-motion 대응은 선택이 아니라 필수예요.

@media (prefers-reduced-motion: reduce) {
  .text, .section .item {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

시맨틱 마크업과 접근성을 함께 고민하는 흐름은 CSS로 완벽한 파이차트 만들기: 시맨틱과 접근성을 고려한 실전 가이드에서도 자세히 다뤘으니 참고하시면 좋아요.

Frontend designer comparing scroll-driven and scroll-triggered animation behaviors side by side System Abstract Visual

마무리: 지금 배워두면 1~2년 뒤에 빛을 봅니다

animation-trigger는 "JS 없이 스크롤 애니메이션을 CSS만으로" 라는 오래된 숙원을 해결하는 속성이에요. 지금 당장 프로덕션에 넣을 수는 없지만, 방향성은 명확합니다. :has(), 컨테이너 쿼리, @scope처럼 **"CSS가 삼키는 JS 영역"**의 연장선에 있거든요.

다음 단계 학습 방향

  1. timeline-trigger 숏핸드 문법을 손에 익히기 — 순서가 의미를 가지니 반복 연습이 필요합니다.
  2. Scroll-driven animations와 비교 실험 — Chrome 팀의 timeline ranges visualizer로 두 방식의 차이를 눈으로 확인해 보세요.
  3. @supports 폴백 패턴 체화 — 실험적 CSS를 다룰 때의 기본기입니다.
  4. prefers-reduced-motion 대응을 습관화 — 접근성은 나중에 붙이는 게 아니라 처음부터 넣는 겁니다.

함께 보면 좋은 글

실험적 스펙을 미리 익혀두는 건 **"나중에 표준이 됐을 때 이미 아는 사람"**이 되기 위한 가장 저렴한 투자예요. 지금은 데모로만 가지고 놀아도 충분합니다. 😄

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.