GODRICH

홈페이지 스크롤 효과 9가지 — 애플 페이지처럼 되는 CSS

홈페이지 스크롤 효과는 페이지를 내리는 손가락 하나에 화면이 반응하게 만드는 장치로, 애플 제품 페이지가 이 분야의 기준입니다. 자바스크립트 라이브러리 하나도 없이 CSS만으로 가로 패널·고정 장면 교체·단어 채움·배경색 전환까지 아홉 가지를 실제로 돌려본 코드로 모았습니다.

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

아홉 개는 화면에 나오는 순서입니다. 페이지를 열자마자 보이는 것(01)에서 시작해, 읽던 본문이 끝나며 만나는 것(02), 본문 흐름 속에서 카드가 쌓이고 문장이 차오르는 것(03~07), 그리고 목록이 반응하거나 한 장이 공개되는 마무리(08~09) 순서입니다. 애플식 페이지를 위에서 아래로 한 번 훑는 길과 같습니다.

01가로로 넘어가는 이야기 패널

세로로 스크롤하면 고정된 화면 안에서 패널이 가로로 넘어가는, 애플 제품 페이지의 대표 구성입니다. scroll-timeline이 스크롤 상자의 위치를 그대로 읽어 트랙을 밀고, position: sticky가 화면을 고정합니다. 제품 특징 소개나 포트폴리오 스토리에 씁니다.

animation-timeline: scroll()scroll-timeline: --hp-scrollposition: sticky
.hp__scroller {
  overflow-y: auto;
  scroll-timeline: --hp-scroll block;
}
.hp__inner { height: 300%; }          // 스티키 영역이 화면 3번 깊이
.hp__pin {
  position: sticky;
  top: 0;
  height: calc(100% / 3);
  overflow: hidden;
}
.hp__track {
  display: flex;
  width: 300%;                        // 패널 3장 = 화면의 300%
  animation: hp-slide linear both;
  animation-timeline: --hp-scroll;
}
@keyframes hp-slide {
  from { transform: translateX(0); }
  to   { transform: translateX(-66.6667%); } // 두 판 너비 ÷ 트랙 폭
}

02끝에서 드러나는 커튼 푸터

본문 패널이 푸터 위를 지나가다가, 스크롤 끝에서 비켜주며 푸터를 커튼처럼 드러냅니다. 푸터는 문서 맨 끝에 두되 position: sticky; bottom: 0으로 뷰포트 바닥에 미리 붙여 둡니다. 연락처·저작권 푸터나 마무리 인사에 어울립니다.

