ตามมาตรฐาน 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) ต้องพึ่งพา Eventtransitionendหรือanimationendในการ Unmount DOM หากตั้งเป็นnoneตัว Event จะไม่ถูกยิง ส่งผลให้ Modal หรือ Dropdown ค้างปิดไม่ลง การใช้0.01msทำให้จบอนิเมชันทันทีในระดับสายตามนุษย์มองไม่เห็น แต่ Event ยังยิงครบถ้วน
รูปแบบการปรับลดที่ถูกต้องตามหลัก WCAG (Subtle vs Distracting)
WCAG ไม่ได้ห้ามการเปลี่ยนสถานะทั้งหมด สิ่งที่ต้องลดคือ การเคลื่อนไหวผ่านพื้นที่สายตาขนาดใหญ่ (Large Spatial Movement) ไม่ใช่การห้ามเปลี่ยนสี
| ประเภท UI | โหมดปกติ (Motion Safe) | โหมดลดการเคลื่อนไหว (Reduced Motion) |
|---|---|---|
| Modal / Dialog | Zoom-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
- เปิดหน้าเว็บแล้วกด F12 เพื่อเปิด Chrome DevTools
- กด Ctrl + Shift + P (Windows) หรือ Cmd + Shift + P (Mac)
- พิมพ์
Renderingแล้วกด Enter เพื่อเปิดแท็บ Rendering - เลื่อนหาหัวข้อ Emulate CSS media feature prefers-reduced-motion
- เลือกเป็น
prefers-reduced-motion: reduceแล้วลองกดทดสอบปุ่มหรือ Modal เพื่อดูผลลัพธ์ทันที