GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

css 스크롤 애니메이션 효과 9가지 — 뷰포트 구간 값으로 전환

css 스크롤 애니메이션 효과는 값 하나가 아홉 가지 다른 움직임을 만든다. 화면을 내리면 카드 한 장이 흐릿하게 떠 있다가 점점 또렷해진다. 그 흐림 정도는 스크롤 위치가 아니라 그 요소가 화면을 통과하는 구간 값 하나로 정해진다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아래 아홉 개는 그 구간 값을 어디에 물리는지로 나눴다. 앞쪽 여섯은 요소가 화면에 들어오는 구간(entry) 하나만 보고, 가운데 둘(04·05)은 화면을 완전히 통과하는 구간(cover) 전체를 보며, 08번도 다시 cover 로 돌아와 그림자 값을 늘린다. 같은 값이라도 어디에 대입하느냐에 따라 크기·색·기울기·간격처럼 전혀 다른 장면이 된다.

값이 어디서 갈리는가 — 구간 값 하나가 아홉 장면을 나눈다

CSS 만으로 이 아홉 개를 움직이는 축은 두 줄이다. animation-timeline: view() 가 키프레임의 진행률을 요소가 가장 가까운 스크롤 컨테이너를 지나는 위치에 묶고, animation-range 가 그 지나는 구간 중 어디부터 어디까지를 0%~100% 로 잡을지 고른다. entry 는 요소가 들어오기 시작해 완전히 들어올 때까지, cover 는 요소가 화면에 처음 걸치는 순간부터 완전히 빠져나갈 때까지다. 이 값 하나가 css 마우스 스크롤 애니메이션에서 카드 확대부터 배지 색 전환까지 아홉 장면을 각각 다른 속성에 물린다. 데모는 CSS.supports('animation-timeline: view()') 로 지원 여부를 먼저 재고, 미지원이면 entry 항목은 IntersectionObserver 교차 비율로, cover 항목은 스크롤 위치에서 요소 높이·뷰 높이로 직접 통과 진행률을 계산해 같은 속성에 대입한다. 값과 구간의 전체 스펙은 MDN 의 animation-range 문서에 정리돼 있다.

01구간 값에 맞춰 커지며 또렷해지는 카드

카드가 화면에 들어오는 동안 흐려져 있던 모습이 커지면서 또렷해진다. entry 구간의 진행률 값을 그대로 opacity 에, 시작 배율 0.8 에서 1 까지의 폭에 곱해 scale 에 대입한다.

구간 값에 맞춰 커지며 또렷해지는 카드
새 창에서 열기

01 · 구간 값에 맞춰 커지며 또렷해지는 카드 — 측정 누적면적 16.106% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

animation-range: entryanimation-timeline: view()--fade-scale-start
// 네이티브: animation-timeline: view() + animation-range: entry 가 진입 구간 값을
// 직접 투명도·크기에 묶는다.
.supports-vt .fsc__card {
  animation-name: fscEnter;
  animation-duration: 1s;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: entry;
}
@keyframes fscEnter {
  0%   { opacity: 0; transform: scale(var(--fade-scale-start, .8)); }
  100% { opacity: 1; transform: scale(1); }
}

측정: 누적면적 16.1%, 23/23 프레임에서 움직였다.

02경계 값이 옮겨가며 드러나는 썸네일

썸네일을 가린 사각형이 오른쪽부터 걷히며 이미지를 조금씩 보여준다. clip-path 의 오른쪽 경계 값이 entry 구간 진행률을 따라 100% 에서 0% 로 줄어들어, 가려진 폭만큼만 이미지가 드러난다.

경계 값이 옮겨가며 드러나는 썸네일
새 창에서 열기

02 · 경계 값이 옮겨가며 드러나는 썸네일 — 측정 누적면적 17.112% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

