Guest Adventurer 🥉 Lv.1
🪙 50
🔷 10
← กลับคลังเนื้อหา
Article · programming

HTML & CSS EP14: CSS Transitions และ Animations — สร้างเว็บไซต์เคลื่อนไหวอย่างมืออาชีพ

HTML & CSS

บทเรียน HTML และ CSS ตอนที่ 14 เรียนรู้การสร้างเอฟเฟกต์เคลื่อนไหวด้วย CSS Transitions, Keyframes, Animation Properties, Transform, Timing Functions และ Accessibility พร้อมตัวอย่างที่นำไปทดลองและต่อยอดเป็นเว็บไซต์จริงได้

HTML & CSS EP14: CSS Transitions และ Animations — สร้างเว็บไซต์เคลื่อนไหวอย่างมืออาชีพ

ในบทก่อนหน้า เราได้เรียนรู้การจัดวางองค์ประกอบด้วย CSS Grid และการสร้าง Layout ที่รองรับหน้าจอหลากหลายขนาดแล้ว บทนี้จะพาไปอีกขั้นด้วยการเพิ่มการเคลื่อนไหวให้เว็บไซต์ ไม่ว่าจะเป็นปุ่มที่เปลี่ยนสีอย่างนุ่มนวล การ์ดที่ยกตัวขึ้นเมื่อเลื่อนเมาส์ เมนูที่ปรากฏพร้อมเอฟเฟกต์ หรือข้อความที่เคลื่อนไหวเมื่อเปิดหน้าเว็บ ทั้งหมดนี้สามารถทำได้ด้วย CSS โดยไม่จำเป็นต้องเขียน JavaScript สำหรับทุกเอฟเฟกต์
🎯 เป้าหมายของบทเรียนเมื่อจบบทนี้ คุณจะสามารถเลือกใช้ `transition` สำหรับการเปลี่ยนสถานะอย่างนุ่มนวล ใช้ `@keyframes` สำหรับแอนิเมชันที่มีหลายขั้นตอน ควบคุมระยะเวลาและจังหวะการเคลื่อนไหว สร้าง Hover Effects และ Loading Indicators ตลอดจนออกแบบแอนิเมชันที่คำนึงถึงผู้ใช้ที่ไม่ต้องการการเคลื่อนไหวได้

1. ทำความเข้าใจ CSS Transitions และ CSS Animations

**CSS Transition** ใช้เปลี่ยนค่าของ CSS Property จากสถานะหนึ่งไปยังอีกสถานะหนึ่งอย่างต่อเนื่อง โดยทั่วไปจะเกิดเมื่อมีการเปลี่ยนสถานะ เช่น `:hover`, `:focus-visible` หรือเมื่อมีการเพิ่มคลาสจาก JavaScript ส่วน **CSS Animation** ใช้กำหนดลำดับการเคลื่อนไหวผ่าน `@keyframes` ทำให้มีหลายจุดเปลี่ยนสถานะ และสามารถเล่นซ้ำหรือเริ่มทำงานเมื่อโหลดหน้าเว็บได้โดยไม่ต้องอาศัยการ Hover
คุณสมบัติCSS TransitionCSS Animation
การเริ่มทำงานมักเริ่มเมื่อค่า CSS เปลี่ยนเริ่มตามการกำหนด Animation
กำหนดขั้นตอนกลางเปลี่ยนจากค่าเริ่มต้นไปยังค่าปลายทางกำหนดหลาย Keyframes ได้
การทำซ้ำไม่ทำซ้ำต่อเนื่องโดยตัวมันเองกำหนดจำนวนรอบหรือวนไม่สิ้นสุดได้
ตัวอย่างที่เหมาะสมปุ่มเปลี่ยนสี การ์ดยกตัวLoading Spinner การปรากฏขององค์ประกอบ

2. เริ่มต้นด้วย Transition

