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

HTML & CSS EP11: CSS Grid ขั้นสูง — จัดตำแหน่งองค์ประกอบและสร้าง Responsive Layout

HTML & CSS

บทเรียน HTML และ CSS ตอนที่ 11 เจาะลึก CSS Grid ขั้นสูง การกำหนดตำแหน่งด้วยเส้น Grid การใช้ named lines และ grid-template-areas การจัดวางองค์ประกอบแบบซ้อนกัน การควบคุมขนาดด้วย minmax() และการสร้าง Responsive Dashboard พร้อมตัวอย่างโค้ด HTML เต็มหน้าที่นำไปทดลองได้ทันที

HTML & CSS EP11: ยกระดับ CSS Grid สำหรับเว็บไซต์จริง

จากบทเรียนก่อนหน้าที่เราได้รู้จัก `display: grid`, `grid-template-columns`, `gap`, `minmax()` และ `auto-fit` แล้ว ตอนนี้เราจะนำความรู้เหล่านั้นมาต่อยอดสู่การออกแบบเลย์เอาต์ที่ซับซ้อนขึ้น เช่น Dashboard ระบบจัดการสินค้า หน้ารายงานธุรกิจ และหน้าเว็บไซต์ที่มีส่วนเนื้อหาหลายขนาด เป้าหมายคือเขียน CSS ที่อ่านง่าย ปรับขนาดได้ และดูแลรักษาได้สะดวก โดยไม่ต้องกำหนดตำแหน่งทุกองค์ประกอบด้วยค่าคงที่
💡 เป้าหมายของบทเรียนเมื่อจบบทนี้ คุณจะสามารถกำหนดตำแหน่งรายการด้วย Grid Lines ใช้ Named Areas จัดโครงสร้างหน้าเว็บ ควบคุมการยืดและหดของคอลัมน์ และออกแบบเลย์เอาต์ที่ปรับตามหน้าจอโดยใช้ CSS Grid ร่วมกับ Media Queries ได้

1. ทำความเข้าใจ Grid Lines และ Grid Tracks

CSS Grid ประกอบด้วยเส้นแบ่งพื้นที่ที่เรียกว่า **Grid Lines** และพื้นที่ระหว่างเส้นที่เรียกว่า **Grid Tracks** โดย Grid Track อาจเป็นแถวหรือคอลัมน์ก็ได้ เส้น Grid จะมีหมายเลขเริ่มจาก 1 ในทิศทางปกติของเลย์เอาต์ และสามารถนำหมายเลขเส้นมาใช้ระบุตำแหน่งเริ่มต้นและสิ้นสุดของรายการได้
<div class="layout">
  <div class="item item-a">A</div>
  <div class="item item-b">B</div>
  <div class="item item-c">C</div>
</div>

<style>
.layout {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: 100px 100px;
  gap: 12px;
}

.item-a {
  grid-column: 1 / 3;
  grid-row: 1;
}

.item-b {
  grid-column: 3;
  grid-row: 1 / 3;
}

.item-c {
  grid-column: 1 / 3;
  grid-row: 2;
}
</style>
ในตัวอย่างนี้ A ครอบคลุมคอลัมน์ที่หนึ่งถึงก่อนเส้นที่สาม ส่วน B อยู่ในคอลัมน์ที่สามและครอบคลุมสองแถว ขณะที่ C ใช้สองคอลัมน์แรกในแถวที่สอง การระบุจุดเริ่มต้นและจุดสิ้นสุดด้วยเส้นช่วยให้ควบคุมรูปแบบเลย์เอาต์ได้ละเอียดกว่าการจัดวางอัตโนมัติเพียงอย่างเดียว

2. ใช้ span เพื่อกำหนดขนาดพื้นที่ที่รายการครอบครอง

หากไม่ต้องการอ้างอิงหมายเลขเส้นโดยตรง สามารถใช้ `span` เพื่อกำหนดว่ารายการควรกินพื้นที่กี่แทร็ก ตัวอย่างเช่น `grid-column: span 2` หมายถึงให้รายการครอบคลุมสองคอลัมน์ ส่วน `grid-row: span 2` ให้ครอบคลุมสองแถว ตำแหน่งเริ่มต้นยังขึ้นอยู่กับอัลกอริทึมการจัดวางของ Grid และพื้นที่ที่เหลืออยู่
.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px;
}

