PlusMagi's Blog By Pitt Phunsanit Technology วิธีจัดการ Animation และ Transition ด้วย Tailwind CSS ตามเกณฑ์ WCAG 2.2 (prefers-reduced-motion)

วิธีจัดการ Animation และ Transition ด้วย Tailwind CSS ตามเกณฑ์ WCAG 2.2 (prefers-reduced-motion)

ตามมาตรฐาน WCAG 2.2 Success Criterion 2.3.3 (Animation from Interactions) องค์ประกอบที่มีการขยับ เคลื่อนไหว หรือซูมขนาดใหญ่ สามารถกระตุ้นอาการเวียนศีรษะ คลื่นไส้ หรือกระทบต่อผู้ป่วยโรคระบบการทรงตัว (Vestibular Disorders) ได้ ผู้ใช้กลุ่มนี้จึงมักเปิดการตั้งค่า “Reduce Motion” ในระบบปฏิบัติการ (iOS, macOS, Windows, Android)

ในฝั่งเว็บ เราตรวจจับการตั้งค่านี้ผ่าน Media Query @media (prefers-reduced-motion: reduce) โดย Tailwind CSS มี Variant ในตัวที่ช่วยจัดการเรื่องนี้ได้ทันที


รู้จัก Tailwind Variant: motion-safe และ motion-reduce

Tailwind ให้ Utility Variants มา 2 ตัวที่ทำงานตรงข้ามกัน

  • motion-safe
    สั่งให้ Transition/Animation ทำงาน เฉพาะเมื่อผู้ใช้ไม่ได้เปิดโหมดลดการเคลื่อนไหว (แนะนำให้ใช้แนวทางนี้เป็นหลัก)
  • motion-reduce
    สั่งให้สไตล์ทำงาน เมื่อผู้ใช้เปิดโหมดลดการเคลื่อนไหว (ใช้สำหรับปิดหรือดรอปอนิเมชันลง)

JavaScript

<!-- วิธีที่ 1: ใช้ motion-safe (ปลอดภัยและกระชับที่สุด) -->
<button class="transition-transform motion-safe:hover:scale-105 motion-safe:duration-200">
  ปุ่มสั่งงาน
</button>

<!-- วิธีที่ 2: ใช้ motion-reduce สั่ง override กลับเป็นค่าปกติ -->
<div class="animate-spin motion-reduce:animate-none">
  <!-- สัญลักษณ์โหลดดิ้ง -->
</div>

Global Reset: ปิดอนิเมชันทั้งเว็บใน 3 บรรทัด (Fail-safe)

เพื่อป้องกันไม่ให้มีอนิเมชันหลุดรอดจากคอมโพเนนต์ที่นักพัฒนาลืมใส่ Variant ให้เพิ่มโค้ด CSS ระดับ Global ลงใน globals.css (หรือ main CSS file)

JavaScript

/* globals.css */
@layer base {
  @media (prefers-reduced-motion: reduce) {
    *,
    ::before,
    ::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }
}

ทำไมต้องใช้ 0.01ms แทนที่จะเป็น 0s หรือ none?

เพราะไลบรารี JavaScript หรือ Transition Hooks บางตัว (เช่น Framer Motion, Transition Component ของ Headless UI) ต้องพึ่งพา Event transitionend หรือ animationend ในการ Unmount DOM หากตั้งเป็น none ตัว Event จะไม่ถูกยิง ส่งผลให้ Modal หรือ Dropdown ค้างปิดไม่ลง การใช้ 0.01ms ทำให้จบอนิเมชันทันทีในระดับสายตามนุษย์มองไม่เห็น แต่ Event ยังยิงครบถ้วน


รูปแบบการปรับลดที่ถูกต้องตามหลัก WCAG (Subtle vs Distracting)

WCAG ไม่ได้ห้ามการเปลี่ยนสถานะทั้งหมด สิ่งที่ต้องลดคือ การเคลื่อนไหวผ่านพื้นที่สายตาขนาดใหญ่ (Large Spatial Movement) ไม่ใช่การห้ามเปลี่ยนสี

ประเภท UIโหมดปกติ (Motion Safe)โหมดลดการเคลื่อนไหว (Reduced Motion)
Modal / DialogZoom-in / Slide จากด้านบน พร้อม Fadeเปลี่ยนเป็น Instant Fade In หรือปรากฏทันที
ปุ่มกด (Button Hover)ขยายขนาด scale-105 และลอยขึ้น -translate-y-1เปลี่ยนเฉพาะ สีพื้นหลัง (Background Color)
Loading Spinnerหมุนวนต่อเนื่อง (animate-spin)สลับเป็น ข้อความ “กำลังโหลด…” หรือ Pulse ความสว่างแบบช้า ๆ
การเลื่อนหน้าจอSmooth Scrolling (scroll-smooth)Jump ตรงไปยังจุดหมายทันที (scroll-auto)

ตัวอย่างคอมโพเนนต์จริง: Modal Transition & Smooth Scroll

ตัวอย่างที่ 1: Modal Transition ที่สลับจากการ Slide มาเป็น Fade นุ่มนวล

JavaScript

<!-- กล่อง Modal: โหมดปกติจะขยับขึ้นและขยายขนาด แต่ถ้า reduced motion จะแค่เปลี่ยน Opacity -->
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
  <div class="w-full max-w-md p-6 bg-white dark:bg-slate-900 rounded-xl shadow-xl
              /* Transition พื้นฐาน */
              transition-all duration-200
              /* สภาพปกติ: สไลด์ขึ้นและขยาย */
              motion-safe:translate-y-0 motion-safe:scale-100
              /* ป้องกันไม่ให้แกว่งเมื่อเปิด/ปิด */
              motion-reduce:transform-none">
    <h3 class="text-lg font-bold text-slate-900 dark:text-white">ข้อความแจ้งเตือน</h3>
    <p class="mt-2 text-sm text-slate-600 dark:text-slate-300">
      แอนิเมชันเปิดหน้าต่างนี้จะไม่กระตุกสายตาของผู้ใช้ที่ตั้งค่า Reduce Motion
    </p>
  </div>
</div>

ตัวอย่างที่ 2: Smooth Scrolling ที่ปิดตัวอัตโนมัติ

JavaScript

<!-- ใส่ที่แท็ก <html> เพื่อให้เลื่อนนุ่มนวลเฉพาะคนที่ไม่ได้เปิดโหมดลดการเคลื่อนไหว -->
<html class="motion-safe:scroll-smooth" lang="th">
  <body>...</body>
</html>

วิธีทดสอบใน Chrome DevTools

  1. เปิดหน้าเว็บแล้วกด F12 เพื่อเปิด Chrome DevTools
  2. กด Ctrl + Shift + P (Windows) หรือ Cmd + Shift + P (Mac)
  3. พิมพ์ Rendering แล้วกด Enter เพื่อเปิดแท็บ Rendering
  4. เลื่อนหาหัวข้อ Emulate CSS media feature prefers-reduced-motion
  5. เลือกเป็น prefers-reduced-motion: reduce แล้วลองกดทดสอบปุ่มหรือ Modal เพื่อดูผลลัพธ์ทันที