คุณสมบัติ `transition` ช่วยให้การเปลี่ยนค่า CSS ไม่เกิดขึ้นทันที แต่ค่อย ๆ เปลี่ยนตามเวลาที่กำหนด รูปแบบพื้นฐานคือ `transition: property duration timing-function delay;` โดย `property` คือคุณสมบัติที่ต้องการเปลี่ยน `duration` คือระยะเวลา `timing-function` คือรูปแบบความเร็ว และ `delay` คือเวลาหน่วงก่อนเริ่ม
<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ตัวอย่าง CSS Transition</title>
  <style>
    body {
      font-family: system-ui, sans-serif;
      background: #f1f5f9;
      padding: 32px;
      color: #0f172a;
    }
    .demo-button {
      border: 0;
      border-radius: 12px;
      padding: 14px 24px;
      background: #2563eb;
      color: #fff;
      font-size: 16px;
      cursor: pointer;
      transition: background-color 250ms ease, transform 250ms ease, box-shadow 250ms ease;
    }
    .demo-button:hover {
      background: #1d4ed8;
      transform: translateY(-3px);
      box-shadow: 0 10px 20px rgb(37 99 235 / 22%);
    }
    .demo-button:focus-visible {
      outline: 3px solid #93c5fd;
      outline-offset: 4px;
    }
  </style>
</head>
<body>
  <h1>ปุ่มที่ตอบสนองต่อผู้ใช้</h1>
  <p>เลื่อนเมาส์เหนือปุ่ม หรือใช้แป้น Tab เพื่อโฟกัส แล้วสังเกตการเปลี่ยนแปลง</p>
  <button class="demo-button" type="button">ทดลอง Hover Effect</button>
</body>
</html>
ในตัวอย่างนี้ `background-color` เปลี่ยนสีอย่างนุ่มนวล ส่วน `transform: translateY(-3px)` ทำให้ปุ่มเลื่อนขึ้นเล็กน้อย และ `box-shadow` เพิ่มมิติให้ปุ่ม การระบุ Property ที่ต้องการเปลี่ยนอย่างชัดเจนช่วยให้อ่านโค้ดง่ายกว่าการใช้ `transition: all` โดยไม่จำเป็น

2.1 เลือก Timing Function ให้เหมาะกับการเคลื่อนไหว

Timing Function ควบคุมว่าการเคลื่อนไหวจะเร็วหรือช้าในแต่ละช่วง แม้จะกำหนดระยะเวลาเท่ากัน แต่ผลลัพธ์ที่เห็นอาจแตกต่างกันได้
ค่าพฤติกรรมการใช้งานที่เหมาะสม
`linear`เคลื่อนที่ด้วยอัตราคงที่การเคลื่อนที่ที่ต้องการความสม่ำเสมอ
`ease`เริ่มและจบอย่างนุ่มนวลเอฟเฟกต์ทั่วไปในส่วนติดต่อผู้ใช้
`ease-in`เริ่มช้าแล้วเร็วขึ้นองค์ประกอบที่กำลังเริ่มเคลื่อนออก
`ease-out`เริ่มเร็วแล้วช้าลงการปรากฏหรือการหยุดขององค์ประกอบ
`ease-in-out`เริ่มช้า เร็วขึ้น แล้วช้าลงการเคลื่อนไหวที่ต้องการความนุ่มนวลทั้งสองด้าน
เคล็ดลับเอฟเฟกต์ของปุ่มและการ์ดมักใช้เวลาประมาณ 150–300 มิลลิวินาทีเป็นจุดเริ่มต้นที่ดี แต่ควรทดลองกับเนื้อหาและอุปกรณ์จริง ระยะเวลาที่เหมาะสมขึ้นอยู่กับบริบท ไม่ใช่ค่าตายตัวสำหรับทุกเว็บไซต์

3. ใช้ Transform สร้างการเคลื่อนไหว

`transform` เป็นเครื่องมือสำคัญสำหรับการเคลื่อนไหวแบบสองมิติ โดยไม่จำเป็นต้องเปลี่ยนตำแหน่งใน Layout โดยตรง คุณสมบัติที่ใช้บ่อย ได้แก่ `translate()` สำหรับเลื่อนตำแหน่ง `scale()` สำหรับปรับขนาด `rotate()` สำหรับหมุน และ `skew()` สำหรับเอียงองค์ประกอบ เมื่อนำมารวมกับ `transition` จะสร้างเอฟเฟกต์ที่ดูเป็นธรรมชาติได้ง่าย
.card {
  transition: transform 220ms ease, box-shadow 220ms ease;
}

