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

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

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

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

아홉 가지 scroll indicator css 는 인기 순이 아니라 독자 눈에 먼저 걸리는 자리 순으로 놓았습니다. 화면 가장자리에서 의식하지 않아도 보이는 것(01~04) 다음, 본문 안에서 직접 눈에 들어오는 것(05~06), 마지막으로 글 구조 자체를 설명하는 것(07~09)입니다. 아홉 개 전부 animation-timeline: scroll() 하나로 스크립트 없이 움직이고, 이 기능을 아직 못 쓰는 브라우저를 위한 대체판은 함정 섹션에 있습니다. 진행률 대신 요소를 차례로 등장시키는 쪽을 찾는다면 순차적 애니메이션을 먼저 보는 편이 빠르고, 가로로 넘어가는 화면의 진행은 CSS 캐러셀이 맡습니다.

01상단 얇은 바

화면 맨 위 3px 바

화면 맨 위에 붙은 3px 바가 읽은 만큼 왼쪽부터 채워집니다.

상단 얇은 바
새 창에서 열기

01 · 상단 얇은 바 — 측정 누적면적 1.0% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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원형 링 배지

원형 프로그레스바 css 를 conic-gradient 로

화면 오른쪽 아래에 뜬 원형 배지가 conic-gradient 링으로 채워집니다.

원형 링 배지
새 창에서 열기

02 · 원형 링 배지 — 측정 누적면적 0.736% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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섹션 점 내비

지금 읽는 구간의 점만

세로로 늘어선 점 여섯 개 중 지금 읽는 구간의 점만 커지고 밝아집니다.

섹션 점 내비
새 창에서 열기

03 · 섹션 점 내비 — 측정 누적면적 0.683% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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사이드 세로 바

오른쪽 가장자리 세로 트랙

화면 오른쪽 가장자리 얇은 트랙이 위에서 아래로 채워져 읽은 위치를 보여줍니다.

사이드 세로 바
새 창에서 열기

04 · 사이드 세로 바 — 측정 누적면적 0.833% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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글자 채우기

읽은 비율만큼 글자 색 채우기

제목 글자 색이 읽은 비율만큼 왼쪽부터 채워집니다.

글자 채우기
새 창에서 열기

05 · 글자 채우기 — 측정 누적면적 2.938% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

background-clip:textlinear-gradient두 색

이미지 없이 되는 이유

background-clip: text로 그라데이션을 그리기 때문에 이미지 없이 순수 CSS만으로 구현됩니다.

@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까지 정수로

오른쪽 위 작은 숫자가 스크롤한 만큼 0에서 100까지 정수로 오릅니다.

숫자 %
새 창에서 열기

06 · 숫자 % — 측정 누적면적 0.872% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

@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챕터 스텝

챕터 원 네 개 중 하나

챕터 번호 원 네 개 중 지금 읽는 챕터의 원만 채워집니다.

챕터 스텝
새 창에서 열기

07 · 챕터 스텝 — 측정 누적면적 3.558% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

원 채움챕터 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목차 하이라이트

알약이 슬라이드로 따라붙는다

사이드 목차에서 지금 읽는 제목에만 옅은 색 알약이 슬라이드로 따라붙습니다.

목차 하이라이트
새 창에서 열기

08 · 목차 하이라이트 — 측정 누적면적 21.896% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateY 알약현재 항목만좌측 보더 3px

진한 단색을 버린 까닭

진한 단색 알약은 전환 중 글자 대비가 깨져서 14% 옅은 틴트로 바꿨습니다.

.toc__pill {
  position: absolute;
  background: rgba(47, 109, 246, .14);
  border-left: 3px solid $color;
  transition: transform 200ms ease-in-out;
}

09읽은 시간

숫자가 줄어드는 문구

"남은 N분" 문구의 숫자가 스크롤할수록 줄어듭니다.

읽은 시간
새 창에서 열기

09 · 읽은 시간 — 측정 누적면적 1.545% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

카운트다운분 단위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 문서에서 다시 확인할 수 있습니다.

steps(1) 만으로는 안 잡힌다

끝까지 읽었다는 신호를 축하로 바꾸는 컨페티 축하 애니메이션도 같은 함정을 안고 있어, 진행률이 100%에 닿는 순간을 잘못 잡으면 아무것도 없는 화면에서 터집니다.

Firefox처럼 아직 정식 지원이 없는 브라우저에서 9개 전부 CSS만으로 두면 진행 표시가 그냥 멈춰 있습니다. 그래서 zip의 react/ 판은 처음부터 window의 scroll 이벤트(또는 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

css only scroll indicator 를 못 쓰는 브라우저에서는 어떻게 하나요?

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

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

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

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

H2가 4개 미만이면 알약이 거의 움직이지 않아 효과가 안 보입니다. 저희는 H2 4개 이상인 글에만 08을 붙이고, 그보다 짧은 글에는 01 상단 바만 남깁니다. 막대 대신 점이나 목차로 현재 위치를 보여 주는 변형은 스크롤 인디케이터 9종에 따로 모았습니다.

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

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