.dashboard-card--featured {
  grid-column: span 2;
  grid-row: span 2;
}

.dashboard-card--wide {
  grid-column: span 2;
}

.dashboard-card--full {
  grid-column: 1 / -1;
}
⚠️ ระวังการกำหนดตำแหน่งซ้ำซ้อนหากกำหนดให้หลายรายการครอบครองพื้นที่เดียวกัน อาจเกิดการซ้อนทับได้โดยตั้งใจหรือไม่ตั้งใจก็ได้ ควรตรวจสอบ `grid-column`, `grid-row` และลำดับการวางรายการให้สอดคล้องกับโครงสร้างเนื้อหา

3. ตั้งชื่อ Grid Lines เพื่อให้ CSS อ่านง่าย

นอกจากการใช้หมายเลขเส้นแล้ว CSS Grid ยังอนุญาตให้ตั้งชื่อเส้นใน `grid-template-columns` หรือ `grid-template-rows` ได้ การตั้งชื่อช่วยให้โค้ดสื่อความหมาย โดยเฉพาะเลย์เอาต์ที่มีคอลัมน์สำหรับเมนูหลัก เนื้อหา และแผงด้านข้าง
.page {
  display: grid;
  grid-template-columns:
    [sidebar-start] 240px
    [sidebar-end content-start] minmax(0, 1fr)
    [content-end];
  gap: 20px;
}

.sidebar {
  grid-column: sidebar-start / sidebar-end;
}

.main-content {
  grid-column: content-start / content-end;
}
ชื่อเส้นอย่าง `sidebar-start` และ `content-end` ทำให้เจตนาของเลย์เอาต์ชัดเจนกว่าหมายเลขเส้นเพียงอย่างเดียว หากโครงสร้างมีการเปลี่ยนจำนวนคอลัมน์ในภายหลัง ควรตรวจสอบชื่อเส้นและกฎที่อ้างอิงชื่อเหล่านั้นให้ตรงกับโครงสร้างใหม่

4. วางแผนผังหน้าด้วย grid-template-areas

สำหรับหน้าที่มีโครงสร้างชัดเจน การใช้ `grid-template-areas` ช่วยให้มองเห็นผังหน้าเว็บจาก CSS ได้ทันที โดยกำหนดชื่อพื้นที่ให้กับองค์ประกอบผ่าน `grid-area` แต่ละแถวในแผนผังต้องมีจำนวนเซลล์เท่ากัน และชื่อพื้นที่เดียวกันควรสร้างเป็นรูปทรงสี่เหลี่ยมผืนผ้าต่อเนื่องกัน
.app-layout {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr) 280px;
  grid-template-areas:
    "header header header"
    "sidebar main aside"
    "footer footer footer";
  gap: 16px;
}

.app-header { grid-area: header; }
.app-sidebar { grid-area: sidebar; }
.app-main { grid-area: main; }
.app-aside { grid-area: aside; }
.app-footer { grid-area: footer; }
ในตัวอย่างนี้ Header และ Footer กินพื้นที่ครบทั้งสามคอลัมน์ ส่วน Sidebar, Main และ Aside แบ่งพื้นที่ในแถวกลาง วิธีนี้เหมาะกับ Dashboard ที่มีเมนูนำทาง เนื้อหาหลัก และแผงข้อมูลประกอบ

5. ปรับผังหน้าเว็บด้วย Media Queries

เลย์เอาต์สามคอลัมน์อาจเหมาะกับจอคอมพิวเตอร์ แต่แคบเกินไปบนมือถือ เราจึงสามารถเปลี่ยน `grid-template-areas` ภายใน Media Query เพื่อเรียงส่วนต่าง ๆ ใหม่ตามลำดับที่เหมาะสม การเปลี่ยนผังด้วย CSS ช่วยให้ใช้ HTML ชุดเดิมได้โดยไม่จำเป็นต้องสร้างเนื้อหาซ้ำ
.app-layout {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr) 260px;
  grid-template-areas:
    "header header header"
    "sidebar main aside"
    "footer footer footer";
  gap: 16px;
}

@media (max-width: 900px) {
  .app-layout {
    grid-template-columns: 200px minmax(0, 1fr);
    grid-template-areas:
      "header header"
      "sidebar main"
      "aside aside"
      "footer footer";
  }
}

