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

HTML และ CSS EP19: CSS Grid ขั้นสูง — สร้าง Dashboard, Sidebar และ Layout เว็บไซต์จริง

HTML & CSS

บทเรียนต่อเนื่องจาก EP18 สำหรับผู้เรียน HTML และ CSS โดยฝึกใช้ CSS Grid ขั้นสูงเพื่อสร้างโครงหน้า Dashboard, Sidebar, Header, Content Area และ Footer เรียนรู้ grid-template-areas, การจัดวางข้ามแถวและคอลัมน์, minmax(), การซ้อน Grid, Responsive Layout และการสร้าง Dashboard ที่ทดลองใช้งานได้จริง

EP19 — CSS Grid ขั้นสูง: จากกริดการ์ดสู่โครงสร้างเว็บไซต์จริง

จาก EP18 เราได้เรียนรู้การสร้างกริดด้วย `display: grid`, `grid-template-columns`, `repeat()`, `minmax()` และ `gap` แล้ว ในบทนี้เราจะนำความรู้เหล่านั้นมาต่อยอดเป็นโครงสร้างเว็บไซต์ที่มีหลายส่วน เช่น Dashboard สำหรับผู้ดูแลระบบ แถบนำทางด้านข้าง พื้นที่แสดงข้อมูล และส่วนสรุปผล โดยเน้นการเขียน CSS ที่เข้าใจง่าย ปรับเปลี่ยนได้ และรองรับทั้งหน้าจอคอมพิวเตอร์ แท็บเล็ต และมือถือ
สิ่งที่คุณจะทำได้เมื่อจบบทเรียนคุณจะสามารถออกแบบโครงสร้างหน้าด้วย grid-template-areas กำหนดคอลัมน์ที่ยืดหยุ่นด้วย minmax(0, 1fr) ใช้ grid-column และ grid-row จัดพื้นที่พิเศษ ซ้อน Grid ภายในส่วนต่าง ๆ และปรับโครงหน้าให้เหมาะกับมือถือด้วย Media Queries

1. วางแผนโครงสร้างก่อนเขียน CSS

ก่อนสร้าง Dashboard ให้แยกหน้าจอออกเป็นพื้นที่หลัก ได้แก่ Header สำหรับชื่อระบบและการดำเนินการ Sidebar สำหรับเมนูนำทาง Main สำหรับเนื้อหาหลัก และ Footer สำหรับข้อมูลท้ายหน้า เมื่อแบ่งพื้นที่ชัดเจนแล้วจึงเลือกใช้ CSS Grid เพื่อกำหนดความสัมพันธ์ระหว่างพื้นที่เหล่านั้น วิธีนี้ช่วยลดการใช้ตำแหน่งแบบตายตัว และทำให้ปรับเลย์เอาต์สำหรับหน้าจอขนาดเล็กได้ง่ายขึ้น
พื้นที่หน้าที่แนวทางการจัดวาง
Headerชื่อระบบ ปุ่มค้นหา และการดำเนินการครอบคลุมความกว้างด้านบน
Sidebarเมนูและลิงก์นำทางคอลัมน์ด้านซ้ายบนหน้าจอใหญ่
Mainการ์ดสรุป สถิติ และรายการข้อมูลใช้พื้นที่ที่เหลือด้วย minmax(0, 1fr)
Footerข้อมูลลิขสิทธิ์หรือรายละเอียดระบบแสดงท้ายเนื้อหาหรือเต็มความกว้าง

2. ใช้ grid-template-areas เพื่อกำหนดโครงหน้า

