09モーション削減時の静的ダウンロード表示 — モーション削減設定でアニメーションの代わりに進捗値の文言だけが変わる方式
モーション削減がオンだとバーや位置の値は動かず、進捗値を示す文言だけがそのまま変わります。
使いどころアクセシビリティ設定を守ったダウンロード進捗表示、モーション最小化を求めたユーザー画面
.drm__staticBoxSTEP.drm--force-reduce

<main class="stage">
<div class="drm is-demo" id="drm">
<div class="drm__col">
<span class="drm__colLabel">通常モーション</span>
<div class="drm__track"><div class="drm__fill" id="drmFillA"></div></div>
<span class="drm__pct" id="drmPctA">0%</span>
</div>
<div class="drm__col drm__col--reduced">
<span class="drm__colLabel">モーション減</span>
<div class="drm__staticBox"><span class="drm__pct" id="drmPctB">0%</span></div>
</div>
<button class="drm__btn" id="drmBtn" type="button">モーション減に切替</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 진행 값 하나를 두 판이 다르게 그린다 — 왼쪽 transform 갱신 · 오른쪽 문구만 교체 — index.html paint() 참조
@include stage($stage-ink);
.drm {
width: 100%;
max-width: 260px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: $sp-3;
}
.drm__col {
display: grid;
justify-items: center;
gap: $sp-2;
}
.drm__colLabel {
font: 600 11px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: rgba($subject-cream, .8);
text-align: center;
}
.drm__track {
width: 100%;
height: $sp-2;
border-radius: $r-pill;
background: rgba($subject-cream, .16);
overflow: hidden;
}
.drm__fill {
height: 100%;
width: 100%;
transform-origin: left center;
transform: scaleX(0);
border-radius: $r-pill;
background: $subject-blue-light;
transition: transform $dur-instant linear;
}
.drm--force-reduce .drm__fill { transition: none; }
.drm__pct {
font: 700 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: $subject-cream;
font-variant-numeric: tabular-nums;
}
.drm__staticBox {
width: 100%;
min-height: $sp-2;
padding: $sp-1 0;
border-radius: $r-sm;
background: rgba($subject-cream, .16);
display: grid;
place-items: center;
// 의도적으로 막대·위치를 움직이지 않는다 — 모션 감소 판은 문구 교체만(둥근 칩, 진행 막대 아님)
}
.drm__btn {
grid-column: 1 / -1;
width: 100%;
border: 0;
border-radius: $r-control;
padding: $sp-2 $sp-4;
background: $subject-cream;
color: $stage-ink;
font: 700 11px/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) {
.drm { max-width: 210px; gap: $sp-2; }
.drm__colLabel { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.drm__fill { transition: none; }
.drm__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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.drm {
width: 100%;
max-width: 260px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.drm__col {
display: grid;
justify-items: center;
gap: 8px;
}
.drm__colLabel {
font: 600 11px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: rgba(255, 247, 230, 0.8);
text-align: center;
}
.drm__track {
width: 100%;
height: 8px;
border-radius: 999px;
background: rgba(255, 247, 230, 0.16);
overflow: hidden;
}
.drm__fill {
height: 100%;
width: 100%;
transform-origin: left center;
transform: scaleX(0);
border-radius: 999px;
background: #5b8cf8;
transition: transform 140ms linear;
}
.drm--force-reduce .drm__fill {
transition: none;
}
.drm__pct {
font: 700 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #fff7e6;
font-variant-numeric: tabular-nums;
}
.drm__staticBox {
width: 100%;
min-height: 8px;
padding: 4px 0;
border-radius: 8px;
background: rgba(255, 247, 230, 0.16);
display: grid;
place-items: center;
}
.drm__btn {
grid-column: 1/-1;
width: 100%;
border: 0;
border-radius: 12px;
padding: 8px 16px;
background: #fff7e6;
color: #17141a;
font: 700 11px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
cursor: pointer;
transition: transform 140ms ease-out;
}
.drm__btn:active {
transform: scale(0.96);
}
@media (max-width: 340px) {
.drm {
max-width: 210px;
gap: 8px;
}
.drm__colLabel {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.drm__fill {
transition: none;
}
.drm__btn {
transition: none;
}
}(function () {
var root = document.getElementById('drm');
var fillA = document.getElementById('drmFillA');
var pctA = document.getElementById('drmPctA');
var pctB = document.getElementById('drmPctB');
var btn = document.getElementById('drmBtn');
// 상수: 進行中 속도(%/틱) · 틱 간격(ms)
var STEP = 4;
var TICK_MS = 130;
var forcedReduce = false;
var pct = 0;
// 계산 겹: 같은 進行中 값 하나를 두 판이 다르게 그린다 — 왼쪽은 매 틱 transform 갱신, 오른쪽은 문구만 교체(레이아웃·트랜지션 0)
function paint() {
fillA.style.transform = 'scaleX(' + (pct / 100).toFixed(3) + ')';
pctA.textContent = pct + '%';
pctB.textContent = pct + '%';
}
function tick() {
pct = (pct + STEP) % 101;
paint();
setTimeout(tick, TICK_MS);
}
tick();
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
forcedReduce = !forcedReduce;
root.classList.toggle('drm--force-reduce', forcedReduce);
btn.textContent = forcedReduce ? '通常モーションに切替' : 'モーション減に切替';
});
})();import { useEffect, useRef } from "react";
import styles from "./DownloadManagerReducedMotion.module.scss";
type Box<T> = { current: T };
export default function DownloadManagerReducedMotion() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const fillA = root.querySelector(`.${styles.fill}`) as HTMLElement | null;
const pctA = root.querySelector(`.${styles.pctA}`) as HTMLElement | null;
const pctB = root.querySelector(`.${styles.pctB}`) as HTMLElement | null;
const btn = root.querySelector(`.${styles.btn}`) as HTMLElement | null;
if (!fillA || !pctA || !pctB || !btn) return;
// 상수: 진행 속도(%/틱) · 틱 간격(ms)
const STEP = 4;
const TICK_MS = 130;
let forcedReduce = false;
let pct = 0;
let timer = 0;
let stopped = false;
// 계산 겹: 같은 진행 값 하나를 두 판이 다르게 그린다 — 왼쪽은 매 틱 transform 갱신, 오른쪽은 문구만 교체(레이아웃·트랜지션 0)
function paint() {
fillA!.style.transform = "scaleX(" + (pct / 100).toFixed(3) + ")";
pctA!.textContent = pct + "%";
pctB!.textContent = pct + "%";
}
function tick() {
if (stopped) return;
pct = (pct + STEP) % 101;
paint();
timer = window.setTimeout(tick, TICK_MS);
}
tick();
function onClick() {
root!.classList.remove(styles["is-demo"]);
forcedReduce = !forcedReduce;
root!.classList.toggle(styles["drm--force-reduce"], forcedReduce);
btn!.textContent = forcedReduce ? "일반 모션 전환" : "모션 감소 전환";
}
btn.addEventListener("click", onClick);
return () => {
stopped = true;
window.clearTimeout(timer);
btn.removeEventListener("click", onClick);
};
}, []);
return (
<div ref={rootRef} className={`${styles.stage} ${styles.drm} ${styles["is-demo"]}`}>
<div className={styles.col}>
<span className={styles.colLabel}>일반 모션</span>
<div className={styles.track}><div className={styles.fill} /></div>
<span className={`${styles.pct} ${styles.pctA}`}>0%</span>
</div>
<div className={`${styles.col} ${styles["col--reduced"]}`}>
<span className={styles.colLabel}>모션 감소</span>
<div className={styles.staticBox}><span className={`${styles.pct} ${styles.pctB}`}>0%</span></div>
</div>
<button className={styles.btn} type="button">모션 감소 전환</button>
</div>
);
}09 モーション削減時の静的ダウンロード表示 — モーション削減設定でアニメーションの代わりに進捗値の文言だけが変わる方式 — 計測 累積面積 0.643% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · ダウンロード 進捗バー 9選 — 値がパーセントで更新される9つの方式
このデモが属する記事
ダウンロード 進捗バー 9選 — 値がパーセントで更新される9つの方式このダウンロード 進捗バー特集では、9つのうち1つは複数のファイルが同時に受信される間、各ファイルの進捗値が一覧の中で並んで上がり、1つは検証が失敗するたびに再試行回数の値が増えて進捗値が最初に戻り、もう1つは完了した項目の位置値が一覧の決 この記事のデモをすべて見る

