Files
2nd/10_Wiki/Topics/AI_and_ML/웹 접근성 및 prefers-reduced-motion.md
T
Antigravity Agent f8b21af4be Wiki cleanup: error-doc removal, dedup merge, link normalization
10_Wiki/Topics 대규모 정리:
- 오류 캡처/미완성 stub 문서 227개 제거
- 교차폴더 중복 43클러스터 병합 (63파일 → redirect)
- 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건
- 카테고리 MOC 6개 신규 생성
- Graph 섹션 미해결 related-keyword 링크 10,058건 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:52:15 +09:00

6.1 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-웹-접근성-및-prefers-reduced-motion 웹 접근성 및 prefers-reduced-motion 10_Wiki/Topics verified self
Web Accessibility
prefers-reduced-motion
A11y
Motion Sensitivity
none A 0.9 applied
accessibility
a11y
css
animation
web
2026-05-10 pending
language framework
css web-platform

웹 접근성 및 prefers-reduced-motion

매 한 줄

"매 motion 은 default-on 이 아닌 opt-in 으로 design 되어야 한다". WCAG 2.2 / WAI-ARIA / EN 301 549 의 누적된 standard 와 함께, prefers-reduced-motion media query 는 매 vestibular disorder, migraine, attention disorder 사용자 의 web 접근성 lifeline 이다. 2026 의 production app 은 motion 을 system preference 에 따라 dynamic 하게 swap 한다.

매 핵심

매 a11y 의 기둥 (WCAG 2.2)

  • Perceivable: text alt, color contrast 4.5:1, captions.
  • Operable: keyboard nav, focus visible, no seizure trigger.
  • Understandable: clear label, predictable nav, error help.
  • Robust: valid markup, ARIA correctly used.

매 motion 의 위험

  • Vestibular: parallax, large translate → nausea.
  • Photosensitive: flashing > 3Hz → seizure (WCAG 2.3.1).
  • Cognitive: auto-play loop → distraction.

매 응용

  1. Hero 의 fade-in 만, no parallax for reduced.
  2. Transition 의 fade swap (no slide).
  3. Loading spinner 의 static fallback.

💻 패턴

CSS media query

/* default: full motion */
.card {
  transition: transform 300ms cubic-bezier(.2,.8,.2,1),
              opacity 200ms ease;
}
.card:hover { transform: translateY(-4px) scale(1.02); }

/* reduced: minimize */
@media (prefers-reduced-motion: reduce) {
  .card,
  .card * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
  .card:hover { transform: none; }
}

Tailwind v4

<div class="transition motion-reduce:transition-none
            hover:scale-105 motion-reduce:hover:scale-100
            motion-safe:animate-bounce">
  ...
</div>

JS detection (matchMedia)

const reduceMotionMQ = window.matchMedia("(prefers-reduced-motion: reduce)");

function shouldAnimate(): boolean { return !reduceMotionMQ.matches; }

reduceMotionMQ.addEventListener("change", e => {
  document.documentElement.dataset.motion = e.matches ? "reduce" : "full";
});

React hook

import { useEffect, useState } from "react";

export function usePrefersReducedMotion() {
  const [reduced, setReduced] = useState(
    () => typeof window !== "undefined" &&
          window.matchMedia("(prefers-reduced-motion: reduce)").matches
  );
  useEffect(() => {
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, []);
  return reduced;
}

// usage
function Hero() {
  const reduce = usePrefersReducedMotion();
  return reduce
    ? <StaticHero />
    : <ParallaxHero />;
}

Framer Motion

import { motion, useReducedMotion } from "framer-motion";

function Card() {
  const reduce = useReducedMotion();
  return (
    <motion.div
      initial={{ opacity: 0, y: reduce ? 0 : 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: reduce ? 0 : 0.4 }}
    />
  );
}

GSAP

import gsap from "gsap";
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;

if (!reduce) {
  gsap.from(".hero", { y: 60, opacity: 0, duration: 0.8 });
} else {
  gsap.set(".hero", { opacity: 1, y: 0 });
}

Focus visible (keyboard a11y)

:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}
<a href="#main" class="skip-link">Skip to main content</a>

<style>
.skip-link {
  position: absolute; left: -9999px;
}
.skip-link:focus {
  position: fixed; top: 1rem; left: 1rem;
  padding: .5rem 1rem; background: black; color: white;
  z-index: 9999;
}
</style>

ARIA + visible-text-only icon button

<button aria-label="Close dialog" type="button">
  <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">
    <path d="M6 6l12 12M6 18L18 6" stroke="currentColor"/>
  </svg>
</button>

Color contrast check (CI)

// tests/contrast.test.ts
import { hex } from "wcag-contrast";
import tokens from "../tokens.json";

test("text on bg meets AA", () => {
  expect(hex(tokens.color.text, tokens.color.bg)).toBeGreaterThanOrEqual(4.5);
});

매 결정 기준

상황 Approach
decorative parallax disable on reduce
critical feedback motion keep, but shorten
auto-playing carousel pause, user-controlled
keyboard-only user focus-visible + skip-link
screen reader ARIA label + semantic HTML
color signal only add icon/text alternative

기본값: motion-safe wrapper + ARIA + WCAG AA contrast + axe CI.

🔗 Graph

🤖 LLM 활용

언제: ARIA label suggestion, alt text drafting, animation reduce variant proposal, axe failure interpretation. 언제 X: real screen-reader testing — NVDA/JAWS/VoiceOver 의 manual run 필수.

안티패턴

  • outline: none no focus-visible: keyboard user 의 invisible nav.
  • Auto-play 무한 motion: ignores reduced-motion entirely.
  • ARIA without semantic: <div role="button"> 대신 <button> 사용.
  • Color-only meaning: red/green 만으로 status — colorblind 차단.

🧪 검증 / 중복

  • Verified (WCAG 2.2 W3C 2023, MDN prefers-reduced-motion, axe-core rules).
  • 신뢰도 A.

🕓 Changelog

날짜 변경
2026-05-08 Phase 1
2026-05-10 Manual cleanup — a11y + prefers-reduced-motion patterns.