@media (max-width: 620px) {
  .app-layout {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "header"
      "main"
      "sidebar"
      "aside"
      "footer";
  }
}
✅ หลักการ Responsive ที่ควรจำกำหนดผังหลักสำหรับหน้าจอใหญ่ก่อน แล้วใช้ Media Queries ปรับคอลัมน์และพื้นที่ตามความกว้างที่เหมาะสม อย่าลืมทดสอบทั้งหน้าจอขนาดกลางและมือถือจริง เพราะความยาวของข้อความและขนาดภาพอาจส่งผลต่อเลย์เอาต์

6. ควบคุมขนาดด้วย minmax() และ min-width: 0

ปัญหาที่พบบ่อยใน Grid คือคอลัมน์ขยายกว้างเกินพื้นที่ เพราะเนื้อหาภายในมีขนาดขั้นต่ำโดยปริยาย เช่น URL ยาว ตารางกว้าง หรือข้อความที่ตัดบรรทัดไม่ได้ การใช้ `minmax(0, 1fr)` ช่วยอนุญาตให้แทร็กหดได้ถึงศูนย์ในส่วนขั้นต่ำ และการกำหนด `min-width: 0` ให้รายการช่วยลดการขยายเกินคาด อย่างไรก็ตาม เนื้อหาที่ยาวมากอาจยังต้องจัดการด้วย `overflow-wrap` หรือกฎอื่นตามชนิดข้อมูล
.content-grid {
  display: grid;
  grid-template-columns: 240px minmax(0, 1fr);
  gap: 20px;
}

.content-grid > * {
  min-width: 0;
}

.long-text {
  overflow-wrap: anywhere;
}

.content-grid img {
  max-width: 100%;
  height: auto;
}

7. สร้างการ์ดที่มีขนาดไม่เท่ากันอย่างเป็นระบบ

ในหน้าแสดงบทความหรือสินค้า เราอาจต้องการการ์ดเด่นที่ใหญ่กว่ารายการอื่น สามารถใช้ `grid-column: span 2` เพื่อขยายการ์ดให้กินสองคอลัมน์ และใช้ `grid-row: span 2` เพื่อเพิ่มความสูงตามจำนวนแถวที่ครอบครอง แต่ควรตรวจสอบขนาดหน้าจอและจำนวนคอลัมน์ที่เหลือ เพื่อป้องกันเลย์เอาต์ดูไม่สมดุล
.article-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: minmax(160px, auto);
  gap: 18px;
}

.article-card--featured {
  grid-column: span 2;
  grid-row: span 2;
}

@media (max-width: 700px) {
  .article-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .article-card--featured {
    grid-column: auto;
    grid-row: auto;
  }
}
เมื่อหน้าจอเล็กลง ตัวอย่างนี้จะเปลี่ยนเป็นคอลัมน์เดียวและยกเลิกการขยายการ์ดเด่น เพื่อให้แต่ละรายการใช้พื้นที่เต็มแถวตามลำดับ การออกแบบลักษณะนี้ช่วยลดความจำเป็นในการเลื่อนแนวนอนบนอุปกรณ์ขนาดเล็ก

8. ใช้ auto-placement และ grid-auto-flow

หากไม่ได้กำหนดตำแหน่งให้ทุก Grid Item เบราว์เซอร์จะจัดวางรายการที่เหลือตามกฎ Auto-placement โดยอัตโนมัติ ค่าเริ่มต้นของ `grid-auto-flow` คือ `row` ซึ่งวางรายการตามแถว หากใช้ `column` จะจัดวางตามคอลัมน์ ส่วน `dense` อนุญาตให้เบราว์เซอร์ย้อนกลับไปเติมช่องว่างที่เหลืออยู่ได้ แต่ลำดับที่มองเห็นอาจไม่ตรงกับลำดับการอ่านตาม DOM จึงควรใช้ด้วยความระมัดระวัง
.gallery {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: minmax(120px, auto);
  grid-auto-flow: row;
  gap: 14px;
}

/* ใช้ dense เมื่อยอมรับการเติมช่องว่างได้ */
.gallery--dense {
  grid-auto-flow: row dense;
}
⚠️ อย่าพึ่งพาตำแหน่งที่มองเห็นเพียงอย่างเดียวการจัดวางด้วย Grid ไม่ควรทำให้ลำดับภาพที่ผู้ใช้เห็นขัดแย้งกับลำดับการอ่านของเอกสาร โดยเฉพาะเมื่อใช้ `dense` หรือกำหนดตำแหน่งรายการด้วยตนเอง ควรจัดลำดับ HTML ให้มีความหมายและใช้งานด้วยคีย์บอร์ดได้อย่างสมเหตุสมผล

