본문으로 건너뛰기
wooncloud

reduced-motion 에서 CSS 애니메이션을 원하는 프레임에 멈추기

reduced-motion 설정 시 루프 애니메이션이 사라지는 문제를 해결하기 위해 음수 animation-delay와 paused 속성을 활용해 특정 프레임에 멈추게 하는 방법과 이를 활용한 스크린샷 검수 팁을 소개합니다

·4 min read·— views··

사용법을 보여 주는 짧은 루프 애니메이션을 CSS keyframes 로 만들다가, 모션 줄이기 설정을 켜 보니 화면이 텅 비어 있는 걸 발견했습니다. 커서가 움직이고 버튼이 눌리는 4~8초짜리였습니다.

왜 빈 화면이 나올까요?

접근성을 챙기는 프로젝트에는 보통 이런 전역 규칙이 들어 있습니다.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

애니메이션이 즉시 끝나고 마지막 프레임에서 멈춥니다. 그런데 루프 애니메이션의 마지막 프레임은 대개 처음으로 돌아가기 직전의 빈 화면이죠.

모션을 줄이라는 요청이지 정보를 지우라는 요청은 아닌데, 모션을 줄인 사용자는 아무것도 못 보게 됩니다.

음수 delay 에 paused 를 걸면 됩니다

animation-delay 가 음수면 애니메이션이 이미 그만큼 진행된 상태에서 시작합니다. 여기에 paused 를 걸면 그 프레임에 그대로 멈춥니다.

/* 스테이지 컨테이너에 --dur(전체 길이)와 --still(-poster × 길이)을 변수로 둔다 */
.stage[data-still="true"] *,
.stage[data-still="true"] *::before,
.stage[data-still="true"] *::after {
  animation-duration: var(--dur) !important;      /* 전역 0.01ms 를 되돌림 */
  animation-iteration-count: infinite !important;
  animation-delay: var(--still) !important;        /* 예: -0.6 × 6000ms = -3600ms */
  animation-play-state: paused !important;
}

JS 쪽은 값만 넣어 주면 됩니다.

const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
stage.dataset.still = String(reduced);
stage.style.setProperty("--dur", `${duration}ms`);
stage.style.setProperty("--still", `${-poster * duration}ms`);

poster 는 0~1 사이 값으로, 6초짜리에서 0.6 이면 3.6초 지점의 장면이 대표 컷이 됩니다.

전역 규칙을 어떻게 이기는 걸까요?

둘 다 !important 라서 특이도가 승부를 가릅니다. .stage[data-still] * 는 (0,2,0) 이고 전역 * 는 (0,0,0) 이라 이쪽이 이깁니다.

대신 전제가 하나 있습니다. 장면 안의 모든 애니메이션이 같은 duration 을 공유하고, 타이밍을 keyframes 퍼센트로만 짜 두어야 합니다. 요소마다 길이나 delay 가 다르면 한 시점으로 멈출 수 없습니다.

덤으로 스크린샷 검수 도구가 됩니다

같은 방식으로 ?at=0.35 같은 쿼리를 받아 임의의 시점에 멈추면, 헤드리스 브라우저로 여러 시점을 찍어서 겹침이나 잘림을 눈으로 확인하면 됩니다.

영상을 녹화하지 않아도 프레임 단위로 리뷰가 되니 꽤 편합니다.

참고