.card:hover {
  transform: translateY(-6px) scale(1.01);
  box-shadow: 0 16px 32px rgb(15 23 42 / 14%);
}

.icon {
  transition: transform 220ms ease;
}

.icon:hover {
  transform: rotate(8deg) scale(1.08);
}

/* ใช้กับองค์ประกอบที่ต้องการหมุน */
.spinner {
  transform: rotate(0deg);
}
ควรใช้ `transform` อย่างระมัดระวังและไม่ขยายองค์ประกอบจนบังเนื้อหาข้างเคียง หากใช้เอฟเฟกต์กับการ์ดจำนวนมาก ให้รักษาระดับการยกตัวและเงาให้สม่ำเสมอ เพื่อให้ผู้ใช้เข้าใจว่าองค์ประกอบใดสามารถโต้ตอบได้

4. สร้าง Animation ด้วย @keyframes

`@keyframes` ช่วยกำหนดค่าของ CSS ในจุดต่าง ๆ ของแอนิเมชัน โดยใช้ `from` และ `to` สำหรับจุดเริ่มต้นและจุดสิ้นสุด หรือใช้เปอร์เซ็นต์ เช่น `0%`, `50%` และ `100%` เมื่อต้องการควบคุมหลายช่วง ตัวอย่างเช่น การเลื่อนเข้ามาพร้อมการค่อย ๆ ปรากฏ หรือการหมุนไอคอนโหลดข้อมูล
@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes pulse {
  0%, 100% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.04);
    opacity: 0.75;
  }
}

.panel {
  animation: fade-in-up 500ms ease-out both;
}

.spinner {
  animation: spin 900ms linear infinite;
}

.status-dot {
  animation: pulse 1600ms ease-in-out infinite;
}
ในตัวอย่าง `fade-in-up` ทำให้องค์ประกอบเริ่มจากตำแหน่งที่ต่ำกว่าเล็กน้อยและมีความโปร่งใสเป็นศูนย์ ก่อนเข้าสู่สถานะปกติ ส่วน `spin` หมุนครบหนึ่งรอบ และ `pulse` ขยายขนาดเล็กน้อยเป็นจังหวะ การใช้ชื่อ Animation ที่สื่อความหมายช่วยให้ทีมพัฒนาเข้าใจวัตถุประสงค์ของแต่ละเอฟเฟกต์ได้ง่าย

4.1 ทำความเข้าใจ Animation Properties

Propertyความหมายตัวอย่าง
`animation-name`ชื่อ Keyframes`fade-in-up`
`animation-duration`ระยะเวลาต่อรอบ`500ms`
`animation-timing-function`จังหวะความเร็ว`ease-out`
`animation-delay`เวลาหน่วงก่อนเริ่ม`150ms`
`animation-iteration-count`จำนวนรอบ`1` หรือ `infinite`
`animation-direction`ทิศทางการเล่น`normal` หรือ `alternate`
`animation-fill-mode`การใช้ค่าก่อนหรือหลังแอนิเมชัน`both`
`animation-play-state`สถานะการเล่น`running` หรือ `paused`
ตัวอย่างการใช้แบบย่อคือ `animation: fade-in-up 500ms ease-out 100ms 1 normal both;` ซึ่งกำหนดชื่อ ระยะเวลา จังหวะความเร็ว เวลาหน่วง จำนวนรอบ ทิศทาง และ Fill Mode ตามลำดับ หากต้องการควบคุมแต่ละค่าชัดเจน สามารถเขียนแยกเป็น Property ได้เช่นกัน

5. สร้าง Loading Spinner และสถานะกำลังทำงาน