9. CSS Grid ทำงานร่วมกับ Flexbox อย่างไร

ไม่จำเป็นต้องเลือกระหว่าง Grid กับ Flexbox เพียงอย่างเดียว แนวทางที่ใช้ได้ดีคือใช้ Grid สำหรับเลย์เอาต์หลักของหน้า เช่น การแบ่ง Sidebar และ Main Content จากนั้นใช้ Flexbox ภายในส่วนย่อย เช่น การจัดปุ่มในแถบเครื่องมือ การวางไอคอนข้างข้อความ หรือการจัดเนื้อหาในส่วนหัวของการ์ด
สถานการณ์แนวทางที่เหมาะสมเหตุผล
แบ่งหน้าเป็น Sidebar และ MainCSS Gridควบคุมแถวและคอลัมน์ของหน้าได้พร้อมกัน
จัดปุ่มในแถบเครื่องมือFlexboxจัดองค์ประกอบตามแกนหลักได้สะดวก
สร้างตารางการ์ดสินค้าCSS Gridควบคุมจำนวนคอลัมน์และช่องว่างได้
จัดไอคอนกับข้อความในปุ่มFlexboxเหมาะกับองค์ประกอบย่อยที่เรียงตามแนวเดียว
สร้าง Dashboard พร้อมการ์ดข้อมูลGrid ร่วมกับ FlexboxGrid จัดหน้า ส่วน Flexbox จัดเนื้อหาภายใน

10. ข้อผิดพลาดที่พบบ่อยในการใช้ CSS Grid

กำหนด grid-template-columns แล้วแต่ยังมีแถบเลื่อนแนวนอน
ตรวจสอบเนื้อหาภายในคอลัมน์ โดยเฉพาะข้อความยาว ตาราง รูปภาพ และองค์ประกอบที่มีความกว้างคงที่ ลองใช้ `minmax(0, 1fr)` กับคอลัมน์ที่ยืดหยุ่น กำหนด `min-width: 0` ให้รายการที่จำเป็น และจัดการข้อความด้วย `overflow-wrap: anywhere` หากเหมาะกับเนื้อหา
พื้นที่ใน grid-template-areas ไม่ตรงกัน
ตรวจสอบว่าแต่ละแถวมีจำนวนเซลล์เท่ากัน และชื่อพื้นที่เดียวกันเชื่อมต่อกันเป็นรูปสี่เหลี่ยมผืนผ้า หลีกเลี่ยงการกำหนดชื่อพื้นที่เดียวกันเป็นส่วนที่แยกจากกันหรือมีรูปทรงไม่ถูกต้อง
การ์ดเด่นล้นออกจากเลย์เอาต์บนมือถือ
ภายใน Media Query สำหรับหน้าจอเล็ก ให้เปลี่ยนจำนวนคอลัมน์และยกเลิกการขยายรายการ เช่น กำหนด `grid-column: auto` และ `grid-row: auto` สำหรับการ์ดเด่น เพื่อให้รายการกลับมาอยู่ในแถวตามปกติ
ทำไมใช้ justify-content แล้วไม่เห็นความเปลี่ยนแปลง?
`justify-content` และ `align-content` ใช้จัดตำแหน่งชุดแทร็กเมื่อมีพื้นที่ว่างในคอนเทนเนอร์ หาก Grid เต็มพื้นที่พอดีหรือแทร็กขยายเต็มแล้ว อาจไม่มีพื้นที่เหลือให้เห็นผลชัดเจน หากต้องการจัดรายการภายในแต่ละเซลล์ ให้ตรวจสอบ `justify-items`, `align-items` หรือ `place-items` แทน

11. แนวทางออกแบบ Grid ให้ดูแลรักษาง่าย