`grid-template-areas` ทำให้เราออกแบบเลย์เอาต์โดยมองเห็นชื่อพื้นที่ได้จาก CSS โดยตรง ตัวอย่างเช่น `"header header"` หมายถึง Header กินพื้นที่สองคอลัมน์ ส่วน `"sidebar main"` หมายถึง Sidebar อยู่คอลัมน์ซ้ายและ Main อยู่คอลัมน์ขวา ชื่อพื้นที่เดียวกันที่อยู่ติดกันจะถูกรวมเป็นพื้นที่เดียว และแต่ละแถวต้องมีจำนวนช่องสอดคล้องกับโครงสร้างกริด
<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ทดลอง Grid Areas</title>
  <style>
    * { box-sizing: border-box; }
    body { margin: 0; padding: 20px; font-family: system-ui, sans-serif; background: #f1f5f9; }
    .page {
      display: grid;
      grid-template-columns: 210px minmax(0, 1fr);
      grid-template-areas:
        "header header"
        "sidebar main"
        "footer footer";
      gap: 12px;
      max-width: 900px;
      margin: auto;
    }
    .area { padding: 22px; border-radius: 12px; color: white; font-weight: 700; }
    .header { grid-area: header; background: #1d4ed8; }
    .sidebar { grid-area: sidebar; background: #0f766e; }
    .main { grid-area: main; background: #7c3aed; min-height: 180px; }
    .footer { grid-area: footer; background: #334155; }
    @media (max-width: 600px) {
      .page {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "header" "main" "sidebar" "footer";
      }
    }
  </style>
</head>
<body>
  <div class="page">
    <header class="area header">Header</header>
    <aside class="area sidebar">Sidebar</aside>
    <main class="area main">Main Content</main>
    <footer class="area footer">Footer</footer>
  </div>
</body>
</html>

3. ทำความเข้าใจ minmax(0, 1fr) และปัญหาคอลัมน์ล้น

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

.main-content {
  min-width: 0;
}

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

.data-table-wrapper {
  max-width: 100%;
  overflow-x: auto;
}

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

4. จัดองค์ประกอบให้กินหลายแถวหรือหลายคอลัมน์

ใน Dashboard บางส่วนอาจต้องเด่นกว่าส่วนอื่น เช่น การ์ดสรุปยอดขายที่กินพื้นที่สองคอลัมน์ หรือกราฟที่มีความสูงมากกว่าการ์ดทั่วไป คุณสามารถใช้ `grid-column: span 2` หรือ `grid-row: span 2` เพื่อกำหนดให้รายการครอบคลุมหลายแทร็กได้ ควรตรวจสอบพื้นที่ที่เหลือและปรับค่า span เมื่อหน้าจอแคบลง เพื่อไม่ให้เกิดการจัดวางที่แน่นหรืออ่านยาก
<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Grid Span Demo</title>
  <style>
    * { box-sizing: border-box; }
    body { margin: 0; padding: 20px; font-family: system-ui, sans-serif; background: #f8fafc; }
    .grid {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      grid-auto-rows: minmax(100px, auto);
      gap: 12px;
      max-width: 800px;
      margin: auto;
    }
    .card { padding: 20px; border-radius: 12px; background: #dbeafe; color: #1e3a8a; }
    .card h2 { margin-top: 0; font-size: 1.1rem; }
    .featured { grid-column: span 2; grid-row: span 2; background: #1d4ed8; color: white; }
    @media (max-width: 600px) {
      .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .featured { grid-column: span 2; grid-row: auto; }
    }
    @media (max-width: 380px) {
      .grid { grid-template-columns: minmax(0, 1fr); }
      .featured { grid-column: auto; }
    }
  </style>
</head>
<body>
  <div class="grid">
    <article class="card featured"><h2>พื้นที่แนะนำ</h2><p>รายการนี้กินพื้นที่สองคอลัมน์และสองแถวบนหน้าจอขนาดใหญ่</p></article>
    <article class="card"><h2>รายการ A</h2><p>รายละเอียดสั้น ๆ</p></article>
    <article class="card"><h2>รายการ B</h2><p>รายละเอียดสั้น ๆ</p></article>
    <article class="card"><h2>รายการ C</h2><p>รายละเอียดสั้น ๆ</p></article>
    <article class="card"><h2>รายการ D</h2><p>รายละเอียดสั้น ๆ</p></article>
  </div>
</body>
</html>

5. ซ้อน Grid เพื่อสร้างส่วนประกอบที่เป็นอิสระ

Grid Container หนึ่งสามารถอยู่ภายใน Grid Item ของอีกกริดได้ วิธีนี้เหมาะกับการสร้างหน้า Dashboard ที่ใช้ Grid ชั้นนอกกำหนด Sidebar และ Main แล้วใช้ Grid ชั้นในจัดการ์ดสถิติ ตารางข้อมูล หรือรายการกิจกรรม ข้อดีคือแต่ละส่วนสามารถปรับคอลัมน์และระยะห่างของตัวเองได้ โดยไม่ต้องให้ทุกส่วนใช้โครงสร้างเดียวกัน
.dashboard {
  display: grid;
  grid-template-columns: 230px minmax(0, 1fr);
  gap: 24px;
}

.dashboard-main {
  min-width: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
  gap: 24px;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 16px;
}

.activity-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}

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

6. ปรับจำนวนคอลัมน์ตามพื้นที่จริง

การ์ดสถิติและการ์ดข้อมูลมักไม่จำเป็นต้องมีจำนวนคอลัมน์คงที่ คุณสามารถใช้ `repeat(auto-fit, minmax(170px, 1fr))` เพื่อให้เบราว์เซอร์เลือกจำนวนคอลัมน์ตามพื้นที่ที่มีอยู่ เมื่อพื้นที่แคบลง รายการจะย้ายไปแถวถัดไปโดยอัตโนมัติ เทคนิคนี้ช่วยลดจำนวน Media Queries ที่ต้องเขียน แต่ยังควรตรวจสอบขนาดขั้นต่ำให้เหมาะกับเนื้อหาและอุปกรณ์จริง
รูปแบบความหมายกรณีใช้งาน
repeat(3, 1fr)สร้างสามคอลัมน์แบ่งพื้นที่เท่ากันหน้าจอที่ต้องการจำนวนคอลัมน์คงที่
repeat(auto-fit, minmax(170px, 1fr))สร้างจำนวนคอลัมน์ตามพื้นที่และขยายแทร็กที่มีรายการการ์ดสถิติหรือการ์ดสินค้า
repeat(auto-fill, minmax(170px, 1fr))รักษาแทร็กที่สร้างได้แม้บางแทร็กไม่มีรายการเลย์เอาต์ที่ต้องการคงรูปแบบแทร็ก
minmax(0, 1fr)ให้แทร็กยืดหยุ่นและมีค่าต่ำสุดเป็นศูนย์คอลัมน์เนื้อหาหลักที่ต้องป้องกันการล้น

7. ออกแบบ Responsive Layout สำหรับหน้าจอหลายขนาด

Responsive Design ไม่ได้หมายถึงการย่อทุกอย่างให้เล็กลง แต่คือการจัดโครงสร้างให้เหมาะกับพื้นที่และวิธีใช้งาน บนเดสก์ท็อป Sidebar อาจอยู่ข้าง Main ส่วนบนมือถือ Sidebar อาจย้ายลงใต้เนื้อหาหรือเปลี่ยนเป็นเมนูที่เข้าถึงได้ง่าย ให้กำหนด Breakpoint ตามจุดที่เนื้อหาเริ่มอึดอัด แทนการยึดติดกับขนาดอุปกรณ์รุ่นใดรุ่นหนึ่ง
.dashboard-layout {
  display: grid;
  grid-template-columns: 240px minmax(0, 1fr);
  grid-template-areas: "sidebar main";
  gap: 24px;
}

.sidebar { grid-area: sidebar; }
.dashboard-main { grid-area: main; min-width: 0; }

@media (max-width: 900px) {
  .dashboard-layout {
    grid-template-columns: 200px minmax(0, 1fr);
    gap: 16px;
  }
}

@media (max-width: 680px) {
  .dashboard-layout {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "main"
      "sidebar";
  }
}
อย่ากำหนด Breakpoint โดยไม่ทดสอบตัวเลข 900px และ 680px เป็นเพียงค่าตัวอย่างสำหรับบทเรียน ควรปรับตามความกว้างจริงของเนื้อหา ทดสอบด้วยหน้าต่างที่แคบและกว้างหลายระดับ และตรวจสอบว่าข้อความ ปุ่ม และเมนูไม่ชนกัน

8. โปรเจกต์จริง: Interactive Dashboard ด้วย CSS Grid

ตัวอย่างนี้รวมแนวคิดของบทเรียนเข้าด้วยกัน ได้แก่ โครงหน้าแบบ Grid, Sidebar, Header, การ์ดสถิติที่ปรับจำนวนคอลัมน์อัตโนมัติ, รายการกิจกรรม และปุ่มเปลี่ยนธีม สามารถบันทึกเป็น `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="ตัวอย่าง Dashboard ด้วย CSS Grid สำหรับฝึก HTML และ CSS">
  <title>Dashboard Lab — CSS Grid</title>
  <style>
    :root {
      color-scheme: light;
      --page: #f1f5f9;
      --surface: #ffffff;
      --text: #172033;
      --muted: #64748b;
      --border: #dbe3ee;
      --primary: #2563eb;
      --primary-soft: #dbeafe;
      --green: #0f766e;
      --purple: #7c3aed;
      --orange: #c2410c;
      --radius: 15px;
    }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      background: var(--page);
      color: var(--text);
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      line-height: 1.55;
    }
    button, a { font: inherit; }
    button:focus-visible, a:focus-visible {
      outline: 3px solid #f59e0b;
      outline-offset: 3px;
    }
    .app {
      min-height: 100vh;
      display: grid;
      grid-template-columns: 235px minmax(0, 1fr);
      grid-template-rows: auto minmax(0, 1fr) auto;
      grid-template-areas:
        "sidebar header"
        "sidebar main"
        "sidebar footer";
    }
    .sidebar {
      grid-area: sidebar;
      padding: 24px 16px;
      background: #172554;
      color: #f8fafc;
    }
    .brand {
      margin: 0 0 28px;
      font-size: 1.25rem;
      font-weight: 800;
      letter-spacing: .2px;
    }
    .brand span { color: #93c5fd; }
    .nav-title {
      margin: 22px 10px 8px;
      color: #cbd5e1;
      font-size: .75rem;
      font-weight: 800;
      text-transform: uppercase;
      letter-spacing: .08em;
    }
    .nav-list {
      display: grid;
      gap: 6px;
      margin: 0;
      padding: 0;
      list-style: none;
    }
    .nav-list a {
      display: block;
      padding: 10px 12px;
      color: #e2e8f0;
      text-decoration: none;
      border-radius: 9px;
    }
    .nav-list a:hover, .nav-list a[aria-current="page"] {
      background: #1d4ed8;
      color: #fff;
    }
    .header {
      grid-area: header;
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: space-between;
      gap: 16px;
      padding: 20px 28px;
      background: var(--surface);
      border-bottom: 1px solid var(--border);
    }
    .header h1 { margin: 0; font-size: clamp(1.3rem, 2.5vw, 1.8rem); }
    .header p { margin: 4px 0 0; color: var(--muted); font-size: .92rem; }
    .theme-toggle {
      padding: 9px 13px;
      border: 1px solid var(--border);
      border-radius: 10px;
      background: var(--surface);
      color: var(--text);
      cursor: pointer;
      font-weight: 700;
    }
    .main {
      grid-area: main;
      min-width: 0;
      padding: 28px;
    }
    .section-head {
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
      align-items: end;
      gap: 12px;
      margin-bottom: 18px;
    }
    .section-head h2 { margin: 0; font-size: 1.3rem; }
    .section-head p { margin: 4px 0 0; color: var(--muted); }
    .stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr));
      gap: 16px;
      margin-bottom: 24px;
    }
    .stat-card {
      min-width: 0;
      padding: 18px;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      background: var(--surface);
    }
    .stat-label { margin: 0; color: var(--muted); font-size: .9rem; }
    .stat-value { margin: 8px 0 4px; font-size: clamp(1.4rem, 2.8vw, 2rem); font-weight: 800; overflow-wrap: anywhere; }
    .stat-note { margin: 0; color: var(--green); font-size: .84rem; }
    .content-grid {
      display: grid;
      grid-template-columns: minmax(0, 1.35fr) minmax(220px, 1fr);
      gap: 18px;
      align-items: start;
    }
    .panel {
      min-width: 0;
      padding: 20px;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      background: var(--surface);
    }
    .panel h2 { margin: 0 0 14px; font-size: 1.08rem; }
    .chart {
      display: grid;
      grid-template-columns: repeat(7, minmax(0, 1fr));
      align-items: end;
      gap: 10px;
      min-height: 190px;
      padding: 12px 0 0;
      border-bottom: 1px solid var(--border);
    }
    .bar-column {
      display: grid;
      grid-template-rows: minmax(100px, 1fr) auto;
      align-items: end;
      gap: 8px;
      min-width: 0;
      height: 170px;
      text-align: center;
      color: var(--muted);
      font-size: .75rem;
    }
    .bar {
      align-self: end;
      width: 100%;
      min-height: 8px;
      border-radius: 7px 7px 2px 2px;
      background: var(--primary);
    }
    .activity-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
    .activity-item {
      display: grid;
      grid-template-columns: 38px minmax(0, 1fr);
      align-items: start;
      gap: 10px;
      padding-bottom: 12px;
      border-bottom: 1px solid var(--border);
    }
    .activity-item:last-child { padding-bottom: 0; border-bottom: 0; }
    .activity-icon {
      display: grid;
      place-items: center;
      width: 38px;
      height: 38px;
      border-radius: 10px;
      background: var(--primary-soft);
      color: var(--primary);
      font-weight: 800;
    }
    .activity-item strong { display: block; font-size: .9rem; }
    .activity-item small { color: var(--muted); }
    .footer {
      grid-area: footer;
      padding: 16px 28px;
      color: var(--muted);
      font-size: .85rem;
    }
    html[data-theme="dark"] {
      color-scheme: dark;
      --page: #0f172a;
      --surface: #1e293b;
      --text: #f1f5f9;
      --muted: #cbd5e1;
      --border: #475569;
      --primary: #60a5fa;
      --primary-soft: #1e3a8a;
      --green: #5eead4;
      --purple: #c4b5fd;
      --orange: #fdba74;
    }
    html[data-theme="dark"] .sidebar { background: #020617; }
    html[data-theme="dark"] .nav-list a { color: #e2e8f0; }
    html[data-theme="dark"] .nav-list a:hover,
    html[data-theme="dark"] .nav-list a[aria-current="page"] { background: #1d4ed8; }
    @media (max-width: 980px) {
      .app {
        grid-template-columns: 190px minmax(0, 1fr);
      }
      .header, .main { padding: 20px; }
      .content-grid { grid-template-columns: minmax(0, 1fr); }
    }
    @media (max-width: 680px) {
      .app {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: auto auto minmax(0, 1fr) auto;
        grid-template-areas:
          "header"
          "sidebar"
          "main"
          "footer";
      }
      .sidebar { padding: 14px 16px; }
      .brand { margin: 0 0 10px; }
      .nav-title { margin: 12px 0 6px; }
      .nav-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .nav-list a { padding: 9px; font-size: .9rem; }
      .header, .main { padding: 18px 16px; }
      .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
      .stat-card { padding: 14px; }
      .chart { gap: 6px; }
      .panel { padding: 16px; }
    }
    @media (max-width: 360px) {
      .stats { grid-template-columns: minmax(0, 1fr); }
      .nav-list { grid-template-columns: minmax(0, 1fr); }
    }
  </style>
</head>
<body>
  <div class="app">
    <aside class="sidebar" aria-label="เมนูหลัก">
      <p class="brand">Level<span>Lab</span></p>
      <p class="nav-title">เมนูหลัก</p>
      <nav>
        <ul class="nav-list">
          <li><a href="#overview" aria-current="page">ภาพรวม</a></li>
          <li><a href="#reports">รายงาน</a></li>
          <li><a href="#activities">กิจกรรม</a></li>
          <li><a href="#settings">ตั้งค่า</a></li>
        </ul>
      </nav>
    </aside>

    <header class="header">
      <div>
        <h1>Dashboard ภาพรวม</h1>
        <p>ตัวอย่างการจัดวางด้วย CSS Grid</p>
      </div>
      <button class="theme-toggle" id="themeToggle" type="button" aria-pressed="false">เปลี่ยนเป็นโหมดมืด</button>
    </header>

    <main class="main" id="overview">
      <section aria-labelledby="statsTitle">
        <div class="section-head">
          <div>
            <h2 id="statsTitle">สรุปข้อมูล</h2>
            <p>ข้อมูลตัวอย่างสำหรับทดลองเลย์เอาต์</p>
          </div>
        </div>
        <div class="stats">
          <article class="stat-card">
            <p class="stat-label">ผู้เข้าชม</p>
            <p class="stat-value">12,480</p>
            <p class="stat-note">เพิ่มขึ้น 8.2%</p>
          </article>
          <article class="stat-card">
            <p class="stat-label">คำสั่งซื้อ</p>
            <p class="stat-value">864</p>
            <p class="stat-note">เพิ่มขึ้น 4.6%</p>
          </article>
          <article class="stat-card">
            <p class="stat-label">รายได้ตัวอย่าง</p>
            <p class="stat-value">฿48,200</p>
            <p class="stat-note">เพิ่มขึ้น 6.1%</p>
          </article>
          <article class="stat-card">
            <p class="stat-label">งานที่รอดำเนินการ</p>
            <p class="stat-value">17</p>
            <p class="stat-note">ติดตามตามรายการ</p>
          </article>
        </div>
      </section>

      <section class="content-grid" aria-label="รายละเอียดภาพรวม">
        <article class="panel" id="reports">
          <h2>แนวโน้มรายสัปดาห์</h2>
          <p class="stat-label">กราฟแท่งจำลองที่สร้างด้วย CSS</p>
          <div class="chart" role="img" aria-label="กราฟแท่งตัวอย่าง เจ็ดวัน โดยวันพฤหัสบดีมีค่าสูงสุด">
            <div class="bar-column"><div class="bar" style="height: 42%"></div><span>จ.</span></div>
            <div class="bar-column"><div class="bar" style="height: 62%"></div><span>อ.</span></div>
            <div class="bar-column"><div class="bar" style="height: 48%"></div><span>พ.</span></div>
            <div class="bar-column"><div class="bar" style="height: 86%"></div><span>พฤ.</span></div>
            <div class="bar-column"><div class="bar" style="height: 70%"></div><span>ศ.</span></div>
            <div class="bar-column"><div class="bar" style="height: 58%"></div><span>ส.</span></div>
            <div class="bar-column"><div class="bar" style="height: 76%"></div><span>อา.</span></div>
          </div>
        </article>

        <article class="panel" id="activities">
          <h2>กิจกรรมล่าสุด</h2>
          <ul class="activity-list">
            <li class="activity-item">
              <span class="activity-icon" aria-hidden="true">A</span>
              <div><strong>เพิ่มบทเรียนใหม่</strong><small>ตัวอย่างรายการ · 10 นาทีที่แล้ว</small></div>
            </li>
            <li class="activity-item">
              <span class="activity-icon" aria-hidden="true">B</span>
              <div><strong>อัปเดตข้อมูลสินค้า</strong><small>ตัวอย่างรายการ · 35 นาทีที่แล้ว</small></div>
            </li>
            <li class="activity-item">
              <span class="activity-icon" aria-hidden="true">C</span>
              <div><strong>ตรวจสอบรายงาน</strong><small>ตัวอย่างรายการ · 1 ชั่วโมงที่แล้ว</small></div>
            </li>
            <li class="activity-item">
              <span class="activity-icon" aria-hidden="true">D</span>
              <div><strong>ปรับปรุงหน้าเว็บไซต์</strong><small>ตัวอย่างรายการ · 2 ชั่วโมงที่แล้ว</small></div>
            </li>
          </ul>
        </article>
      </section>
    </main>

    <footer class="footer" id="settings">
      <p>Dashboard Lab · ตัวอย่าง HTML และ CSS เพื่อการเรียนรู้เท่านั้น</p>
    </footer>
  </div>

  <script>
    const themeToggle = document.querySelector('#themeToggle');
    themeToggle.addEventListener('click', () => {
      const isDark = document.documentElement.dataset.theme !== 'dark';
      document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
      themeToggle.setAttribute('aria-pressed', String(isDark));
      themeToggle.textContent = isDark ? 'เปลี่ยนเป็นโหมดสว่าง' : 'เปลี่ยนเป็นโหมดมืด';
    });
  </script>
</body>
</html>

9. วิเคราะห์โครงสร้างของโปรเจกต์

ในตัวอย่าง Dashboard เราใช้ Grid ชั้นนอกเพื่อแบ่งพื้นที่ Sidebar, Header, Main และ Footer จากนั้นใช้ Grid อีกชุดภายใน Main เพื่อวางการ์ดสถิติและแผงข้อมูล การใช้ `minmax(0, 1fr)` ช่วยให้พื้นที่เนื้อหาหลักหดตัวได้ ขณะที่ `repeat(auto-fit, minmax(...))` ทำให้การ์ดปรับจำนวนคอลัมน์ตามพื้นที่จริง เมื่อหน้าจอแคบลง Media Queries จะเปลี่ยนโครงสร้างให้เหลือคอลัมน์เดียว และจัดเมนูให้เหมาะกับพื้นที่ที่ลดลง
คุณสมบัติใช้ตรงไหนเหตุผล
grid-template-areasโครงสร้างหลักของแอปเห็นความสัมพันธ์ของแต่ละพื้นที่ชัดเจน
minmax(0, 1fr)คอลัมน์ Mainลดปัญหาเนื้อหาดันคอลัมน์ให้ล้น
repeat(auto-fit, minmax(...))การ์ดสถิติปรับจำนวนคอลัมน์ตามพื้นที่
Nested Gridกราฟและรายการกิจกรรมแต่ละส่วนจัดเลย์เอาต์ได้อย่างอิสระ
Media Queriesหน้าจอขนาดเล็กเปลี่ยนโครงสร้างเมื่อพื้นที่ไม่เพียงพอ

10. ข้อควรระวังในการทำ Dashboard

อย่าพึ่งพาการกำหนดความกว้างและความสูงตายตัวกับทุกส่วน เพราะข้อมูลจริงอาจยาวกว่าตัวอย่าง ใช้ `min-width: 0` กับ Grid Item ที่ต้องหดตัวได้ และกำหนด `overflow-x: auto` ให้ตัวครอบตารางที่กว้างเกินพื้นที่แทนการทำให้ทั้งหน้าเลื่อนแนวนอน ระวังการใช้ `order` หรือการย้ายพื้นที่ด้วย CSS จนลำดับการอ่านไม่ตรงกับลำดับ HTML และอย่าลืมตรวจสอบสี ความคมชัด และสถานะโฟกัสของลิงก์หรือปุ่ม
อย่าใช้ CSS แทนการจัดโครงสร้าง HTML ที่ดีCSS Grid ใช้ควบคุมการจัดวางภาพบนหน้าจอ แต่ไม่ได้ทำให้โครงสร้างเอกสาร ความหมายขององค์ประกอบ หรือการนำทางด้วยคีย์บอร์ดถูกต้องโดยอัตโนมัติ ควรใช้ header, nav, main, aside, section และ footer ให้เหมาะกับหน้าที่จริงของเนื้อหา

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

ทดลองต่อยอดโปรเจกต์ Dashboard โดยปรับเปลี่ยนเนื้อหาและกฎ CSS ด้วยตนเอง เป้าหมายไม่ใช่แค่ทำให้หน้าเว็บดูคล้ายตัวอย่าง แต่ต้องเข้าใจว่าคุณสมบัติแต่ละตัวควบคุมพื้นที่ใด และทำไมเลย์เอาต์จึงเปลี่ยนไปเมื่อขนาดหน้าจอเปลี่ยน
1. เปลี่ยนความกว้าง Sidebar จาก 235px เป็น 260px แล้วสังเกตพื้นที่ Main\n2. เพิ่มการ์ดสถิติอีกสองใบโดยไม่กำหนดตำแหน่งให้แต่ละใบ\n3. เปลี่ยน `repeat(auto-fit, minmax(175px, 1fr))` เป็น `repeat(3, minmax(0, 1fr))` แล้วเปรียบเทียบพฤติกรรม\n4. เพิ่มส่วนแจ้งเตือนใหม่ใน Content Grid\n5. ปรับ Media Query ให้ Sidebar ย้ายไปอยู่ใต้ Main เมื่อหน้าจอแคบ\n6. เพิ่มปุ่มอีกหนึ่งปุ่มและตรวจสอบการใช้งานด้วยคีย์บอร์ด\n7. ลองใส่ URL ยาวในรายการกิจกรรมและแก้ปัญหาการล้นโดยไม่ซ่อนเนื้อหาสำคัญ\n8. ปรับสีของโหมดมืดให้ข้อความอ่านได้ชัดเจน
เกณฑ์ตรวจงานโครงหน้าแสดงพื้นที่หลักครบถ้วน การ์ดสถิติจัดเรียงได้ตามพื้นที่ Sidebar ไม่ทำให้เนื้อหาล้น หน้าจอมือถืออ่านได้โดยไม่ต้องเลื่อนแนวนอนทั้งหน้า ปุ่มเปลี่ยนธีมทำงานได้ และ HTML มีลำดับเนื้อหาที่สมเหตุสมผล

12. คำถามที่พบบ่อย

grid-template-areas จำเป็นต้องใช้กับทุกหน้าเว็บหรือไม่?
ไม่จำเป็น เหมาะกับเลย์เอาต์ที่มีพื้นที่หลักชัดเจน เช่น Dashboard หรือหน้าเว็บที่แบ่ง Header, Sidebar, Main และ Footer หากเป็นกริดการ์ดธรรมดา การกำหนด grid-template-columns อย่างเดียวอาจเพียงพอ
ทำไมควรใช้ minmax(0, 1fr) แทน 1fr ในบางตำแหน่ง?
ค่า 0 ช่วยกำหนดขนาดต่ำสุดของแทร็กให้เป็นศูนย์ ลดโอกาสที่ขนาดขั้นต่ำของเนื้อหาจะดันคอลัมน์ให้กว้างเกินพื้นที่ อย่างไรก็ตาม ยังต้องตรวจสอบรูปภาพ ตาราง และข้อความยาว รวมถึงตั้งค่า overflow หรือการตัดบรรทัดให้เหมาะสม
ควรใช้ Grid ซ้อนกันหลายชั้นหรือไม่?
ใช้ได้เมื่อแต่ละส่วนมีหน้าที่และเลย์เอาต์ของตัวเอง เช่น Grid ชั้นนอกแบ่ง Sidebar กับ Main ส่วน Grid ภายใน Main จัดการ์ดสถิติและแผงข้อมูล แต่ควรตั้งชื่อคลาสให้สื่อความหมายและหลีกเลี่ยงโครงสร้างที่ซับซ้อนโดยไม่มีเหตุผล
Grid สามารถสร้างเมนูบนมือถือแทน Sidebar ได้เลยหรือไม่?
Grid ช่วยเปลี่ยนตำแหน่งและโครงสร้างการจัดวางได้ แต่การสร้างเมนูที่เปิดหรือปิดได้ต้องมี HTML และพฤติกรรมเพิ่มเติม เช่น ปุ่มควบคุมที่เข้าถึงได้ด้วยคีย์บอร์ด และ JavaScript สำหรับเปิดหรือปิดเมนู ตัวอย่างในบทนี้ย้าย Sidebar ไปอยู่ใต้ Header เพื่อแสดงแนวคิดการจัดวางเท่านั้น

13. สรุปบทเรียน EP19

ใน EP19 คุณได้ต่อยอด CSS Grid จากการจัดการ์ดทั่วไปไปสู่การสร้างโครงสร้าง Dashboard ที่ประกอบด้วยหลายพื้นที่ เรียนรู้การใช้ `grid-template-areas` เพื่อสื่อความหมายของเลย์เอาต์ การใช้ `grid-column` และ `grid-row` เพื่อกินพื้นที่หลายแทร็ก การซ้อน Grid สำหรับองค์ประกอบย่อย และการออกแบบ Responsive ด้วย Media Queries ความเข้าใจเหล่านี้ช่วยให้คุณสร้างหน้าระบบหลังบ้าน หน้ารายงาน และหน้าแสดงข้อมูลที่ปรับเปลี่ยนได้โดยไม่ต้องอาศัยตำแหน่งแบบตายตัว
เลย์เอาต์ที่ดีเริ่มจากการเข้าใจความสัมพันธ์ของเนื้อหา แล้วจึงเลือกเครื่องมือจัดวางให้เหมาะสม

แหล่งเรียนรู้เพิ่มเติม

หากต้องการศึกษาคุณสมบัติของ CSS Grid เพิ่มเติม สามารถอ่านเอกสารอ้างอิงเกี่ยวกับการสร้างเลย์เอาต์ การจัดแนว และการกำหนดพื้นที่ได้จาก MDN Web Docs ซึ่งมีตัวอย่างและรายละเอียดของคุณสมบัติแต่ละตัว

อ่านเอกสาร CSS Grid Layout

อ่านจนถึงระยะ 500px ก่อนท้ายหน้าและอยู่ในหน้านี้อย่างน้อย 5 นาที เพื่อรับ EXP วันละครั้ง

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

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