08表示オフトグル — 状態値だけ透明度を補間するスイッチ
表示がオフの状態になると、入力表示カードの透明度がオフの値まで徐々に補間されて消え、再びオンの状態になると同じ曲線を逆にたどって現れます。
使いどころ通知を最小化する設定があるチャットアプリ、おやすみモード画面
addEventListeneraria-pressedopacity

<main class="stage">
<section class="panel">
<div class="card" id="card">
<div class="dots"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>
<span class="label">入力中</span>
</div>
<button class="muteBtn" id="muteBtn" type="button" aria-pressed="false" aria-label="入力表示のミュート切替">
ミュート
</button>
<span class="badge" id="muteBadge" hidden>表示オフ</span>
<span class="sr" id="live" role="status" aria-live="polite"></span>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-paper);
.panel {
display: flex;
align-items: center;
gap: $sp-4;
}
.card {
display: flex;
align-items: center;
gap: $sp-2;
padding: $sp-3 $sp-4;
border-radius: $r-card;
background: $subject-ink;
transition: opacity $dur-base $ease-out;
}
.dots { display: flex; gap: $sp-1; }
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: $subject-cream;
}
.label {
font-size: 13px;
color: $subject-cream;
}
.muteBtn {
padding: $sp-2 $sp-3;
border-radius: $r-control;
border: 1px solid $subject-ink;
background: transparent;
color: $subject-ink;
font-size: 13px;
cursor: pointer;
}
.muteBtn[aria-pressed="true"] {
background: $subject-ink;
color: $subject-cream;
}
.badge {
padding: $sp-1 $sp-2;
border-radius: $r-xs;
background: $error-soft;
color: $error;
font-size: 12px;
}
.sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
}
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
}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);
}
.panel {
display: flex;
align-items: center;
gap: 16px;
}
.card {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-radius: 16px;
background: #17141a;
transition: opacity 300ms ease-out;
}
.dots {
display: flex;
gap: 4px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #fff7e6;
}
.label {
font-size: 13px;
color: #fff7e6;
}
.muteBtn {
padding: 8px 12px;
border-radius: 12px;
border: 1px solid #17141a;
background: transparent;
color: #17141a;
font-size: 13px;
cursor: pointer;
}
.muteBtn[aria-pressed=true] {
background: #17141a;
color: #fff7e6;
}
.badge {
padding: 4px 8px;
border-radius: 4px;
background: #ffe0d8;
color: #c2361a;
font-size: 12px;
}
.sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}// 표시 끄기 토글 — 상태 값(켜짐/꺼짐)만큼 카드의 투명도를 보간하고, 꺼짐 표시는 별도 배지로 대비를 지킨다.
// 계산 겹: 상태 값 → 목표 투명도 재계산 → transition 으로 보간. 클릭 리스너 1개(이 항목만).
(function () {
var btn = document.getElementById('muteBtn');
var card = document.getElementById('card');
var badge = document.getElementById('muteBadge');
var live = document.getElementById('live');
if (!btn || !card || !badge || !live) return;
var OFF_OPACITY = 0.3;
var AUTO_CYCLE_MS = 700;
var muted = false;
function apply() {
card.style.opacity = muted ? String(OFF_OPACITY) : '1';
badge.hidden = !muted;
btn.setAttribute('aria-pressed', String(muted));
live.textContent = muted ? '表示オフ' : '表示オン';
}
btn.addEventListener('click', function () {
muted = !muted;
apply();
});
apply();
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return;
setInterval(function () {
muted = !muted;
apply();
}, AUTO_CYCLE_MS);
})();import { useEffect, useRef, useState } from "react";
import styles from "./TypingMuteToggleFade.module.scss";
// 표시 끄기 토글 — 상태 값(켜짐/꺼짐)만큼 카드의 투명도를 보간하고, 꺼짐 표시는 별도 배지로 대비를 지킨다.
// 계산 겹: 상태 값 → 목표 투명도 재계산 → transition 으로 보간. 클릭 리스너 1개(이 항목만).
const OFF_OPACITY = 0.3;
const AUTO_CYCLE_MS = 700;
export default function TypingMuteToggleFade() {
const [muted, setMuted] = useState(false);
const liveRef = useRef<HTMLSpanElement | null>(null);
useEffect(() => {
if (liveRef.current) {
liveRef.current.textContent = muted ? "표시 꺼짐" : "표시 켜짐";
}
}, [muted]);
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
const id = setInterval(() => setMuted((m) => !m), AUTO_CYCLE_MS);
return () => clearInterval(id);
}, []);
return (
<div className={styles.stage}>
<section className={styles.panel}>
<div
className={styles.card}
style={{ opacity: muted ? OFF_OPACITY : 1 }}
>
<div className={styles.dots}>
<span className={styles.dot} />
<span className={styles.dot} />
<span className={styles.dot} />
</div>
<span className={styles.label}>입력 중</span>
</div>
<button
type="button"
className={styles.muteBtn}
aria-pressed={muted}
aria-label="입력 표시 음소거 전환"
onClick={() => setMuted((m) => !m)}
>
음소거
</button>
{muted && <span className={styles.badge}>표시 꺼짐</span>}
<span ref={liveRef} className={styles.sr} role="status" aria-live="polite" />
</section>
</div>
);
}08 表示オフトグル — 状態値だけ透明度を補間するスイッチ — 計測 累積面積 6.372% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · タイピングインジケーターUI 9選 — 3つの点からグループチャットまで
このデモが属する記事
タイピングインジケーターUI 9選 — 3つの点からグループチャットまで「タイピングインジケーター」9選 — 9つのうち1つは入力中の時間が長いほどアバターの縁がゆっくり脈打ち、1つはグループチャットで入力者が変わるたびに残り時間の値で名札の幅を描き直し、もう1つは振幅配列の値で音声入力表示の棒の高さを計算し直 この記事のデモをすべて見る


