04임의값 scroll-timeline
Tailwind는 `scroll-timeline` 전용 유틸이 없어 임의 속성 `[animation-timeline:scroll()]`으로 붙이는데, 브라우저마다 지원이 갈려 이 데모는 같은 진행률을 JS 루프로 안전하게 재현한다.
쓰임긴 글 읽기 진행바, 랜딩 스크롤 진행 인디케이터
@keyframes fill-loop임의 속성caniuse 확인

<main class="stage">
<div class="page">
<p class="page__label">읽은 만큼 채워지는 진행바</p>
<div class="track"><div class="track__fill is-demo"></div></div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
// 실제 스크롤에 물리는 animation-timeline:scroll() 은 브라우저마다 지원이 갈려
// (편브리프 표 참고) 데모에서 쓰지 않는다 — 같은 진행률을 2초 루프로 안전하게 재현한다.
$duration: 2000ms;
$easing: linear;
$color: $subject-blue;
// ── 데모 무대 ──────────────────────────────────────────
@include stage($stage-paper);
.page {
box-sizing: border-box;
width: min(300px, 88vw);
}
.page__label {
margin: 0 0 $sp-4;
color: $subject-ink;
font-weight: 600;
font-size: clamp(13px, 3.6vw, 15px);
line-height: 1.4;
font-family: inherit;
}
.track {
height: $sp-2;
border-radius: $r-pill;
background: rgba($subject-ink, .12);
overflow: hidden;
}
.track__fill {
height: 100%;
border-radius: inherit;
background: $color;
transform-origin: left center; // 규칙8: 진행바 왼쪽 채움
transform: scaleX(0);
}
// 데모: 진행률이 0→1 로 다 차고 나면 리셋해 2초마다 반복
.track__fill.is-demo {
animation: fill-loop $duration $easing infinite;
}
@keyframes fill-loop {
0% { transform: scaleX(0); }
90% { transform: scaleX(1); }
100% { transform: scaleX(1); }
}
// ── 접근성: 움직임 줄이기 설정이면 다 찬 상태로 고정 ──────
@media (prefers-reduced-motion: reduce) {
.track__fill.is-demo { animation: none; transform: scaleX(1); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.page {
box-sizing: border-box;
width: min(300px, 88vw);
}
.page__label {
margin: 0 0 16px;
color: #17141a;
font-weight: 600;
font-size: clamp(13px, 3.6vw, 15px);
line-height: 1.4;
font-family: inherit;
}
.track {
height: 8px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.12);
overflow: hidden;
}
.track__fill {
height: 100%;
border-radius: inherit;
background: #2f6df6;
transform-origin: left center;
transform: scaleX(0);
}
.track__fill.is-demo {
animation: fill-loop 2000ms linear infinite;
}
@keyframes fill-loop {
0% {
transform: scaleX(0);
}
90% {
transform: scaleX(1);
}
100% {
transform: scaleX(1);
}
}
@media (prefers-reduced-motion: reduce) {
.track__fill.is-demo {
animation: none;
transform: scaleX(1);
}
}import type { CSSProperties } from "react";
import styles from "./ArbitraryTimelineProgress.module.scss";
export type ArbitraryTimelineProgressProps = {
label?: string;
/** 진행률이 다 차는 시간, ms — 실제 scroll-timeline 대신 안전한 루프로 재현 */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ArbitraryTimelineProgress({
label = "A progress bar that fills as you read",
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: ArbitraryTimelineProgressProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.page} style={vars}>
<p className={styles.page__label}>{label}</p>
<div className={styles.track}>
<div className={styles.track__fill} />
</div>
</div>
);
}04 임의값 scroll-timeline — 측정 누적면적 1.661% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · Tailwind 애니메이션 유틸 9종(v4) — 한 줄 복붙으로
이 데모가 속한 글
Tailwind 애니메이션 유틸 9종(v4) — 한 줄 복붙으로Tailwind 애니메이션은 tailwind.config.js가 아니라 CSS 파일 안 @theme 블록에 --animate- 변수와 @keyframes를 같이 적어서 만드는 Tailwind v4의 새 방식입니다. 이 글의 데모 전부 보기