clip-path: insetanimation-range: entrycprReveal
.supports-vt .cpr__panel {
  animation-name: cprReveal;
  animation-duration: 1s;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: entry;
}
@keyframes cprReveal {
  0%   { clip-path: inset(0 100% 0 0); }
  100% { clip-path: inset(0 0% 0 0); }
}

측정: 누적면적 17.1%, 23/23 프레임에서 움직였다.

03대각선 경계 값이 훑고 지나가는 배경 이미지

배경 이미지 위를 덮은 대각선 경계가 아래로 훑고 지나가며 이미지를 드러낸다. 등록해 둔 커스텀 프로퍼티 --wipe 값이 구간 진행률을 따라 0% 에서 100% 로 바뀌고, mask-image 그라디언트 경계를 calc(var(--wipe) - 12%) 에서 var(--wipe) 까지로 계산해 대각선 경계가 이동한다.

대각선 경계 값이 훑고 지나가는 배경 이미지
새 창에서 열기

03 · 대각선 경계 값이 훑고 지나가는 배경 이미지 — 측정 누적면적 21.765% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

@property --wipemask-image135deg
.mwi__pic {
  width: 100%;
  height: 100%;
  background:
    radial-gradient(circle at 30% 30%, $subject-sky 0%, transparent 55%),
    linear-gradient(135deg, $subject-blue-deep, $color);
  -webkit-mask-image: linear-gradient(135deg, #000 calc(var(--wipe) - 12%), transparent var(--wipe));
  mask-image: linear-gradient(135deg, #000 calc(var(--wipe) - 12%), transparent var(--wipe));
}

.supports-vt .mwi__pic {
  animation-name: mwiWipe;
  animation-duration: 1s;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: entry;
}
@keyframes mwiWipe {
  0%   { --wipe: 0%; }
  100% { --wipe: 100%; }
}

측정: 누적면적 21.8%, 23/23 프레임에서 움직였다.

04구간 값 따라 초점이 풀렸다 맞는 카드

카드가 화면을 지나는 동안 초점이 풀렸다 맞았다를 반복한다. cover 구간을 0→50→100% 로 나눠 흐림 값을 12px 에서 0px 로 줄였다가 다시 12px 로 늘리는 하나의 굴곡 값(hump)으로 들어올 때와 나갈 때 각각 초점이 풀리고 맞는다.

구간 값 따라 초점이 풀렸다 맞는 카드
새 창에서 열기

04 · 구간 값 따라 초점이 풀렸다 맞는 카드 — 측정 누적면적 28.216% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

animation-range: coverfilter: blurhump
.supports-vt .bfs__card {
  animation-name: bfsFocus;
  animation-duration: 1s;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: cover;
}
@keyframes bfsFocus {
  0%   { filter: blur(var(--blur-max, 12px)); opacity: .55; }
  50%  { filter: blur(0px); opacity: 1; }
  100% { filter: blur(var(--blur-max, 12px)); opacity: .55; }
}

측정: 누적면적 28.2%, 23/23 프레임에서 움직였다.

05구간 값이 각도 값으로 바뀌는 3D 카드

카드가 화면을 지나는 동안 입체적으로 뒤집혀 뒷면을 보여준다. 같은 cover 구간 진행률 값을 rotateY 각도(0deg~180deg)로 바꿔 회전시키고, 구간이 중간을 지나는 순간 backface-visibility 로 감춰 둔 뒷면이 정면에 드러난다.

구간 값이 각도 값으로 바뀌는 3D 카드
새 창에서 열기

05 · 구간 값이 각도 값으로 바뀌는 3D 카드 — 측정 누적면적 17.417% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

preserve-3drotateYbackface-visibility
.supports-vt .r3f__inner {
  animation-name: r3fFlip;
  animation-duration: 1s;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: cover;
}
@keyframes r3fFlip {
  0%   { transform: rotateY(0deg); }
  100% { transform: rotateY(180deg); }
}

측정: 누적면적 17.4%, 23/23 프레임에서 움직였다.

06기울기 값이 끝에서 풀리는 패널

패널이 비스듬히 기울어져 있다가 화면 안쪽으로 들어올수록 반듯해진다. entry 구간 진행률 값을 skewX 각도(-18deg~0deg)로 매핑해, 구간 시작에 가까울수록 기울기가 크게 남아 있다가 값이 끝에 닿는 순간 0deg 로 풀린다.

기울기 값이 끝에서 풀리는 패널
새 창에서 열기

06 · 기울기 값이 끝에서 풀리는 패널 — 측정 누적면적 17.512% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

skewXanimation-range: entrysspShear
.supports-vt .ssp__panel {
  animation-name: sspShear;
  animation-duration: 1s;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: entry;
}
@keyframes sspShear {
  0%   { transform: skewX(-18deg); }
  100% { transform: skewX(0deg); }
}

측정: 누적면적 17.5%, 23/23 프레임에서 움직였다.

07구간 값이 색상 보간 지점이 되는 배지

배지 색이 한 색에서 다른 색으로 서서히 옮겨간다. entry 구간 진행률 값을 두 배경색(파랑 계열 → 주황 계열) 사이의 보간 지점으로 써서, 별도 트랜지션 없이도 색이 값 하나로 연속해서 바뀐다.

구간 값이 색상 값 보간 지점이 되는 배지
새 창에서 열기

07 · 구간 값이 색상 값 보간 지점이 되는 배지 — 측정 누적면적 10.083% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

background-colorCOLOR_FROMcmbMorph
.supports-vt .cmb__pill {
  animation-name: cmbMorph;
  animation-duration: 1s;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: entry;
}
@keyframes cmbMorph {
  0%   { background-color: $subject-blue-deep; }
  100% { background-color: $subject-orange-deep; }
}

측정: 누적면적 10.1%, 23/23 프레임에서 움직였다.

08구간 값만큼 떠오르는 그림자 카드

카드가 화면을 지나는 동안 점점 떠오르는 것처럼 보인다. cover 구간 진행률 값에 맞춰 box-shadow 의 번짐 거리·흐림 값을 calc(6px + --shadow-spread × 비율) 식으로 함께 늘리고, 동시에 translateY 로 살짝 띄운다.

구간 값만큼 떠오르는 그림자 카드
새 창에서 열기

08 · 구간 값만큼 떠오르는 그림자 카드 — 측정 누적면적 28.618% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

box-shadowtranslateY--shadow-spread
.supports-vt .sld__card {
  animation-name: sldLift;
  animation-duration: 1s;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: cover;
}
@keyframes sldLift {
  0%   { box-shadow: 0 6px 10px 0px rgba(23, 20, 26, .35); transform: translateY(0px); }
  100% {
    box-shadow: 0 calc(6px + var(--shadow-spread, 24px) * .3) calc(10px + var(--shadow-spread, 24px) * .6) var(--shadow-spread, 24px) rgba(23, 20, 26, .35);
    transform: translateY(-14px);
  }
}

측정: 누적면적 28.6%, 23/23 프레임에서 움직였다.

09구간 값이 글자 간격 값으로 벌어지는 제목

제목이 좁게 뭉쳐 있다가 화면에 들어올수록 글자 사이가 넓게 벌어진다. entry 구간 진행률 값을 letter-spacing(-0.04em~0.4em)으로 매핑해, 값이 채워지는 만큼 글자 간격과 불투명도가 함께 커진다.

구간 값이 글자 간격 값으로 벌어지는 제목
새 창에서 열기

09 · 구간 값이 글자 간격 값으로 벌어지는 제목 — 측정 누적면적 4.267% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

letter-spacingIntersectionObserverlssStretch
.supports-vt .lss__title {
  animation-name: lssStretch;
  animation-duration: 1s;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: entry;
}
@keyframes lssStretch {
  0%   { letter-spacing: -0.04em; opacity: .35; }
  100% { letter-spacing: 0.4em; opacity: 1; }
}

측정: 누적면적 4.3%, 23/23 프레임에서 움직였다.

구간 값 아홉 개가 실제로 얼마나 움직였는가 — 23구간 델타 실측

24프레임을 캡처해 화면이 바뀐 넓이를 잰 값이다. 움직인 프레임은 인접한 두 캡처 사이 23개 구간(델타) 중 픽셀이 달라진 구간 수다.

항목 누적면적 움직인 프레임
01 구간 값에 맞춰 커지며 또렷해지는 카드 16.1% 23/23
02 경계 값이 옮겨가며 드러나는 썸네일 17.1% 23/23
03 대각선 경계 값이 훑고 지나가는 배경 이미지 21.8% 23/23
04 구간 값 따라 초점이 풀렸다 맞는 카드 28.2% 23/23
05 구간 값이 각도 값으로 바뀌는 3D 카드 17.4% 23/23
06 기울기 값이 끝에서 풀리는 패널 17.5% 23/23
07 구간 값이 색상 보간 지점이 되는 배지 10.1% 23/23
08 구간 값만큼 떠오르는 그림자 카드 28.6% 23/23
09 구간 값이 글자 간격 값으로 벌어지는 제목 4.3% 23/23

값 하나만 바꾸면 장면이 어떻게 달라지는가 — 변형 세 가지

칩 세 개의 상수 값만 바꿔도 같은 계산식이 전혀 다른 인상을 만든다.

이름 바꾼 값 느낌
카드 등장 시작 크기 값 조정 01 VIEWTL_FADE_SCALE_START 0.8 → 0.6 시작 크기를 더 줄이면 카드가 자라는 폭이 넓어져 등장이 힘 있게 보이고, 반대로 1 에 가깝게 올리면 자라는 폭이 거의 없어 있는 듯 없는 듯 지나간다.
초점 전환 흐림 최대 값 조정 04 VIEWTL_BLUR_MAX_PX 12 → 24 최대 흐림을 낮추면 초점이 맞아가는 과정이 눈에 잘 안 띄고, 24px 까지 올리면 카드가 화면에 걸칠 때마다 초점이 크게 흔들리는 게 또렷이 보인다.
그림자 번짐 거리 값 조정 08 VIEWTL_SHADOW_SPREAD_PX 24 → 48 번짐 거리를 줄이면 그림자가 카드 밑에 바짝 붙어 낮게 얹힌 느낌이고, 48px 로 늘리면 그림자가 멀리 퍼져 카드가 화면 위로 훌쩍 떠오른 것처럼 보인다.

css 스크롤 내리면 나타나는 애니메이션을 실제 화면 어디에 붙이나

아홉 개 원본을 전부 담은 zip 은 yagmcszp 을 넣어야 열리는데, 그 안 파일이 위 표의 변형 값까지 그대로 재현해 둔 것이다. 랜딩 페이지 기능 소개 줄에는 01 처럼 카드가 커지며 또렷해지는 구간 값을 붙이면 스크롤할 때마다 다음 기능이 순서대로 강조된다. 포트폴리오 목록에는 02 의 경계 값 공개를, 히어로 섹션 배경에는 03 의 대각선 와이프를 붙이면 첫 화면부터 구간 값이 시선을 끈다.

제품 소개 섹션에는 04 의 초점 전환이, 서비스 카드 묶음에는 05 의 3D 뒤집기가 어울린다. 가격표 패널에는 06 의 기울기 정렬을, 신규 배지에는 07 의 색상 전환을, 카드 목록에는 08 의 그림자 뜨는 효과를 붙이면 화면이 밋밋하지 않다. 섹션 타이틀에는 09 의 글자 간격 확장이 구간 값 하나로도 제목이 살아 있게 만든다.

구간 계산을 스킵하면 깨지는 지점

01·02·06·07·09 처럼 entry 구간을 쓰는 항목은 요소 높이와 뷰 높이로 구간 끝 지점을 스스로 잰다. 뷰보다 요소가 크면 이 값이 100% 를 넘지 않게 잘라 두지 않으면 구간이 절대 안 끝나 애니메이션이 멈춘 것처럼 보이는 함정이 생긴다. 04·05·08 처럼 cover 구간을 쓰는 항목은 스크롤 컨테이너의 스크롤 위치에서 직접 진행률을 계산하는데, 컨테이너가 아예 스크롤되지 않는 화면에 그대로 옮기면 진행률이 0 에 고정돼 깨진다. 03 의 --wipe 처럼 커스텀 프로퍼티로 값을 보간하려면 @property 로 타입(<percentage>)을 등록해 둬야 한다 — 등록이 빠지면 브라우저가 문자열로 취급해 중간값 없이 시작과 끝만 툭 튀어 버린다.

동작 줄임 설정에서 아홉 개가 함께 멈추는 접근성 처리

아홉 항목 모두 prefers-reduced-motion: reduce 아래에서 animation: none !important 로 구간 애니메이션을 끄고, 가장 읽기 편한 상태 하나로 값을 고정한다. 01 은 다 자라서 또렷한 상태, 02·03 은 가려진 부분 없이 전부 드러난 상태, 04 는 가장 흐릿한 값이 아니라 완전히 선명한 값으로, 06 은 기울기 0, 07 은 최종 색, 08 은 그림자가 다 번진 뜬 상태, 09 는 글자 간격이 가장 넓게 벌어진 상태로 멈춘다. 05 만 예외로 뒷면이 아니라 앞면(rotateY 0deg)에 고정하는데, 뒤집힌 채 멈추면 라벨이 안 보이기 때문이다.

css 스크롤 애니메이션 효과는 어디서 더 볼 수 있나?

구간 값 대신 요소가 화면에 걸리는 순간을 감지해서 클래스를 붙이는 쪽을 보고 싶다면 CSS 스크롤 애니메이션 9종 — AOS·GSAP 없이 JS 0줄로 가 있다. 값을 직접 계산하는 엔진이 궁금하면 스크롤 트리거 애니메이션 9종 — GSAP 없이 바닐라 JS 엔진으로 를, 멈춘 뒤에도 값이 스스로 줄어드는 관성 계산이 궁금하면 관성 스크롤 9종 — 멈춘 뒤에도 값이 스스로 줄어드는 아홉 가지 방식 을 본다. 스크롤 애니메이션 css 를 이렇게 세 각도(구간 값·트리거 감지·관성 계산)로 나눠 보면 어떤 화면에 어떤 계산이 맞는지 고르기 쉽다.

구간 값 스크롤 애니메이션을 적용하기 전에 자주 묻는 질문

css 스크롤 애니메이션 효과는 라이브러리 없이도 되나요?

그렇다. 아홉 개 전부 animation-timeline: view() 와 animation-range 만으로 구간 값을 계산하고, 미지원 환경을 위한 짧은 폴백 스크립트 외에 외부 라이브러리는 쓰지 않는다.

스크롤 애니메이션 css 구간 값은 entry 와 cover 중 뭘 골라야 하나요?

요소가 화면에 들어오는 순간의 변화만 보여주고 싶으면 entry(01·02·03·06·07·09), 요소가 화면에 걸쳐 있는 내내 값이 바뀌길 바라면 cover(04·05·08)를 고른다. 같은 계산식이라도 구간을 바꾸면 언제 값이 0 에서 1 로 채워지는지가 통째로 달라진다.

구형 브라우저에서는 어떻게 보이나요?

CSS.supports('animation-timeline: view()') 로 지원 여부를 먼저 확인해, 지원하지 않으면 entry 항목은 교차 비율을, cover 항목은 스크롤 위치를 직접 계산해 같은 속성에 대입한다. 움직임이 사라지는 게 아니라 같은 값을 다른 방식으로 구하는 것뿐이다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.