เมื่อเว็บไซต์ต้องรอข้อมูลจากเซิร์ฟเวอร์ ผู้ใช้ควรได้รับสัญญาณว่าระบบกำลังทำงานอยู่ Loading Spinner เป็นตัวอย่างของแอนิเมชันที่พบได้บ่อย แต่ควรใช้เฉพาะเมื่อเหมาะสม พร้อมมีข้อความอธิบายสถานะสำหรับเทคโนโลยีช่วยเหลือ และไม่ควรปล่อยให้หมุนค้างโดยไม่มีข้อมูลหรือทางเลือกเมื่อการโหลดล้มเหลว
<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Loading Spinner</title>
  <style>
    body {
      font-family: system-ui, sans-serif;
      background: #f8fafc;
      color: #0f172a;
      padding: 28px;
    }
    .loading-box {
      display: flex;
      align-items: center;
      gap: 14px;
      max-width: 340px;
      padding: 20px;
      background: white;
      border: 1px solid #e2e8f0;
      border-radius: 14px;
    }
    .spinner {
      width: 28px;
      height: 28px;
      border: 4px solid #dbeafe;
      border-top-color: #2563eb;
      border-radius: 50%;
      animation: spin 850ms linear infinite;
      flex: 0 0 auto;
    }
    @keyframes spin {
      to { transform: rotate(360deg); }
    }
    @media (prefers-reduced-motion: reduce) {
      .spinner { animation: none; }
    }
  </style>
</head>
<body>
  <h1>สถานะการโหลด</h1>
  <div class="loading-box" role="status" aria-live="polite">
    <span class="spinner" aria-hidden="true"></span>
    <span>กำลังโหลดข้อมูล กรุณารอสักครู่</span>
  </div>
</body>
</html>
องค์ประกอบนี้ใช้ `border` สี่ด้านร่วมกับ `border-top-color` เพื่อสร้างส่วนที่มองเห็นเด่นขึ้น จากนั้นหมุนด้วย `@keyframes` โดยใช้ `aria-hidden="true"` กับวงกลมตกแต่ง และให้ข้อความสถานะเป็นส่วนที่เทคโนโลยีช่วยเหลือสามารถรับรู้ได้

6. สร้างการ์ดที่เคลื่อนไหวอย่างมีลำดับ

ในหน้าที่มีการ์ดหลายใบ เช่น รายการสินค้า บริการ หรือบทความ เราอาจต้องการให้การ์ดค่อย ๆ ปรากฏโดยมีระยะหน่วงต่างกัน วิธีหนึ่งคือกำหนดคลาสสำหรับการ์ดและใช้ `animation-delay` เพื่อสร้างลำดับการแสดงผล อย่างไรก็ตาม ไม่ควรใส่ความล่าช้ามากเกินไปจนผู้ใช้ต้องรอเนื้อหาที่จำเป็น
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 18px;
}

.product-card {
  padding: 20px;
  border: 1px solid #e2e8f0;
  border-radius: 16px;
  background: #fff;
  animation: fade-in-up 450ms ease-out both;
}

.product-card:nth-child(2) {
  animation-delay: 80ms;
}

.product-card:nth-child(3) {
  animation-delay: 160ms;
}

.product-card:nth-child(4) {
  animation-delay: 240ms;
}

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
ระวังการซ่อนเนื้อหาสำคัญหากแอนิเมชันเริ่มต้นด้วย `opacity: 0` เนื้อหาจะมองไม่เห็นจนกว่าแอนิเมชันจะเริ่มทำงาน ควรหลีกเลี่ยงการพึ่งพาเอฟเฟกต์เพื่อให้เนื้อหาสำคัญปรากฏเพียงอย่างเดียว โดยเฉพาะเมื่อมีการโหลดช้า ปิดแอนิเมชัน หรือใช้เบราว์เซอร์ที่มีข้อจำกัด

7. รองรับผู้ใช้ที่ลดการเคลื่อนไหว

ผู้ใช้บางคนตั้งค่าในระบบปฏิบัติการให้ลดการเคลื่อนไหวด้วยเหตุผลด้านความสะดวกสบายหรือการเข้าถึงเว็บไซต์ เราสามารถตรวจสอบการตั้งค่านี้ผ่าน Media Query `prefers-reduced-motion` และลดหรือยกเลิกแอนิเมชันที่ไม่จำเป็นได้ โดยยังคงข้อมูลและสถานะสำคัญให้เข้าถึงได้ครบถ้วน
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* หากองค์ประกอบต้องเคลื่อนไหวเพื่อสื่อสถานะ
   ให้แน่ใจว่ามีข้อความหรือสัญญาณอื่นประกอบด้วย */