z-indexposition: stickySafari에서도 그대로
.fc__main {
  position: relative;
  z-index: 1;                    // 본문이 푸터 위를 지나간다
  min-height: calc(100% + 64px); // 시작부터 푸터를 완전히 덮는다
  margin-bottom: 64px;           // 스크롤 끝에서 푸터 칸이 열린다
  background: #fff;
}
.fc__foot {
  position: sticky;
  bottom: 0;
  z-index: 0;
  height: 64px;
  background: linear-gradient(120deg, #b9f0d8, #ffd6e8);
}

03겹쳐 쌓이는 카드

카드마다 position: sticky가 걸려 있어 다음 카드가 와서 위에 포개지고, 지나간 카드는 자리에 쌓인 채 남습니다. top 값을 한 칸씩 깊어지게 주면 포개지는 간격이 생깁니다. 서비스 소개 단계나 요금제 비교 카드에 씁니다.

position: stickytop 간격JS 0줄
.sk__card {
  position: sticky;
  min-height: 40px;
  margin-bottom: 8px;
  border-radius: 16px;
  box-shadow: 0 6px 12px rgba(23, 20, 26, .32);
}
.sk__card--1 { top: 8px;  background: #ffd6e8; }
.sk__card--2 { top: 16px; background: #d3ecff; }
.sk__card--3 { top: 24px; background: #e4f7d9; }
.sk__card--4 { top: 32px; background: #fff3c9; }

04고정 화면 장면 교체

화면은 제자리에 고정한 채 스크롤한 만큼 장면이 서로 번갈아 부드럽게 바뀌는, 스크롤텔링의 핵심 동작입니다. 장면 셋이 스크롤 타임라인 한 줄을 공유하고 키프레임 구간으로 나눠 갖습니다. 사용법 3단계 안내나 제품 스토리에 씁니다.

animation-timeline: scroll()키프레임 구간opacity
.ps__scroll {
  overflow-y: auto;
  scroll-timeline: --ps-scroll block;
}
.ps__pin {
  position: sticky;
  top: 0;
  height: calc(100% / 3);
  display: grid;
}
.ps__scene--1 { animation: ps-1 linear both; animation-timeline: --ps-scroll; }
.ps__scene--2 { animation: ps-2 linear both; animation-timeline: --ps-scroll; }
.ps__scene--3 { animation: ps-3 linear both; animation-timeline: --ps-scroll; }
@keyframes ps-1 { 0% { opacity: 1; } 33%, 100% { opacity: 0; } }
@keyframes ps-2 { 0%, 30% { opacity: 0; } 38%, 62% { opacity: 1; } 70%, 100% { opacity: 0; } }
@keyframes ps-3 { 0%, 64% { opacity: 0; } 74%, 100% { opacity: 1; } }

05단어마다 차오르는 문장

문장의 단어가 스크롤 위치를 따라 하나씩 선명해지는, 애플 소개 문장에서 볼 수 있는 효과입니다. 단어마다 각자의 view() 타임라인이 붙어서, 따로 시간차를 주지 않아도 지오메트리가 저절로 순서를 만듭니다. 랜딩 첫 문장이나 브랜드 선언 문구에 씁니다.

animation-range 겹침view()글자색만
.wf__w {
  color: rgba(23, 20, 26, .16);   // 옅은 상태가 기본
  animation: wf-in linear both;
  animation-timeline: view();     // 단어 각자의 타임라인
  animation-range: entry 0% cover 42%;
}
@keyframes wf-in {
  from { color: rgba(23, 20, 26, .16); }
  to   { color: #17141a; }
}

06형광펜이 그어지는 강조

스크롤이 닿으면 강조할 말 뒤로 형광펜 색이 왼쪽에서 오른쪽으로 그어지듯 채워집니다. 글자는 그대로 두고 배경 그라디언트의 폭(background-size)만 자라게 하는 것이 핵심입니다. 섹션 제목 강조나 세일 문구에 씁니다.

background-sizeview()글자 안 건드림
.mk__hl {
  background: linear-gradient(#2f6df6, #2f6df6) no-repeat 0 62%;
  background-size: 0% 58%;
  padding: 0 4px;
  animation: mk-sw linear both;
  animation-timeline: view();
  animation-range: entry 30% cover 60%;
}
@keyframes mk-sw {
  from { background-size: 0% 58%; }
  to   { background-size: 100% 58%; }
}

07구간마다 바뀌는 배경색

페이지를 내리는 동안 배경색이 구간마다 부드럽게 바뀌어 장이 바뀌는 느낌을 줍니다. 배경 레이어를 position: sticky로 뒤에 붙이고, 스크롤 타임라인이 색 키프레임을 돌립니다. 서사형 랜딩이나 루틴·요금 안내에 씁니다.

animation-timeline: scroll()background-colorroot 타임라인
.ts__scroll {
  overflow-y: auto;
  scroll-timeline: --ts-scroll block;
}
.ts__bg {
  position: sticky;
  top: 0;
  height: 100%;
  margin-bottom: -100%;  // 섹션이 배경 위로 오게 끌어올린다
  animation: ts-bg linear both;
  animation-timeline: --ts-scroll;
}
@keyframes ts-bg {
  0%   { background-color: #fff7e6; }
  50%  { background-color: #b8f0d0; }
  100% { background-color: #ff9db8; }
}

08스크롤 속도에 기우는 목록

스크롤을 빨리 하면 목록 카드가 그 속도만큼 옆으로 기울었다가, 멈추면 제자리로 돌아옵니다. 속도를 재는 짧은 스크립트가 --vel 변수에 값을 넣고 기울기 계산은 CSS calc가 맡습니다. 갤러리 피드나 뉴스 목록에 씁니다.

JS 10줄--vel 변수skewY
var box = document.querySelector('.feed');
var last = box.scrollTop, vel = 0;
box.addEventListener('scroll', function () {
  vel = Math.max(-24, Math.min(24, box.scrollTop - last));
  last = box.scrollTop;
});
(function decay() {
  vel *= 0.86;
  box.style.setProperty('--vel', vel.toFixed(2));
  requestAnimationFrame(decay);
})();
.feed__row {
  transform: skewY(calc(var(--vel, 0) * -0.18deg));
}

09스포트라이트 서클

고정된 화면에서 원형 창이 스크롤에 맞춰 점점 커지며 감춰둔 이미지를 드러냅니다. 밝은 화면을 위에 얹고 clip-path: circle()의 반지름을 스크롤 타임라인으로 키웁니다. 제품 공개 순간이나 아티스트 소개에 씁니다.

clip-path: circle()scroll-timeline: --sl-scroll이미지 공개
.sl__spot {
  grid-area: 1 / 1;
  background: linear-gradient(135deg, #2f6df6, #8fb4ff);
  clip-path: circle(14% at 50% 50%);
  animation: sl-open linear both;
  animation-timeline: --sl-scroll;
}
@keyframes sl-open {
  from { clip-path: circle(14% at 50% 50%); }
  to   { clip-path: circle(75% at 50% 50%); }
}

어디서 깨지는가 — 함정 하나

가장 많이 걸린 함정은 position: sticky가 흐름 안에서 제 자리를 차지한다는 점이었습니다. 02번을 처음 만들 때 푸터를 본문 앞에 두었더니 스크롤 끝까지 내려도 푸터가 한 번도 나타나지 않았습니다. 푸터를 문서 맨 끝으로 옮기고 본문에 margin-bottom을 줘서야 비로소 본문이 푸터 위를 지나가다 끝에서 비켜주는 그림이 나왔습니다. 07번도 같은 병을 앓았습니다. 배경 레이어를 sticky로 두었더니 배경이 흐름에서 화면 한 칸을 통째로 차지해서 섹션 칩들이 화면 밑에 밀려 캡처에 한 번도 잡히지 않았고, margin-bottom: -100%로 끌어올린 뒤에야 배경 위에 올라앉았습니다. 또 하나, 이 편의 스크롤 타임라인(scroll()·view())은 설치된 크롬에서 직접 렌더해 확인했지만 파이어폭스·사파리는 아직 지원하지 않아서, 지원하지 않는 브라우저에서는 05의 문장이 처음부터 선명하고 09의 원이 처음부터 열려 있는 상태로 고정됩니다. 그래서 스크롤 타임라인을 쓰는 여섯 개는 @supports not이나 기본값으로 최종 모습을 보여 주고, 스크롤 자체는 어떤 브라우저에서도 막히지 않게 했습니다. 아홉 폴더를 이렇게 다시 확인한 뒤 압축했고, ry7akxw2을 안내 문장 그대로 입력하면 이 결과물이 통째로 열립니다.

접근성

아홉 개 모두 prefers-reduced-motion: reduce에서 자동으로 움직이는 부분이 꺼집니다. 05의 단어는 처음부터 진한 색으로, 06의 형광펜은 처음부터 다 그어진 상태로, 09의 원은 활짝 열린 상태로 고정되고, 08은 속도 반응 자체를 잠가 카드가 기울지 않게 했습니다. 스크롤 상자 안은 탭 키로 요소를 차례로 방문할 수 있고, 04처럼 화면이 고정되는 구성에서도 건너뛸 수 있는 다음 섹션이 그 아래 그대로 이어져 키보드로 끝까지 읽을 수 있습니다.

FAQ

스크롤 애니메이션을 넣으면 사이트가 느려지나요?

01·04·07·09의 스크롤 타임라인과 02·03의 스티키는 브라우저가 렌더 단계에서 직접 처리해서, 매 프레임 자바스크립트가 계산하는 방식보다 오히려 가볍습니다. 08만 자바스크립트가 매 프레임 값을 갱신하지만 하는 일은 변수 하나에 숫자를 넣는 것뿐이라 부담이 작습니다.

파이어폭스나 사파리에서는 어떻게 보이나요?

01·04·05·06·07·09는 스크롤 타임라인을 쓰지 못하는 브라우저에서 각각 최종 모습으로 고정됩니다. 패널 1이 보이고, 문장은 선명하고, 형광펜은 그어져 있고, 원은 열려 있습니다. 02·03·08은 이 기능과 무관해서 어느 브라우저에서도 같게 동작합니다.

아이폰에서도 되나요?

02·03의 스티키와 08의 속도 반응은 아이폰 사파리에서도 그대로 됩니다. 스크롤 타임라인을 쓰는 나머지는 아이폰 사파리가 아직 지원하지 않아 최종 모습으로 보입니다. 다만 화면 구조 자체는 그대로라 내용을 읽는 데는 지장이 없습니다.

스크롤 하위 효과는 CSS 애니메이션 카테고리에 더 있고, 시차를 주는 패럴랙스 모음과 헤더가 반응하는 스티키 헤더 모음이 이 글과 이어집니다. animation-timeline의 정식 명세는 MDN animation-timeline 문서에서, position: sticky의 동작은 MDN position 문서에서 확인할 수 있습니다.

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

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