ควรตั้งชื่อคลาสตามหน้าที่ขององค์ประกอบ ใช้ `minmax(0, 1fr)` ในคอลัมน์เนื้อหาที่ยืดหยุ่น และเลือกใช้ `grid-template-areas` เมื่อผังหน้าเป็นส่วนสำคัญที่ต้องอ่านเข้าใจได้รวดเร็ว หลีกเลี่ยงการกำหนดตำแหน่งทุกรายการโดยไม่มีความจำเป็น เพราะจะทำให้เพิ่มหรือลดเนื้อหาในอนาคตได้ยาก ควรให้รายการทั่วไปไหลอัตโนมัติ และกำหนดตำแหน่งเฉพาะรายการที่ต้องการความโดดเด่น
✅ Checklist ก่อนนำขึ้นใช้งานตรวจสอบจำนวนคอลัมน์ในแต่ละขนาดหน้าจอ ตรวจสอบเนื้อหาที่ยาวและรูปภาพ ทดสอบกรณีมีรายการจำนวนต่างกัน ตรวจสอบลำดับการอ่านและการใช้งานด้วยคีย์บอร์ด รวมถึงเปิดเครื่องมือ Developer Tools เพื่อตรวจดูเส้น Grid เมื่อเลย์เอาต์ไม่เป็นไปตามที่คาด

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

สร้างหน้า Dashboard ที่มี Header, Sidebar, Main Content, Summary Panel และ Footer โดยใช้ `grid-template-areas` จากนั้นเพิ่มการ์ดข้อมูลอย่างน้อยหกใบ กำหนดให้การ์ดหนึ่งใบกินพื้นที่สองคอลัมน์ แล้วปรับเลย์เอาต์ให้เหลือคอลัมน์เดียวเมื่อหน้าจอมีความกว้างไม่เกิน 620 พิกเซล สุดท้ายทดลองใส่ข้อความยาวและตรวจสอบว่าไม่มีแถบเลื่อนแนวนอนโดยไม่จำเป็น

13. ตัวอย่างฉบับเต็ม: Responsive Business Dashboard

