04任意値scroll-timeline
Tailwind には `scroll-timeline` 専用ユーティリティがなく、任意プロパティ `[animation-timeline:scroll()]` で付ける。ブラウザごとに対応が割れるため、このデモは同じ進捗を安全な JS ループで再現する。
使いどころ長文記事の読了プログレスバー、LPのスクロール進捗インジケーター
@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 オリジナル · SCSS と React は zip に同梱 · Tailwind アニメーション9選(v4) 一行でコピペOK
このデモが属する記事
Tailwind アニメーション9選(v4) 一行でコピペOKtailwind アニメーションは、tailwind.config.jsを使わずCSSの@themeブロックに--animate-変数と@keyframesを一緒に書くと、その名前がそのままユーティリティクラスになるTailwind v4の この記事のデモをすべて見る