กฎนี้เป็นแนวทางเริ่มต้นสำหรับลดเอฟเฟกต์ทั่วหน้า แต่ในโปรเจกต์จริงควรทดสอบกับทุกองค์ประกอบ เพราะบางแอนิเมชันอาจสื่อสถานะสำคัญ เช่น การแจ้งว่ากำลังโหลดหรือการเปลี่ยนสถานะของข้อมูล การลดการเคลื่อนไหวไม่ควรทำให้ผู้ใช้สูญเสียข้อมูล

8. ประสิทธิภาพ: เลือก Property ที่เหมาะสม

แอนิเมชันบางประเภททำให้เบราว์เซอร์ต้องคำนวณ Layout หรือวาดภาพใหม่หลายครั้ง ซึ่งอาจทำให้เกิดอาการกระตุก โดยทั่วไป `transform` และ `opacity` เป็นตัวเลือกที่เหมาะสำหรับเอฟเฟกต์เคลื่อนไหวจำนวนมาก แต่ไม่ได้หมายความว่าจะเร็วที่สุดในทุกสถานการณ์ ควรตรวจสอบผลจริงด้วยเครื่องมือ Performance ของเบราว์เซอร์
Propertyข้อควรพิจารณาแนวทาง
`transform`มักเหมาะกับการเลื่อน หมุน และปรับขนาดใช้กับเอฟเฟกต์ที่ไม่จำเป็นต้องเปลี่ยน Layout
`opacity`เหมาะกับการค่อย ๆ ปรากฏหรือจางหายระวังการทำให้องค์ประกอบสำคัญมองไม่เห็น
`width` และ `height`อาจทำให้เกิดการคำนวณ Layoutใช้เมื่อจำเป็นและทดสอบประสิทธิภาพ
`top` และ `left`อาจกระทบการจัดวางตามบริบทพิจารณา `transform` สำหรับการเลื่อนภาพ
`box-shadow`การเปลี่ยนแปลงเงาขนาดใหญ่หรือหลายองค์ประกอบอาจมีต้นทุนใช้ความเข้มและจำนวนเอฟเฟกต์อย่างพอดี
อย่าใช้ will-change โดยไม่จำเป็น`will-change` เป็นคำใบ้ให้เบราว์เซอร์เตรียมปรับประสิทธิภาพสำหรับ Property ที่จะเปลี่ยน แต่การใช้กับองค์ประกอบจำนวนมากหรือเปิดทิ้งไว้นานอาจใช้ทรัพยากรเพิ่ม ควรใช้เฉพาะเมื่อมีเหตุผลจากการวัดประสิทธิภาพและนำออกเมื่อไม่จำเป็น

9. ข้อผิดพลาดที่พบบ่อยและวิธีแก้

กำหนด Transition แล้วแต่ไม่มีเอฟเฟกต์
ตรวจสอบว่าค่า CSS เปลี่ยนจริงหรือไม่ เช่น มี `:hover` หรือ `:focus-visible` ถูกเรียกใช้ และ Property ที่กำหนดรองรับการเปลี่ยนค่าแบบต่อเนื่องหรือไม่ อย่าลืมตรวจสอบว่าไม่มี CSS กฎอื่นเขียนทับค่าเดิม
Animation เล่นแล้วไม่คงสถานะสุดท้าย
ตรวจสอบ `animation-fill-mode` หากต้องการให้ค่าจาก Keyframes คงอยู่หลังแอนิเมชัน ให้พิจารณาใช้ `forwards` หรือ `both` ตามความเหมาะสม และตรวจสอบว่าสไตล์พื้นฐานขององค์ประกอบไม่ขัดแย้งกับสถานะที่ต้องการ
แอนิเมชันทำให้หน้าเว็บดูช้าหรือรบกวนผู้ใช้
ลดระยะเวลาและจำนวนองค์ประกอบที่เคลื่อนไหว หลีกเลี่ยงการวนซ้ำแบบไม่มีความจำเป็น เพิ่มทางเลือกสำหรับ `prefers-reduced-motion` และอย่าใช้เอฟเฟกต์เพื่อหน่วงการเข้าถึงเนื้อหาหลัก
การ์ดกระตุกเมื่อมีจำนวนมาก
เริ่มจากลดจำนวนเอฟเฟกต์และใช้ `transform` หรือ `opacity` เมื่อเหมาะสม ตรวจสอบ Performance Timeline และทดสอบบนอุปกรณ์ที่มีประสิทธิภาพต่ำกว่า อย่าตัดสินจากคอมพิวเตอร์ของผู้พัฒนาเพียงเครื่องเดียว

