06Corruption-retry count value — the retry-count value climbs by one and the progress value resets to the start whenever verification fails
Each time the received file fails verification, the retry-count value climbs by one, and the progress value resets to zero and starts rising again.
Best forA file download over an unstable network, resource distribution with integrity checks
.dcr__retryBACKOFF_MULTVERIFY_SEED

<main class="stage">
<div class="dcr is-demo" id="dcr">
<div class="dcr__head">
<span class="dcr__state" id="dcrState">Downloading</span>
<span class="dcr__retry" id="dcrRetry">Retry 0</span>
</div>
<div class="dcr__track"><div class="dcr__fill" id="dcrFill"></div></div>
<button class="dcr__btn" id="dcrBtn" type="button">Retry now</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 재시도 대기 = 기저값 × 배수^재시도횟수(지수 백오프) — index.html backoffMs() 참조
@include stage($stage-yellow);
.dcr {
width: 100%;
max-width: 230px;
display: grid;
gap: $sp-3;
}
.dcr__head {
display: flex;
align-items: baseline;
justify-content: space-between;
font: 600 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: $subject-ink;
}
.dcr--bad .dcr__state { color: $error; }
.dcr--ok .dcr__state { color: $subject-ink; }
.dcr__retry { font-variant-numeric: tabular-nums; color: rgba($subject-ink, .68); }
.dcr__track {
width: 100%;
height: $sp-3;
border-radius: $r-pill;
background: rgba($subject-ink, .14);
overflow: hidden;
}
.dcr__fill {
height: 100%;
width: 100%;
transform-origin: left center;
transform: scaleX(0);
border-radius: $r-pill;
background: $subject-ink;
transition: transform $dur-instant linear;
}
.dcr--bad .dcr__fill { background: $error; }
.dcr--ok .dcr__fill { background: $subject-mint; }
.dcr__btn {
width: 100%;
border: 0;
border-radius: $r-control;
padding: $sp-2 $sp-4;
background: $subject-ink;
color: $subject-cream;
font: 700 12px/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) {
.dcr { max-width: 195px; }
}
@media (prefers-reduced-motion: reduce) {
.dcr__fill, .dcr__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: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.dcr {
width: 100%;
max-width: 230px;
display: grid;
gap: 12px;
}
.dcr__head {
display: flex;
align-items: baseline;
justify-content: space-between;
font: 600 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #17141a;
}
.dcr--bad .dcr__state {
color: #c2361a;
}
.dcr--ok .dcr__state {
color: #17141a;
}
.dcr__retry {
font-variant-numeric: tabular-nums;
color: rgba(23, 20, 26, 0.68);
}
.dcr__track {
width: 100%;
height: 12px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.14);
overflow: hidden;
}
.dcr__fill {
height: 100%;
width: 100%;
transform-origin: left center;
transform: scaleX(0);
border-radius: 999px;
background: #17141a;
transition: transform 140ms linear;
}
.dcr--bad .dcr__fill {
background: #c2361a;
}
.dcr--ok .dcr__fill {
background: #4cd4a6;
}
.dcr__btn {
width: 100%;
border: 0;
border-radius: 12px;
padding: 8px 16px;
background: #17141a;
color: #fff7e6;
font: 700 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
cursor: pointer;
transition: transform 140ms ease-out;
}
.dcr__btn:active {
transform: scale(0.96);
}
@media (max-width: 340px) {
.dcr {
max-width: 195px;
}
}
@media (prefers-reduced-motion: reduce) {
.dcr__fill, .dcr__btn {
transition: none;
}
}(function () {
var root = document.getElementById('dcr');
var state = document.getElementById('dcrState');
var retryEl = document.getElementById('dcrRetry');
var fill = document.getElementById('dcrFill');
var btn = document.getElementById('dcrBtn');
// 시드 고정: 회차별 검증 결과(참=검증 통과). 0·1회차는 Corrupted, 2회차부터 통과
var VERIFY_SEED = [false, false, true, true, true];
// 상수: 틱 간격 · 기저 대기(ms) · 지수 배수 — 재시도할 때마다 대기 시간이 늘어난다
var TICK_MS = 90;
var BASE_BACKOFF_MS = 260;
var BACKOFF_MULT = 2;
var MAX_PCT = 100;
var pct = 0;
var retry = 0;
var timer = null;
var btnDisabled = false;
// 계산 겹: Retry 대기 = 기저값 × 배수^재시도횟수(지수 백오프) — 매 실패마다 대기가 길어진다
function backoffMs() {
return BASE_BACKOFF_MS * Math.pow(BACKOFF_MULT, Math.min(retry, 4));
}
function paint() {
fill.style.transform = 'scaleX(' + (pct / MAX_PCT).toFixed(3) + ')';
retryEl.textContent = 'Retry ' + retry;
}
function verify() {
var ok = VERIFY_SEED[retry % VERIFY_SEED.length];
if (ok) {
state.textContent = 'Done';
root.classList.add('dcr--ok');
setTimeout(reset, 900);
} else {
state.textContent = 'Corrupted';
root.classList.add('dcr--bad');
retry++;
pct = 0;
paint();
var wait = backoffMs();
timer = setTimeout(function () {
root.classList.remove('dcr--bad');
state.textContent = 'Downloading';
download();
}, wait);
}
}
function download() {
if (pct >= MAX_PCT) { verify(); return; }
pct = Math.min(MAX_PCT, pct + 9);
paint();
timer = setTimeout(download, TICK_MS);
}
function reset() {
root.classList.remove('dcr--ok', 'dcr--bad');
pct = 0; retry = 0; state.textContent = 'Downloading';
paint();
download();
}
download();
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
if (timer) clearTimeout(timer);
reset();
});
})();import { useEffect, useRef } from "react";
import styles from "./DownloadManagerCorruptRetry.module.scss";
type Box<T> = { current: T };
export default function DownloadManagerCorruptRetry() {
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 retryEl = root.querySelector(`.${styles.retry}`) as HTMLElement | null;
const fill = root.querySelector(`.${styles.fill}`) as HTMLElement | null;
const btn = root.querySelector(`.${styles.btn}`) as HTMLElement | null;
if (!state || !retryEl || !fill || !btn) return;
// 시드 고정: 회차별 검증 결과(참=검증 통과). 0·1회차는 손상, 2회차부터 통과
const VERIFY_SEED = [false, false, true, true, true];
const TICK_MS = 90;
const BASE_BACKOFF_MS = 260;
const BACKOFF_MULT = 2;
const MAX_PCT = 100;
let pct = 0;
let retry = 0;
let timer = 0;
let stopped = false;
// 계산 겹: 재시도 대기 = 기저값 × 배수^재시도횟수(지수 백오프) — 매 실패마다 대기가 길어진다
function backoffMs() {
return BASE_BACKOFF_MS * Math.pow(BACKOFF_MULT, Math.min(retry, 4));
}
function paint() {
fill!.style.transform = "scaleX(" + (pct / MAX_PCT).toFixed(3) + ")";
retryEl!.textContent = "재시도 " + retry;
}
function verify() {
const ok = VERIFY_SEED[retry % VERIFY_SEED.length];
if (ok) {
state!.textContent = "완료";
root!.classList.add(styles["dcr--ok"]);
timer = window.setTimeout(reset, 900);
} else {
state!.textContent = "손상";
root!.classList.add(styles["dcr--bad"]);
retry++;
pct = 0;
paint();
const wait = backoffMs();
timer = window.setTimeout(() => {
root!.classList.remove(styles["dcr--bad"]);
state!.textContent = "받는 중";
download();
}, wait);
}
}
function download() {
if (stopped) return;
if (pct >= MAX_PCT) { verify(); return; }
pct = Math.min(MAX_PCT, pct + 9);
paint();
timer = window.setTimeout(download, TICK_MS);
}
function reset() {
root!.classList.remove(styles["dcr--ok"], styles["dcr--bad"]);
pct = 0; retry = 0; state!.textContent = "받는 중";
paint();
download();
}
download();
function onClick() {
root!.classList.remove(styles["is-demo"]);
window.clearTimeout(timer);
reset();
}
btn.addEventListener("click", onClick);
return () => {
stopped = true;
window.clearTimeout(timer);
btn.removeEventListener("click", onClick);
};
}, []);
return (
<div ref={rootRef} className={`${styles.stage} ${styles.dcr} ${styles["is-demo"]}`}>
<div className={styles.head}>
<span className={styles.state}>받는 중</span>
<span className={styles.retry}>재시도 0</span>
</div>
<div className={styles.track}><div className={styles.fill} /></div>
<button className={styles.btn} type="button">지금 재시도</button>
</div>
);
}06 Corruption-retry count value — the retry-count value climbs by one and the progress value resets to the start whenever verification fails — Measured area 2.188% · moving frames 16/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
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
Loaders09 A Percent Loader That Smoothly Follows Uneven Real Progress and Pulses the Logo at the EndLogo Loading Animation: 9 Progress-Driven Marks
Loaders07 Error shake and retry that remembers where it brokeFile Upload Progress Bar Loader: 9 UI Demos