ตัวอย่างต่อไปนี้เป็นหน้า HTML แบบเต็ม สามารถบันทึกเป็น `index.html` แล้วเปิดด้วยเบราว์เซอร์ได้ทันที ตัวอย่างใช้ Grid Areas สำหรับโครงสร้างหน้า ใช้ Grid ซ้อนกันสำหรับการ์ดสรุปข้อมูล และใช้ Flexbox ในแถบหัวเรื่องและส่วนแสดงสถานะ มี Media Queries สำหรับหน้าจอขนาดกลางและมือถือ
<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Responsive Business Dashboard</title>
  <style>
    * { box-sizing: border-box; }
    :root {
      --page: #f1f5f9;
      --surface: #ffffff;
      --ink: #172033;
      --muted: #64748b;
      --border: #dbe3ef;
      --primary: #2563eb;
      --success: #15803d;
    }
    body {
      margin: 0;
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      background: var(--page);
      color: var(--ink);
      line-height: 1.5;
    }
    .dashboard {
      min-height: 100vh;
      display: grid;
      grid-template-columns: 230px minmax(0, 1fr);
      grid-template-rows: auto minmax(0, 1fr) auto;
      grid-template-areas:
        "sidebar header"
        "sidebar main"
        "sidebar footer";
      gap: 18px;
      padding: 18px;
      max-width: 1500px;
      margin: 0 auto;
    }
    .sidebar {
      grid-area: sidebar;
      align-self: stretch;
      padding: 20px;
      background: #172554;
      color: #fff;
      border-radius: 18px;
    }
    .brand { margin: 0 0 22px; font-size: 1.2rem; }
    .nav-list {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 0;
      margin: 0;
      list-style: none;
    }
    .nav-list a {
      display: block;
      padding: 10px 12px;
      color: #dbeafe;
      text-decoration: none;
      border-radius: 9px;
    }
    .nav-list a:hover,
    .nav-list a:focus-visible {
      background: #1e40af;
      color: #fff;
      outline-offset: 3px;
    }
    .header {
      grid-area: header;
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 12px;
      padding: 20px 24px;
      border: 1px solid var(--border);
      border-radius: 16px;
      background: var(--surface);
    }
    .header h1 { margin: 0; font-size: clamp(1.35rem, 3vw, 2rem); }
    .header p { margin: 4px 0 0; color: var(--muted); }
    .date-label {
      padding: 8px 12px;
      border-radius: 999px;
      background: #dbeafe;
      color: #1d4ed8;
      font-size: .9rem;
      font-weight: 700;
    }
    .main { grid-area: main; min-width: 0; }
    .section-title { margin: 0 0 14px; font-size: 1.2rem; }
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 14px;
    }
    .stat-card {
      min-width: 0;
      padding: 18px;
      border: 1px solid var(--border);
      border-radius: 15px;
      background: var(--surface);
    }
    .stat-label { margin: 0; color: var(--muted); font-size: .92rem; }
    .stat-value { margin: 8px 0; font-size: clamp(1.25rem, 2.5vw, 1.8rem); font-weight: 800; overflow-wrap: anywhere; }
    .stat-change { margin: 0; color: var(--success); font-size: .88rem; }
    .content-grid {
      display: grid;
      grid-template-columns: minmax(0, 1.5fr) minmax(240px, 1fr);
      gap: 16px;
      margin-top: 18px;
    }
    .panel {
      min-width: 0;
      padding: 20px;
      border: 1px solid var(--border);
      border-radius: 15px;
      background: var(--surface);
    }
    .panel h2 { margin: 0 0 14px; font-size: 1.1rem; }
    .activity-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
    .activity-item {
      display: flex;
      align-items: flex-start;
      gap: 12px;
      padding-bottom: 12px;
      border-bottom: 1px solid #e2e8f0;
    }
    .activity-item:last-child { padding-bottom: 0; border-bottom: 0; }
    .activity-icon {
      display: grid;
      place-items: center;
      flex: 0 0 40px;
      width: 40px;
      height: 40px;
      border-radius: 11px;
      background: #dbeafe;
    }
    .activity-item p { margin: 0; }
    .activity-item small { color: var(--muted); }
    .progress-row { margin: 14px 0; }
    .progress-label { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 7px; font-size: .92rem; }
    .progress-track { height: 9px; border-radius: 999px; background: #e2e8f0; overflow: hidden; }
    .progress-fill { height: 100%; border-radius: inherit; background: var(--primary); }
    .footer {
      grid-area: footer;
      padding: 12px 4px;
      color: var(--muted);
      font-size: .9rem;
    }
    @media (max-width: 1050px) {
      .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .content-grid { grid-template-columns: minmax(0, 1fr); }
    }
    @media (max-width: 700px) {
      .dashboard {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: auto auto minmax(0, 1fr) auto;
        grid-template-areas:
          "header"
          "sidebar"
          "main"
          "footer";
        padding: 12px;
        gap: 12px;
      }
      .sidebar { padding: 14px; }
      .brand { margin-bottom: 12px; }
      .nav-list { flex-direction: row; flex-wrap: wrap; }
      .nav-list a { padding: 8px 10px; }
      .header { padding: 16px; }
      .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    }
    @media (max-width: 420px) {
      .stats-grid { grid-template-columns: minmax(0, 1fr); }
      .date-label { width: 100%; text-align: center; }
    }
  </style>
</head>
<body>
  <div class="dashboard">
    <aside class="sidebar">
      <h2 class="brand">LevelLance</h2>
      <nav aria-label="เมนูหลัก">
        <ul class="nav-list">
          <li><a href="#overview">ภาพรวม</a></li>
          <li><a href="#activity">กิจกรรม</a></li>
          <li><a href="#performance">ประสิทธิภาพ</a></li>
        </ul>
      </nav>
    </aside>

    <header class="header">
      <div>
        <h1>Business Dashboard</h1>
        <p>ภาพรวมตัวอย่างข้อมูลธุรกิจ</p>
      </div>
      <span class="date-label">ข้อมูลตัวอย่างเพื่อการเรียนรู้</span>
    </header>

    <main class="main" id="overview">
      <h2 class="section-title">สรุปผลการดำเนินงาน</h2>
      <section class="stats-grid" aria-label="ตัวชี้วัดสำคัญ">
        <article class="stat-card">
          <p class="stat-label">ยอดขายตัวอย่าง</p>
          <p class="stat-value">฿128,450</p>
          <p class="stat-change">เพิ่มขึ้น 8.2%</p>
        </article>
        <article class="stat-card">
          <p class="stat-label">คำสั่งซื้อ</p>
          <p class="stat-value">324</p>
          <p class="stat-change">เพิ่มขึ้น 4.5%</p>
        </article>
        <article class="stat-card">
          <p class="stat-label">ลูกค้าตัวอย่าง</p>
          <p class="stat-value">1,280</p>
          <p class="stat-change">เพิ่มขึ้น 6.1%</p>
        </article>
        <article class="stat-card">
          <p class="stat-label">อัตราสำเร็จ</p>
          <p class="stat-value">96.8%</p>
          <p class="stat-change">อยู่ในเกณฑ์เป้าหมาย</p>
        </article>
      </section>

      <section class="content-grid">
        <article class="panel" id="activity">
          <h2>กิจกรรมล่าสุด</h2>
          <ul class="activity-list">
            <li class="activity-item">
              <span class="activity-icon" aria-hidden="true">🛒</span>
              <div><p><strong>มีคำสั่งซื้อใหม่</strong></p><small>ตัวอย่างกิจกรรมสำหรับแสดงรายการล่าสุด</small></div>
            </li>
            <li class="activity-item">
              <span class="activity-icon" aria-hidden="true">👤</span>
              <div><p><strong>มีลูกค้าใหม่</strong></p><small>ตัวอย่างการแจ้งเตือนภายในแดชบอร์ด</small></div>
            </li>
            <li class="activity-item">
              <span class="activity-icon" aria-hidden="true">📦</span>
              <div><p><strong>อัปเดตข้อมูลสินค้า</strong></p><small>ตัวอย่างการเปลี่ยนแปลงข้อมูลระบบ</small></div>
            </li>
          </ul>
        </article>

        <article class="panel" id="performance">
          <h2>เป้าหมายรายเดือน</h2>
          <div class="progress-row">
            <div class="progress-label"><span>ยอดขาย</span><strong>72%</strong></div>
            <div class="progress-track" role="progressbar" aria-label="เป้าหมายยอดขาย" aria-valuemin="0" aria-valuemax="100" aria-valuenow="72"><div class="progress-fill" style="width:72%"></div></div>
          </div>
          <div class="progress-row">
            <div class="progress-label"><span>คำสั่งซื้อ</span><strong>58%</strong></div>
            <div class="progress-track" role="progressbar" aria-label="เป้าหมายคำสั่งซื้อ" aria-valuemin="0" aria-valuemax="100" aria-valuenow="58"><div class="progress-fill" style="width:58%"></div></div>
          </div>
          <div class="progress-row">
            <div class="progress-label"><span>ความพึงพอใจ</span><strong>91%</strong></div>
            <div class="progress-track" role="progressbar" aria-label="เป้าหมายความพึงพอใจ" aria-valuemin="0" aria-valuemax="100" aria-valuenow="91"><div class="progress-fill" style="width:91%"></div></div>
          </div>
        </article>
      </section>
    </main>

    <footer class="footer">Responsive Business Dashboard — ตัวอย่างการประยุกต์ใช้ CSS Grid และ Flexbox</footer>
  </div>
</body>
</html>

14. สรุปบทเรียน EP11

CSS Grid ขั้นสูงช่วยให้สร้างหน้าเว็บที่มีโครงสร้างซับซ้อนได้อย่างเป็นระบบ คุณได้เรียนรู้ Grid Lines, การใช้ `span`, Named Lines, `grid-template-areas`, Media Queries, `minmax()` และ Auto-placement รวมถึงวิธีใช้ Grid ร่วมกับ Flexbox เพื่อแบ่งหน้าที่การจัดเลย์เอาต์ให้เหมาะสมกับแต่ละส่วนของเว็บไซต์
🎯 ภารกิจหลังเรียนนำตัวอย่าง Dashboard ไปปรับใช้กับข้อมูลของคุณเอง เพิ่มการ์ดข้อมูลอย่างน้อยหนึ่งใบ ทดลองเปลี่ยนผังบนหน้าจอมือถือ และตรวจสอบว่าเนื้อหายังคงอ่านง่าย ไม่มีการซ้อนทับโดยไม่ตั้งใจ และใช้งานด้วยคีย์บอร์ดได้
เลย์เอาต์ที่แข็งแรงเริ่มจากโครงสร้างที่ชัดเจน แล้วจึงเพิ่มรายละเอียดเพื่อให้เหมาะกับผู้ใช้และอุปกรณ์แต่ละประเภท
อ่านจนถึงระยะ 500px ก่อนท้ายหน้าและอยู่ในหน้านี้อย่างน้อย 5 นาที เพื่อรับ EXP วันละครั้ง

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

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