10. Workshop: สร้างหน้าแสดงบริการพร้อม Transition และ Animation

ตัวอย่างต่อไปนี้เป็นหน้าเว็บ HTML แบบเต็มไฟล์ที่รวมแนวคิดจากบทเรียน ได้แก่ Layout แบบ Grid การ์ดบริการที่ยกตัวเมื่อ Hover ปุ่มที่เปลี่ยนสถานะ แอนิเมชันปรากฏตัวแบบสั้น และการรองรับ `prefers-reduced-motion` สามารถคัดลอกโค้ดทั้งหมดไปบันทึกเป็น `index.html` แล้วเปิดในเบราว์เซอร์เพื่อทดลองได้ทันที
<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="ตัวอย่างหน้าแสดงบริการที่ใช้ CSS Transitions และ Animations">
  <title>บริการดิจิทัล | CSS Motion Workshop</title>
  <style>
    :root {
      color-scheme: light;
      --primary: #2563eb;
      --primary-dark: #1d4ed8;
      --ink: #0f172a;
      --muted: #475569;
      --surface: #ffffff;
      --page: #f1f5f9;
      --border: #dbe4f0;
    }

    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      line-height: 1.65;
      color: var(--ink);
      background: var(--page);
    }

    .site-header {
      background: var(--surface);
      border-bottom: 1px solid var(--border);
    }

    .header-inner,
    main,
    .site-footer {
      width: min(1100px, calc(100% - 32px));
      margin-inline: auto;
    }

    .header-inner {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 16px;
      padding-block: 16px;
    }

    .brand {
      margin: 0;
      font-size: 1.1rem;
      font-weight: 800;
      letter-spacing: -0.02em;
    }

    .nav-link {
      color: var(--primary-dark);
      font-weight: 650;
      text-decoration-thickness: 2px;
      text-underline-offset: 4px;
    }

    main {
      padding-block: 48px 64px;
    }

    .hero {
      max-width: 720px;
      margin-bottom: 32px;
      animation: fade-in-up 450ms ease-out both;
    }

    .eyebrow {
      margin: 0 0 8px;
      color: var(--primary-dark);
      font-size: 0.82rem;
      font-weight: 800;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    h1 {
      margin: 0 0 14px;
      font-size: clamp(2rem, 5vw, 3.5rem);
      line-height: 1.12;
      letter-spacing: -0.04em;
    }

    .hero p:last-child {
      margin: 0;
      color: var(--muted);
      font-size: 1.05rem;
    }

    .service-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr));
      gap: 18px;
    }

    .service-card {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      padding: 24px;
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 18px;
      box-shadow: 0 4px 12px rgb(15 23 42 / 4%);
      animation: fade-in-up 500ms ease-out both;
      transition: transform 220ms ease, box-shadow 220ms ease, border-color 220ms ease;
    }

    .service-card:nth-child(2) {
      animation-delay: 70ms;
    }

    .service-card:nth-child(3) {
      animation-delay: 140ms;
    }

    .service-card:nth-child(4) {
      animation-delay: 210ms;
    }

    .service-card:hover {
      transform: translateY(-5px);
      border-color: #93c5fd;
      box-shadow: 0 16px 32px rgb(15 23 42 / 10%);
    }

    .service-card:focus-within {
      outline: 3px solid #bfdbfe;
      outline-offset: 3px;
    }

    .service-icon {
      display: grid;
      width: 48px;
      height: 48px;
      place-items: center;
      margin-bottom: 16px;
      border-radius: 14px;
      background: #dbeafe;
      font-size: 1.5rem;
      transition: transform 220ms ease;
    }

    .service-card:hover .service-icon {
      transform: rotate(-5deg) scale(1.06);
    }

    .service-card h2 {
      margin: 0 0 8px;
      font-size: 1.2rem;
    }

    .service-card p {
      margin: 0 0 20px;
      color: var(--muted);
    }

    .button {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-height: 44px;
      margin-top: auto;
      padding: 10px 16px;
      border: 0;
      border-radius: 10px;
      background: var(--primary);
      color: white;
      font: inherit;
      font-weight: 750;
      text-decoration: none;
      cursor: pointer;
      transition: background-color 180ms ease, transform 180ms ease;
    }

    .button:hover {
      background: var(--primary-dark);
      transform: translateY(-2px);
    }

    .button:focus-visible,
    .nav-link:focus-visible {
      outline: 3px solid #93c5fd;
      outline-offset: 4px;
    }

    .notice {
      margin-top: 28px;
      padding: 18px 20px;
      border: 1px solid #bfdbfe;
      border-radius: 14px;
      background: #eff6ff;
      color: #1e3a8a;
    }

    .site-footer {
      padding-block: 22px;
      border-top: 1px solid var(--border);
      color: var(--muted);
      font-size: 0.9rem;
    }

    @keyframes fade-in-up {
      from {
        opacity: 0;
        transform: translateY(12px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }

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

      .service-card:hover,
      .service-card:hover .service-icon,
      .button:hover {
        transform: none;
      }
    }
  </style>
</head>
<body>
  <header class="site-header">
    <div class="header-inner">
      <p class="brand">Motion Studio</p>
      <a class="nav-link" href="#services">บริการของเรา</a>
    </div>
  </header>

  <main>
    <section class="hero" aria-labelledby="page-title">
      <p class="eyebrow">ออกแบบเพื่อธุรกิจยุคใหม่</p>
      <h1 id="page-title">บริการดิจิทัลที่ทำให้ธุรกิจเติบโต</h1>
      <p>ทดลองเอฟเฟกต์ด้วย CSS Transitions และ Animations ที่ช่วยให้ส่วนติดต่อผู้ใช้ตอบสนองอย่างนุ่มนวล โดยไม่ลดทอนความชัดเจนของเนื้อหา</p>
    </section>

    <section id="services" class="service-grid" aria-label="รายการบริการ">
      <article class="service-card">
        <div class="service-icon" aria-hidden="true">✦</div>
        <h2>ออกแบบเว็บไซต์</h2>
        <p>สร้างหน้าเว็บที่อ่านง่าย รองรับหลายขนาดหน้าจอ และมีรูปแบบสอดคล้องกับแบรนด์</p>
        <a class="button" href="#contact">ดูรายละเอียด</a>
      </article>

      <article class="service-card">
        <div class="service-icon" aria-hidden="true">⌘</div>
        <h2>พัฒนาระบบ</h2>
        <p>ออกแบบฟังก์ชันสำหรับงานธุรกิจ โดยคำนึงถึงการใช้งานจริงและการดูแลระยะยาว</p>
        <a class="button" href="#contact">ดูรายละเอียด</a>
      </article>

      <article class="service-card">
        <div class="service-icon" aria-hidden="true">▣</div>
        <h2>ประสบการณ์ผู้ใช้</h2>
        <p>ปรับปรุงการนำทาง สถานะปุ่ม และการตอบสนองให้เข้าใจได้ง่ายยิ่งขึ้น</p>
        <a class="button" href="#contact">ดูรายละเอียด</a>
      </article>

      <article class="service-card">
        <div class="service-icon" aria-hidden="true">↗</div>
        <h2>วิเคราะห์ข้อมูล</h2>
        <p>จัดระเบียบข้อมูลและนำเสนอสิ่งที่จำเป็นต่อการตัดสินใจของทีม</p>
        <a class="button" href="#contact">ดูรายละเอียด</a>
      </article>
    </section>

    <aside class="notice" id="contact">
      <strong>ทดลองใช้งาน:</strong> เลื่อนเมาส์เหนือการ์ดและปุ่ม หรือใช้แป้น Tab เพื่อทดสอบการโฟกัส เอฟเฟกต์จะลดลงเมื่อระบบของผู้ใช้ตั้งค่าให้ลดการเคลื่อนไหว
    </aside>
  </main>

  <footer class="site-footer">
    <p>ตัวอย่างเพื่อการเรียนรู้ HTML และ CSS — ปรับแต่งข้อความ สี และระยะเวลาได้ตามต้องการ</p>
  </footer>
</body>
</html>

11. แบบฝึกหัดท้ายบท

ลองแก้ไขตัวอย่างหน้าเว็บด้วยตัวเอง โดยเปลี่ยนค่าต่าง ๆ แล้วสังเกตผลลัพธ์ก่อนและหลังแก้ไข การฝึกจากการทดลองจะช่วยให้เข้าใจความสัมพันธ์ระหว่าง Property และพฤติกรรมของแอนิเมชันได้ดีกว่าการจำรูปแบบโค้ดเพียงอย่างเดียว
แบบฝึกหัดที่ 1: ปรับความเร็วปุ่ม
เปลี่ยนระยะเวลา Transition ของปุ่มจาก 180ms เป็น 400ms แล้วทดลองเปรียบเทียบว่าการตอบสนองรู้สึกแตกต่างกันอย่างไร จากนั้นลองใช้ `ease-in-out` และ `linear` โดยคงระยะเวลาเท่าเดิม
แบบฝึกหัดที่ 2: สร้าง Animation ใหม่
เขียน `@keyframes` ชื่อ `slide-in` ให้กล่องเริ่มจาก `transform: translateX(-20px)` และ `opacity: 0` ก่อนจบที่ `translateX(0)` และ `opacity: 1` แล้วนำไปใช้กับองค์ประกอบที่เหมาะสม
แบบฝึกหัดที่ 3: ปรับการ์ดสินค้า
เพิ่มการ์ดอีกสองใบลงใน Grid ปรับ `gap` ให้เหมาะสม และทดลองกำหนด `animation-delay` ให้แต่ละใบแตกต่างกัน โดยต้องแน่ใจว่าการหน่วงไม่ทำให้ผู้ใช้ต้องรอเนื้อหาสำคัญ
แบบฝึกหัดที่ 4: ตรวจสอบ Accessibility
เปิดการตั้งค่า Reduce Motion ในระบบปฏิบัติการหรือเครื่องมือจำลองของเบราว์เซอร์ แล้วตรวจสอบว่าเอฟเฟกต์ที่ไม่จำเป็นลดลงหรือไม่ และเนื้อหายังเข้าถึงได้ครบถ้วน

12. สรุปบทเรียน

CSS Transitions เหมาะกับการเปลี่ยนสถานะอย่างนุ่มนวล ส่วน CSS Animations และ `@keyframes` เหมาะกับการกำหนดการเคลื่อนไหวหลายช่วงหรือการเล่นซ้ำ การใช้ `transform` และ `opacity` มักเป็นจุดเริ่มต้นที่ดีสำหรับเอฟเฟกต์ทั่วไป แต่ควรทดสอบประสิทธิภาพจริง เลือกระยะเวลาให้เหมาะสม และรองรับผู้ใช้ที่ต้องการลดการเคลื่อนไหวเสมอ
Checklist ก่อนนำขึ้นเว็บไซต์จริงตรวจสอบว่าเอฟเฟกต์ช่วยสื่อสารสถานะ ไม่รบกวนการอ่าน มีสถานะ Focus สำหรับผู้ใช้คีย์บอร์ด ไม่ซ่อนเนื้อหาสำคัญ ไม่ทำงานวนซ้ำโดยไม่มีเหตุผล และรองรับ `prefers-reduced-motion` พร้อมทดสอบทั้งหน้าจอเล็กและใหญ่
แอนิเมชันที่ดีไม่ใช่การทำให้ทุกอย่างเคลื่อนไหว แต่คือการทำให้การเปลี่ยนแปลงที่สำคัญเข้าใจง่ายขึ้น โดยไม่ขวางทางผู้ใช้

ดูเอกสาร CSS Transitions จาก MDN

ดูเอกสาร CSS Animations จาก MDN


บทถัดไป EP15: จะต่อยอดจากการสร้างเอฟเฟกต์ด้วย CSS ไปสู่การออกแบบองค์ประกอบส่วนติดต่อผู้ใช้ที่นำกลับมาใช้ซ้ำได้ การจัดการสถานะของ UI และการวางโครงสร้างหน้าเว็บให้ดูแลรักษาง่ายขึ้น
อ่านจนถึงระยะ 500px ก่อนท้ายหน้าและอยู่ในหน้านี้อย่างน้อย 5 นาที เพื่อรับ EXP วันละครั้ง

⏳ กำลังจับเวลาอ่าน...

🌐
เลือกภาษา / Select Language
21 World Languages Supported