GODRICH

CSS 프로그레스바 애니메이션 9종 — 스크롤 읽기 진행 표시

CSS 프로그레스바 애니메이션은 지금까지 얼마나 읽었는지를 스크롤 위치만으로 계산해 보여주는 장치입니다. AOS·GSAP 없이, JS 0줄로 아홉 가지 형태를 모았습니다 — 상단 바, 원형 배지, 점 내비, 세로 바, 글자 채우기, 숫자, 챕터 스텝, 목차, 읽은 시간까지.

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

아홉 가지는 인기 순이 아니라 독자 눈에 먼저 걸리는 자리 순으로 놓았습니다. 화면 가장자리에서 의식하지 않아도 보이는 것(01~04) 다음, 본문 안에서 직접 눈에 들어오는 것(05~06), 마지막으로 글 구조 자체를 설명하는 것(07~09)입니다. 아홉 개 전부 animation-timeline: scroll() 하나로 스크립트 없이 움직이고, 이 기능을 아직 못 쓰는 브라우저를 위한 대체판은 함정 섹션에 있습니다.

01상단 얇은 바

화면 맨 위에 붙은 3px 바가 읽은 만큼 왼쪽부터 채워집니다. 긴 글이나 문서 페이지 헤더 아래에 가장 흔히 씁니다.

scroll()3px왼쪽 정렬
.bar {
  width: 100%;
  transform: scaleX(0);
  transform-origin: left;

  @supports (animation-timeline: scroll()) {
    animation: fill linear both;
    animation-timeline: scroll(root);
  }
}
@keyframes fill { to { transform: scaleX(1); } }

02원형 링 배지

화면 오른쪽 아래에 뜬 원형 배지가 conic-gradient 링으로 채워집니다. 플로팅 위로가기 버튼과 묶어 쓰면 버튼 하나가 내비게이션과 진행 표시를 겸합니다.

conic-gradient@property고정 위치
@property --p { syntax: '<number>'; initial-value: 0; inherits: false; }

.badge {
  --p: 0;
  border-radius: 50%;
  background: conic-gradient($color calc(var(--p) * 1%), $stage-paper 0);

  @supports (animation-timeline: scroll()) {
    animation: fill-ring linear both;
    animation-timeline: scroll(root);
  }
}
@keyframes fill-ring { from { --p: 0; } to { --p: 100; } }

03섹션 점 내비

세로로 늘어선 점 여섯 개 중 지금 읽는 구간의 점만 커지고 밝아집니다. 원페이지 스토리텔링 사이트나 랜딩 섹션 내비에 씁니다.

scale 1.6배점 6개세로 정렬
const io = new IntersectionObserver((entries) => {
  const top = entries
    .filter((e) => e.isIntersecting)
    .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
  if (top) setActive(sections.indexOf(top.target));
}, { threshold: [0.3, 0.6] });
sections.forEach((el) => io.observe(el));

04사이드 세로 바

화면 오른쪽 가장자리 얇은 트랙이 위에서 아래로 채워져 읽은 위치를 보여줍니다. 문서 사이트나 긴 기술 블로그의 여백에 붙습니다.

scaleY우측 4px고정
.rail__fill {
  height: 100%;
  transform: scaleY(0);
  transform-origin: top;

  @supports (animation-timeline: scroll()) {
    animation: fill-y linear both;
    animation-timeline: scroll(root);
  }
}
@keyframes fill-y { to { transform: scaleY(1); } }

05글자 채우기

제목 글자 색이 읽은 비율만큼 왼쪽부터 채워집니다. background-clip: text로 그라데이션을 그리기 때문에 이미지 없이 순수 CSS만으로 구현됩니다.

background-clip:textlinear-gradient두 색
@property --p { syntax: '<percentage>'; initial-value: 0%; inherits: false; }

