CSS 패럴랙스 스크롤 9종 — AOS·GSAP 없이, JS 0줄
CSS 패럴랙스 스크롤 아홉 개를 모았습니다. 레이어마다 이동 속도를 다르게 주는 원리 하나로 배경이 내용보다 느리게 움직이는 착시를 만듭니다. 격자에서는 아홉 개가 2초 간격으로 자동 반복되니 먼저 눈으로 훑고 마음에 드는 것만 zip으로 받으면 됩니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 배경 느리게 이동
- 02 3레이어 깊이감
- 03 텍스트가 이미지 위로 밀림
- 04 히어로 축소 패럴랙스
- 05 가로 타임라인 패럴랙스
- 06 카드 깊이 레이어
- 07 스티키+패럴랙스 조합
- 08 마우스 이동 패럴랙스
- 09 스크롤 줌아웃
아홉 개는 인기순이 아니라 패럴랙스가 실제로 쓰이는 배치 순서로 놓았습니다. 01~02는 배경 레이어 하나 또는 여러 개가 콘텐츠보다 느리게 움직이는 가장 기본형이고, 03~05는 텍스트·히어로·가로 카드처럼 콘텐츠 자체가 움직이는 응용입니다. 06~09는 카드 쌓임·스티키 고정·마우스 연동·줌아웃처럼 다른 CSS 패턴과 패럴랙스를 섞은 것이라 맨 뒤에 뒀습니다.
01배경 느리게 이동
배경 레이어 하나만 scroll(root) 타임라인에 걸어 스크롤보다 느리게 위로 이동시키고, 앞의 글자는 그대로 둡니다. 둘 사이 속도 차이가 벌어질수록 깊이가 더 또렷하게 보입니다.
.bg {
@supports (animation-timeline: scroll()) {
animation: drift linear both;
animation-timeline: scroll(root);
}
}
@keyframes drift {
from { transform: translateY(0); }
to { transform: translateY(-24px); }
}
023레이어 깊이감
뒤·중간·앞 세 레이어를 같은 scroll(root) 타임라인에 걸되 이동량만 다르게 줍니다. 뒤 레이어일수록 적게 움직이고 옅게 칠해 카메라에서 먼 것처럼 보이게 합니다.
// @supports (animation-timeline: scroll()) 로 감싸는 건 01 과 같다
.back { animation: drift-back linear both; animation-timeline: scroll(root); }
.mid { animation: drift-mid linear both; animation-timeline: scroll(root); }
.front { animation: drift-front linear both; animation-timeline: scroll(root); }
@keyframes drift-back { from { transform: translateY(0); } to { transform: translateY(-10px); } }
@keyframes drift-mid { from { transform: translateY(0); } to { transform: translateY(-26px); } }
@keyframes drift-front { from { transform: translateY(0); } to { transform: translateY(-46px); } }
03텍스트가 이미지 위로 밀림
이미지 블록은 거의 고정에 가깝게 조금만 움직이고, 그 위 제목만 훨씬 큰 폭으로 움직여 이미지 경계를 가로질러 지나갑니다. mix-blend-mode: difference로 겹치는 순간 글자 색이 바뀝니다.
.image { animation: image-drift linear both; animation-timeline: scroll(root); }
.text {
mix-blend-mode: difference;
animation: text-drift linear both;
animation-timeline: scroll(root);
}
@keyframes image-drift { from { transform: translateY(0); } to { transform: translateY(-14px); } }
@keyframes text-drift { from { transform: translateY(60px); } to { transform: translateY(-70px); } }
04히어로 축소 패럴랙스
히어로 제목이 스크롤하는 동안 scale과 opacity를 함께 줄여 다음 섹션에 자리를 내줍니다. 크기와 투명도를 같이 바꾸면 사라지는 방향이 또렷해집니다.
.title {
animation: shrink linear both;
animation-timeline: scroll(root);
}
@keyframes shrink {
from { transform: scale(1); opacity: 1; }
to { transform: scale(.55); opacity: .25; }
}
05가로 타임라인 패럴랙스
세로로 스크롤하는 동안 가로로 나열된 카드 줄을 translateX로 왼쪽으로 흘려보냅니다. 세로 스크롤 하나가 가로 이동을 만든다는 걸 보여주는 예입니다.
.track {
display: flex;
gap: 14px;
animation: travel linear both;
animation-timeline: scroll(root);
}
@keyframes travel {
from { transform: translateX(0); }
to { transform: translateX(-120px); }
}
06카드 깊이 레이어
겹쳐 쌓인 카드 세 장이 각자 다른 속도로 밀려나며 펼쳐집니다. 맨 앞 카드가 가장 멀리·가장 빠르게 움직이고 뒤 카드일수록 적게 움직이며 살짝 커집니다.
.card--1 { animation: peel-1 linear both; animation-timeline: scroll(root); }
.card--2 { animation: peel-2 linear both; animation-timeline: scroll(root); }
.card--3 { animation: peel-3 linear both; animation-timeline: scroll(root); }
@keyframes peel-1 { from { transform: translateY(0) scale(1); } to { transform: translateY(-34px) scale(1); } }
@keyframes peel-2 { from { transform: translateY(9px) scale(.94); } to { transform: translateY(-25px) scale(1); } }
@keyframes peel-3 { from { transform: translateY(18px) scale(.88); } to { transform: translateY(-30px) scale(1); } }
07스티키+패럴랙스 조합
앞 문장은 position: sticky로 화면에 붙박이로 두고 뒤 배경만 그 위로 계속 흘러가게 합니다. 고정된 것과 움직이는 것이 한 화면에 같이 있어야 패럴랙스라는 걸 보여줍니다.
.caption { position: sticky; top: 40%; }
.bg {
animation: bg-drift linear both;
animation-timeline: scroll(root);
}
@keyframes bg-drift {
from { transform: translateY(0); }
to { transform: translateY(-40px); }
}
08마우스 이동 패럴랙스
이 항목만 스크롤이 아니라 커서 위치로 움직입니다. pointermove로 받은 좌표를 CSS 변수로 넘기고 레이어마다 배율만 다르게 곱해 입체감을 만듭니다.
// JS (10줄 이내): pointermove 로 커서 위치를 CSS 변수로
el.addEventListener('pointermove', function (e) {
var r = el.getBoundingClientRect();
el.style.setProperty('--mx', ((e.clientX - r.left) / r.width - 0.5).toFixed(3));
el.style.setProperty('--my', ((e.clientY - r.top) / r.height - 0.5).toFixed(3));
});
// SCSS: 레이어마다 --depth 로 배율만 다르게
.layer {
transform: translate(calc(var(--mx, 0) * var(--depth) * 22px), calc(var(--my, 0) * var(--depth) * 22px));
}
09스크롤 줌아웃
확대돼 있던 미디어 블록이 스크롤할수록 scale이 줄어들며 주변 레이아웃이 드러납니다. 시작 배율을 크게 줄수록 열리는 느낌이 강해집니다.
.media {
transform: scale(1.45);
animation: zoom-out linear both;
animation-timeline: scroll(root);
}
@keyframes zoom-out {
from { transform: scale(1.45); }
to { transform: scale(1); }
}
어디서 깨지는가 — 패럴랙스의 함정 셋
첫 번째는 animation-timeline: scroll() 자체의 지원 범위입니다. 이 편의 아홉 개는 모두 scroll(root) 하나로 실제 스크롤에 반응하는데, Chrome·Edge는 115부터, Safari는 26부터 정식 지원하고 Firefox는 2026년 9월 현재 정식판이 아니라 플래그 뒤에 있습니다. caniuse의 최신 지원표를 확인한 뒤 쓰는 걸 권하고, 아홉 개 파일 모두 @supports not (animation-timeline: scroll())로 미지원 브라우저에서는 그냥 정지 화면으로 보이게 했습니다.
두 번째는 레이어 쌓임 순서입니다. 02·06처럼 레이어가 여러 장 겹치면 z-index를 정하지 않아도 마크업에 나중에 쓴 요소가 위로 올라오는데, 뒤 레이어를 나중에 추가하면 앞 레이어를 가려 패럴랙스가 아니라 정지 화면처럼 보입니다. 이 zip의 02·06은 뒤에서 앞 순서로 마크업을 쓰고 필요한 곳만 z-index: 1을 더했습니다.
세 번째는 어지럼증입니다. 레이어가 서로 다른 속도로 움직이면 전정기관에 예민한 사람에게는 뱃멀미 같은 불편함을 줄 수 있어서, 09처럼 배율 변화가 큰 항목일수록 이동 폭을 과하게 키우지 않는 게 안전합니다. 아홉 개 모두 실제로 렌더해 이동 폭을 눈으로 맞춘 뒤 압축했고, 이 zip의 비밀번호는 x6462unj입니다.
접근성
"동작 줄이기"를 켠 환경에서는 prefers-reduced-motion: reduce 조건이 걸려 아홉 개 모두 움직임이 멎고, 굳이 애니메이션 없이도 뜻이 통하는 마지막 화면만 바로 나타납니다. 01·02·05·07은 레이어가 시작 위치에 멈추고, 03·04·09는 다 이동한 뒤의 모습으로 고정되며, 06은 카드가 펼쳐지지 않은 채 겹친 그대로 남고, 08은 커서를 움직여도 레이어가 따라오지 않습니다. 세부 조건은 MDN prefers-reduced-motion에서 볼 수 있습니다.
패럴랙스 말고 다른 스크롤 효과는 CSS 애니메이션 카테고리에서 더 볼 수 있고, 사이트 운영 방식은 소개 페이지에 정리돼 있습니다.
FAQ
패럴랙스에 왜 top·left 대신 transform을 쓰나요?
top이나 left를 바꾸면 브라우저가 레이아웃을 다시 계산해야 해서 스크롤할 때마다 버벅일 수 있습니다. transform은 레이아웃 단계를 건너뛰고 합성 레이어에서만 처리되기 때문에 프레임이 덜 떨어집니다. 이 편의 아홉 개는 모두 translate·scale·opacity만 애니메이션합니다.
패럴랙스가 어지럼증을 유발할 수 있나요?
레이어 간 속도 차이가 크거나 화면 전체가 흔들리듯 움직이면 전정기관이 예민한 사용자에게 불편할 수 있습니다. 그래서 이 편은 이동 폭을 24~120px 사이로 제한했고, prefers-reduced-motion에서는 전부 정지 화면으로 남게 했습니다.
React 컴포넌트에서도 실제 스크롤 연동이 되나요?
네, 됩니다. animation-timeline은 JS가 아니라 CSS 엔진이 계산하므로 react/ 폴더의 컴포넌트도 SCSS 모듈 안에 같은 scroll(root) 규칙만 두면 됩니다. 08번(마우스 이동)만 좌표를 읽는 로직이 필요해 pointermove 핸들러를 useCallback으로 감쌌습니다.