03Pause/resume state-value switch — the progress value stops changing and starts again each time the state value toggles between paused and running
When the state value switches to paused, the progress value stops right where it is, and when it switches back to running, it continues rising from that same point.
Best forA large installer download, a mobile data-saving download screen
.dpr__stateAUTO_PAUSE_AT.dpr__fill

<main class="stage">
<div class="dpr is-demo" id="dpr">
<div class="dpr__head">
<span class="dpr__state" id="dprState">In progress</span>
<span class="dpr__pct" id="dprPct">0%</span>
</div>
<div class="dpr__track"><div class="dpr__fill" id="dprFill"></div></div>
<button class="dpr__btn" id="dprBtn" type="button">Pause</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 경과 시간 = 정지 전 누적값 + (지금 - 재개 시각) — index.html elapsed() 참조
@include stage($stage-orange);
.dpr {
width: 100%;
max-width: 240px;
display: grid;
gap: $sp-3;
}
.dpr__head {
display: flex;
align-items: baseline;
justify-content: space-between;
font: 600 14px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: $subject-ink;
}
.dpr--paused .dpr__state { text-decoration: underline; text-decoration-thickness: 2px; }
.dpr__pct { font-variant-numeric: tabular-nums; color: $subject-ink; }
.dpr__track {
width: 100%;
height: $sp-3;
border-radius: $r-pill;
background: rgba($subject-ink, .16);
overflow: hidden;
}
.dpr__fill {
height: 100%;
width: 100%;
transform-origin: left center;
transform: scaleX(0);
border-radius: $r-pill;
background: $subject-ink;
}
.dpr--paused .dpr__fill { background: $subject-orange-deep; }
.dpr__btn {
width: 100%;
border: 0;
border-radius: $r-control;
padding: $sp-2 $sp-4;
background: $subject-ink;
color: $subject-cream;
font: 700 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
cursor: pointer;
transition: transform $dur-instant $ease-out;
&:active { transform: scale(.96); }
}
@media (max-width: 340px) {
.dpr { max-width: 200px; }
}
@media (prefers-reduced-motion: reduce) {
.dpr__btn { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.dpr {
width: 100%;
max-width: 240px;
display: grid;
gap: 12px;
}
.dpr__head {
display: flex;
align-items: baseline;
justify-content: space-between;
font: 600 14px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #17141a;
}
.dpr--paused .dpr__state {
text-decoration: underline;
text-decoration-thickness: 2px;
}
.dpr__pct {
font-variant-numeric: tabular-nums;
color: #17141a;
}
.dpr__track {
width: 100%;
height: 12px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.16);
overflow: hidden;
}
.dpr__fill {
height: 100%;
width: 100%;
transform-origin: left center;
transform: scaleX(0);
border-radius: 999px;
background: #17141a;
}
.dpr--paused .dpr__fill {
background: #c93000;
}
.dpr__btn {
width: 100%;
border: 0;
border-radius: 12px;
padding: 8px 16px;
background: #17141a;
color: #fff7e6;
font: 700 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
cursor: pointer;
transition: transform 140ms ease-out;
}
.dpr__btn:active {
transform: scale(0.96);
}
@media (max-width: 340px) {
.dpr {
max-width: 200px;
}
}
@media (prefers-reduced-motion: reduce) {
.dpr__btn {
transition: none;
}
}(function () {
var root = document.getElementById('dpr');
var state = document.getElementById('dprState');
var pctEl = document.getElementById('dprPct');
var fill = document.getElementById('dprFill');
var btn = document.getElementById('dprBtn');
// 상수: Total 소요 시간(ms) · 자동 루프의 Stopped 구간(ms)
var TOTAL_MS = 2600;
var AUTO_PAUSE_AT = [0.28, 0.62];
var AUTO_PAUSE_MS = 420;
var paused = false;
var startedAt = performance.now();
var pausedElapsed = 0; // 정지되기 전까지 이미 쌓인 경과 값
var raf = null;
var autoIdx = 0;
// 계산 겹: 경과 시간 = Stopped 전 누적값(pausedElapsed) + (지금 - Resume 시각). 재개할 때 0으로 안 끊긴다
function elapsed() {
if (paused) return pausedElapsed;
return pausedElapsed + (performance.now() - startedAt);
}
function paint() {
var e = Math.min(TOTAL_MS, elapsed());
var p = e / TOTAL_MS;
fill.style.transform = 'scaleX(' + p.toFixed(4) + ')';
pctEl.textContent = Math.round(p * 100) + '%';
state.textContent = paused ? 'Stopped' : (p >= 1 ? 'Done' : 'In progress');
root.classList.toggle('dpr--paused', paused && p < 1);
return p;
}
function setPaused(v) {
if (v === paused) return;
if (v) {
pausedElapsed = elapsed();
paused = true;
} else {
startedAt = performance.now();
paused = false;
}
}
function tick() {
var p = paint();
if (!paused && p < 1 && autoIdx < AUTO_PAUSE_AT.length && p >= AUTO_PAUSE_AT[autoIdx]) {
setPaused(true);
autoIdx++;
setTimeout(function () { if (root.classList.contains('is-demo')) setPaused(false); }, AUTO_PAUSE_MS);
}
if (p >= 1) {
setTimeout(function () {
pausedElapsed = 0; startedAt = performance.now(); paused = false; autoIdx = 0; paint();
}, 700);
}
raf = requestAnimationFrame(tick);
}
raf = requestAnimationFrame(tick);
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
setPaused(!paused);
btn.textContent = paused ? 'Resume' : 'Pause';
});
})();import { useEffect, useRef } from "react";
import styles from "./DownloadManagerPauseResume.module.scss";
type Box<T> = { current: T };
export default function DownloadManagerPauseResume() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const state = root.querySelector(`.${styles.state}`) as HTMLElement | null;
const pctEl = root.querySelector(`.${styles.pct}`) as HTMLElement | null;
const fill = root.querySelector(`.${styles.fill}`) as HTMLElement | null;
const btn = root.querySelector(`.${styles.btn}`) as HTMLElement | null;
if (!state || !pctEl || !fill || !btn) return;
// 상수: 전체 소요 시간(ms) · 자동 루프의 정지 구간(ms)
const TOTAL_MS = 2600;
const AUTO_PAUSE_AT = [0.28, 0.62];
const AUTO_PAUSE_MS = 420;
let paused = false;
let startedAt = performance.now();
let pausedElapsed = 0;
let raf = 0;
let autoIdx = 0;
let stopped = false;
// 계산 겹: 경과 시간 = 정지 전 누적값(pausedElapsed) + (지금 - 재개 시각). 재개할 때 0으로 안 끊긴다
function elapsed() {
if (paused) return pausedElapsed;
return pausedElapsed + (performance.now() - startedAt);
}
function paint() {
const e = Math.min(TOTAL_MS, elapsed());
const p = e / TOTAL_MS;
fill!.style.transform = "scaleX(" + p.toFixed(4) + ")";
pctEl!.textContent = Math.round(p * 100) + "%";
state!.textContent = paused ? "정지" : p >= 1 ? "완료" : "진행";
root!.classList.toggle(styles["dpr--paused"], paused && p < 1);
return p;
}
function setPaused(v: boolean) {
if (v === paused) return;
if (v) {
pausedElapsed = elapsed();
paused = true;
} else {
startedAt = performance.now();
paused = false;
}
}
function tick() {
if (stopped) return;
const p = paint();
if (!paused && p < 1 && autoIdx < AUTO_PAUSE_AT.length && p >= AUTO_PAUSE_AT[autoIdx]) {
setPaused(true);
autoIdx++;
window.setTimeout(() => { if (root!.classList.contains(styles["is-demo"])) setPaused(false); }, AUTO_PAUSE_MS);
}
if (p >= 1) {
window.setTimeout(() => {
pausedElapsed = 0; startedAt = performance.now(); paused = false; autoIdx = 0; paint();
}, 700);
}
raf = requestAnimationFrame(tick);
}
raf = requestAnimationFrame(tick);
function onClick() {
root!.classList.remove(styles["is-demo"]);
setPaused(!paused);
btn!.textContent = paused ? "재개" : "일시정지";
}
btn.addEventListener("click", onClick);
return () => {
stopped = true;
cancelAnimationFrame(raf);
btn.removeEventListener("click", onClick);
};
}, []);
return (
<div ref={rootRef} className={`${styles.stage} ${styles.dpr} ${styles["is-demo"]}`}>
<div className={styles.head}>
<span className={styles.state}>진행</span>
<span className={styles.pct}>0%</span>
</div>
<div className={styles.track}><div className={styles.fill} /></div>
<button className={styles.btn} type="button">일시정지</button>
</div>
);
}03 Pause/resume state-value switch — the progress value stops changing and starts again each time the state value toggles between paused and running — Measured area 1.799% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · Download Progress Bar: 9 Loading UI Patterns
The post this demo lives in
Download Progress Bar: 9 Loading UI PatternsThis download progress bar roundup: One of nine keeps each file's progress value climbing in a list, another resets it t See all demos in this post
If you like this effect - similar demos
Loaders08 Chunked pause and resume that only re-fills the sent partFile Upload Progress Bar Loader: 9 UI Demos
Loaders05 Icon progress ring — an indicator whose circular progress value fills with the pulled distance and morphs into a checkmark once completePull to Refresh CSS: 9 Loading Patterns
Loaders02 Regen-tick glow toggle — each time a value ticks upward at a fixed interval, a flash lights the bar's leading edgeCSS Progress Bar: 9 Game-Style Fill Patterns