.headline {
  --p: 0%;
  background-image: linear-gradient(90deg, $color var(--p), $dim var(--p));
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

06숫자 %

오른쪽 위 작은 숫자가 스크롤한 만큼 0에서 100까지 정수로 오릅니다. 긴 설문이나 튜토리얼의 진행률에 씁니다.

@property정수 보간% 붙이기
@property --num { syntax: '<integer>'; initial-value: 0; inherits: false; }

.counter__num {
  --num: 0;
  counter-reset: num var(--num);
  &::before { content: counter(num) '%'; }

  @supports (animation-timeline: scroll()) {
    animation: count-up linear both;
    animation-timeline: scroll(root);
  }
}
@keyframes count-up { from { --num: 0; } to { --num: 100; } }

07챕터 스텝

챕터 번호 원 네 개 중 지금 읽는 챕터의 원만 채워집니다. 튜토리얼 시리즈나 다단계 가이드 글에 씁니다.

원 채움챕터 4개scale 1.25배
@keyframes fill-2 {
  0%, 25%    { background: transparent; transform: scale(1); }
  33%        { background: $color; transform: scale(1.25); }
  40%, 100%  { background: $color; transform: scale(1); }
}
.step:nth-child(2) .step__circle { animation: fill-2 linear both; }

08목차 하이라이트

사이드 목차에서 지금 읽는 제목에만 옅은 색 알약이 슬라이드로 따라붙습니다. 진한 단색 알약은 전환 중 글자 대비가 깨져서 14% 옅은 틴트로 바꿨습니다.

translateY 알약현재 항목만좌측 보더 3px
.toc__pill {
  position: absolute;
  background: rgba(47, 109, 246, .14);
  border-left: 3px solid $color;
  transition: transform 200ms ease-in-out;
}

09읽은 시간

"남은 N분" 문구의 숫자가 스크롤할수록 줄어듭니다. 남은 분은 전체 분량에서 읽은 비율만큼 뺀 값입니다.

카운트다운분 단위scroll() 연동
@property --min { syntax: '<integer>'; initial-value: 12; inherits: false; }

.time__num {
  --min: 12;
  counter-reset: min var(--min);
  &::before { content: counter(min); }
}
@keyframes count-down { from { --min: 12; } to { --min: 0; } }

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

animation-timeline: scroll()의 지원이 아직 갈립니다. 실측(2026-09-10)은 이렇습니다.

기능 Chrome·Edge Safari Firefox
animation-timeline scroll() 115+ 26+ (2025-09) 158+, 2026-09 현재 정식판은 플래그 뒤
@property 85+ 16.4+ 128+

두 기능의 최신 지원 범위는 MDN animation-timeline 문서와 MDN @property 문서에서 다시 확인할 수 있습니다.

Firefox처럼 아직 정식 지원이 없는 브라우저에서 9개 전부 CSS만으로 두면 진행 표시가 그냥 멈춰 있습니다. 그래서 zip의 react/ 판은 처음부터 windowscroll 이벤트(또는 03·08처럼 섹션 단위면 IntersectionObserver)로 같은 값을 계산해 넘기는 JS 대체판을 함께 담았습니다 — CSS가 못 움직이는 브라우저에서도 React 컴포넌트는 그대로 움직입니다. 코드를 실제로 렌더해 보며 겪은 함정도 하나 있습니다. 07 챕터 스텝을 순간 전환(steps(1))으로만 만들었더니 프레임 사이 변화가 있는 구간이 네 장뿐이어서 측정 스크립트가 "거의 안 움직인다"고 판정했습니다. 전환 지점에 scale(1.25) 팝을 넣어 구간을 벌리자 그제서야 눈에 보이는 움직임이 됐습니다. 이 값들이 이미 반영된 아홉 개 파일의 압축 비밀번호는 3rmg234a이고, 아래에서 zip 하나로 받을 수 있습니다.

접근성

아홉 개 모두 prefers-reduced-motion: reduce에서 애니메이션이 꺼집니다. 01·02·04·05·06·07·09처럼 누적 진행을 채워 보여주는 항목은 진행률이 0인 상태가 아니라 다 읽은 최종 상태로 고정되고, 03 점 내비와 08 목차처럼 지금 보는 구간 하나만 켜서 보여주는 항목은 첫 항목이 켜진 상태로 고정됩니다. 06 숫자와 09 읽은 시간처럼 counter()로 그린 숫자는 스크린 리더가 아예 못 읽으므로, 숫자 자체는 aria-hidden으로 숨기고 감싸는 요소에 role="status" 또는 role="progressbar"aria-valuenow를 따로 붙였습니다. 08 목차처럼 지금 읽는 제목을 가리키는 항목은 실제 <a href="#섹션id">라서 키보드로도 그대로 도달할 수 있습니다. 03 점 내비의 점은 장식이라 aria-hidden으로 숨겨져 있어 스크린 리더나 키보드 이동의 대상이 아닙니다. prefers-reduced-motion 미디어 쿼리 자체의 지원 범위는 MDN 문서에서 확인할 수 있습니다.

다른 스크롤 애니메이션과 CSS 주제는 CSS 애니메이션 카테고리에 모여 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다.

FAQ

scroll() 타임라인을 못 쓰는 브라우저에서는 어떻게 하나요?

@supports (animation-timeline: scroll())로 감싸 두었기 때문에 지원이 없으면 그 안의 애니메이션은 그냥 적용되지 않습니다. zip의 react/ 판은 처음부터 scroll 이벤트로 같은 값을 계산하므로, CSS 대신 이 React 컴포넌트를 쓰면 Firefox를 포함한 모든 브라우저에서 움직입니다.

아홉 개 중 어떤 걸 같이 써도 되나요?

01 상단 바와 04 사이드 바처럼 위치가 겹치지 않는 것끼리는 함께 둬도 됩니다. 다만 06 숫자와 09 읽은 시간처럼 같은 진행 정보를 서로 다른 숫자로 보여주는 것끼리는 하나만 고릅니다. 둘 다 켜면 어느 숫자를 봐야 할지 헷갈립니다.

목차 항목이 몇 개일 때부터 08을 쓰는 게 좋나요?

H2가 4개 미만이면 알약이 거의 움직이지 않아 효과가 안 보입니다. 저희는 H2 4개 이상인 글에만 08을 붙이고, 그보다 짧은 글에는 01 상단 바만 남깁니다.

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

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