09モーション削減代替切替 — モーション削減の値がオンになるとポップオーバー・シート・振動の値が即座に切り替わる方式
モーション削減の値がオンになると前の8つの移動・振動の値がすべて消え、表示・非表示の値だけが即座に切り替わります。
使いどころモーション最小化設定をオンにしたユーザー、低スペック機器のフォーム画面
rm__pop--normalrm__pop--reducedprefers-reduced-motion

<main class="stage">
<div class="rm is-demo" id="rm">
<div class="rm__col">
<span class="rm__tag">通常</span>
<div class="rm__mini">
<button type="button" class="rm__field" id="rmNormalField">3月14日</button>
<div class="rm__pop rm__pop--normal" id="rmNormalPop">
<div class="rm__popRow">12 13 <b>14</b> 15 16</div>
</div>
</div>
</div>
<div class="rm__col">
<span class="rm__tag">低モーション</span>
<div class="rm__mini">
<button type="button" class="rm__field" id="rmReducedField">3月14日</button>
<div class="rm__pop rm__pop--reduced" id="rmReducedPop">
<div class="rm__popRow">12 13 <b>14</b> 15 16</div>
</div>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 같은 열림 흐름을 두 칸에 나란히 재생한다 — 왼쪽(일반)은 스프링 스케일,
// 오른쪽(감소된 모션)은 opacity 만. prefers-reduced-motion 실측정에서는 왼쪽도 opacity 만 쓴다.
@include stage($stage-ink);
.rm { display: flex; gap: $sp-2; width: 100%; max-width: 260px; justify-content: center; }
.rm__col { display: grid; gap: $sp-1; justify-items: center; }
.rm__tag { font-size: 9px; font-weight: 700; color: $subject-blue-light; }
.rm__mini { position: relative; width: 108px; }
.rm__field {
width: 100%; border: none; border-radius: $r-control; padding: $sp-1 $sp-2;
background: $subject-cream; color: $subject-ink; font-size: 10px; font-weight: 700; cursor: pointer;
box-shadow: $shadow-raised;
}
.rm__pop {
position: absolute; top: 100%; left: 0; right: 0; margin-top: $sp-1;
box-sizing: border-box; padding: $sp-1; border-radius: $r-sm;
background: $stage-paper; box-shadow: $shadow-raised;
opacity: 0; pointer-events: none;
}
.rm__pop--normal {
transform: scale(.8);
transform-origin: top center;
transition: transform $dur-enter $ease-elastic, opacity $dur-quick $ease-out;
}
.rm__pop--normal.is-open { transform: scale(1); opacity: 1; pointer-events: auto; }
.rm__pop--reduced {
transition: opacity $dur-quick $ease-out;
}
.rm__pop--reduced.is-open { opacity: 1; pointer-events: auto; }
.rm__popRow { font-size: 9px; color: $subject-ink; text-align: center; letter-spacing: .04em; }
.rm__popRow b { color: $subject-blue-deep; }
@media (max-width: 340px) {
.rm { max-width: 200px; gap: $sp-1; }
.rm__mini { width: 84px; }
.rm__field { font-size: 9px; }
.rm__popRow { font-size: 8px; }
}
@media (prefers-reduced-motion: reduce) {
.rm__pop--normal { transform: none; transition: opacity $dur-quick $ease-out; }
}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);
}
.rm {
display: flex;
gap: 8px;
width: 100%;
max-width: 260px;
justify-content: center;
}
.rm__col {
display: grid;
gap: 4px;
justify-items: center;
}
.rm__tag {
font-size: 9px;
font-weight: 700;
color: #5b8cf8;
}
.rm__mini {
position: relative;
width: 108px;
}
.rm__field {
width: 100%;
border: none;
border-radius: 12px;
padding: 4px 8px;
background: #fff7e6;
color: #17141a;
font-size: 10px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.rm__pop {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
box-sizing: border-box;
padding: 4px;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
opacity: 0;
pointer-events: none;
}
.rm__pop--normal {
transform: scale(0.8);
transform-origin: top center;
transition: transform 600ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 220ms ease-out;
}
.rm__pop--normal.is-open {
transform: scale(1);
opacity: 1;
pointer-events: auto;
}
.rm__pop--reduced {
transition: opacity 220ms ease-out;
}
.rm__pop--reduced.is-open {
opacity: 1;
pointer-events: auto;
}
.rm__popRow {
font-size: 9px;
color: #17141a;
text-align: center;
letter-spacing: 0.04em;
}
.rm__popRow b {
color: #0a4fe8;
}
@media (max-width: 340px) {
.rm {
max-width: 200px;
gap: 4px;
}
.rm__mini {
width: 84px;
}
.rm__field {
font-size: 9px;
}
.rm__popRow {
font-size: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.rm__pop--normal {
transform: none;
transition: opacity 220ms ease-out;
}
}(function () {
var rm = document.getElementById('rm');
var normalField = document.getElementById('rmNormalField');
var reducedField = document.getElementById('rmReducedField');
var normalPop = document.getElementById('rmNormalPop');
var reducedPop = document.getElementById('rmReducedPop');
var open = false;
function render() {
normalPop.classList.toggle('is-open', open);
reducedPop.classList.toggle('is-open', open);
}
function human() {
rm.classList.remove('is-demo');
stopAutoplay();
}
function toggle() {
open = !open;
render();
}
normalField.addEventListener('click', function () { human(); toggle(); });
reducedField.addEventListener('click', function () { human(); toggle(); });
var autoTimer = null;
function stopAutoplay() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }
function tick() { toggle(); }
render();
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) { autoTimer = setInterval(tick, 620); }
})();import { useEffect, useState } from 'react';
import styles from './DatePickerReducedMotion.module.scss';
export default function DatePickerReducedMotion() {
const [isDemo, setIsDemo] = useState(true);
const [open, setOpen] = useState(false);
function human() {
setIsDemo(false);
}
function toggle() {
setOpen((v) => !v);
}
useEffect(() => {
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return;
const timer = window.setInterval(() => {
if (!isDemo) return;
setOpen((v) => !v);
}, 620);
return () => window.clearInterval(timer);
}, [isDemo]);
return (
<div className={`${styles.rm} ${isDemo ? styles.isDemo : ''}`}>
<div className={styles.rmCol}>
<span className={styles.rmTag}>일반</span>
<div className={styles.rmMini}>
<button
type="button"
className={styles.rmField}
onClick={() => { human(); toggle(); }}
>
3월 14일
</button>
<div className={`${styles.rmPop} ${styles.rmPopNormal} ${open ? styles.isOpen : ''}`}>
<div className={styles.rmPopRow}>12 13 <b>14</b> 15 16</div>
</div>
</div>
</div>
<div className={styles.rmCol}>
<span className={styles.rmTag}>감소된 모션</span>
<div className={styles.rmMini}>
<button
type="button"
className={styles.rmField}
onClick={() => { human(); toggle(); }}
>
3월 14일
</button>
<div className={`${styles.rmPop} ${styles.rmPopReduced} ${open ? styles.isOpen : ''}`}>
<div className={styles.rmPopRow}>12 13 <b>14</b> 15 16</div>
</div>
</div>
</div>
</div>
);
}09 モーション削減代替切替 — モーション削減の値がオンになるとポップオーバー・シート・振動の値が即座に切り替わる方式 — 計測 累積面積 3.151% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · css 日付選択 9選 — ポップオーバーで開閉する9つの方式
このデモが属する記事
css 日付選択 9選 — ポップオーバーで開閉する9つの方式css 日付選択:9つのうち1つは入力欄の焦点値がオンになるとカレンダーがポップオーバーとして開き、1つは許可範囲外の日付の霞み値が大きくなって選べない状態が現れ、もう1つは画面幅の値が狭くなるとポップオーバーが下から上がるシートに変わりま この記事のデモをすべて見る


