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

HTML & CSS EP15: CSS Variables และ Design System — สร้าง UI ที่ปรับแต่งง่ายและดูแลรักษาได้

HTML & CSS

บทเรียน HTML และ CSS ตอนที่ 15 เรียนรู้ CSS Custom Properties หรือ CSS Variables การกำหนด Design Tokens การสร้างระบบสีและระยะห่าง การทำธีมสว่างและมืด การออกแบบ Component ที่นำกลับมาใช้ซ้ำได้ และการจัดโครงสร้าง CSS ให้ดูแลรักษาง่าย พร้อมตัวอย่าง Full-page HTML ที่ทดลองได้ทันที

HTML & CSS EP15: CSS Variables และ Design System — สร้าง UI ที่ปรับแต่งง่ายและดูแลรักษาได้

จากบทเรียนก่อนหน้า เราได้เรียนรู้การสร้างเอฟเฟกต์ด้วย CSS Transitions และ Animations แล้ว เมื่อเว็บไซต์เริ่มมีหลายหน้า มีปุ่มหลายรูปแบบ การ์ดจำนวนมาก และสีที่ต้องใช้ร่วมกัน ปัญหาที่มักเกิดขึ้นคือการต้องแก้ CSS ซ้ำหลายจุด เช่น เปลี่ยนสีหลักของแบรนด์แล้วต้องค้นหาค่าเดิมทั่วทั้งไฟล์ หรือปรับระยะห่างให้สม่ำเสมอแต่แต่ละส่วนใช้ตัวเลขไม่เหมือนกัน บทนี้จะพาไปรู้จัก **CSS Variables หรือ CSS Custom Properties** และแนวคิด **Design System** เพื่อให้เรากำหนดค่ากลางเพียงครั้งเดียว แล้วนำไปใช้ซ้ำในหลายองค์ประกอบได้อย่างเป็นระบบ
🎯 เป้าหมายของบทเรียนเมื่อเรียนจบ คุณจะสามารถประกาศและเรียกใช้ CSS Variables ด้วย `--variable-name` และ `var()` สร้าง Design Tokens สำหรับสี ฟอนต์ ระยะห่าง และเงา ออกแบบปุ่มและการ์ดที่ใช้รูปแบบร่วมกัน สร้างธีมสว่างและมืดด้วยการเปลี่ยนค่าตัวแปร และจัดโครงสร้าง CSS ให้เหมาะกับโปรเจกต์ที่ต้องพัฒนาต่อในระยะยาว

1. CSS Variables คืออะไร

CSS Custom Properties เป็นคุณสมบัติที่ช่วยเก็บค่าซึ่งสามารถนำไปใช้ซ้ำใน CSS ได้ ชื่อของตัวแปรต้องขึ้นต้นด้วยเครื่องหมายขีดสองตัว เช่น `--color-primary` และเรียกใช้ผ่านฟังก์ชัน `var()` โดยทั่วไปเรามักประกาศตัวแปรที่ใช้ร่วมกันใน `:root` เพื่อให้เข้าถึงได้จากองค์ประกอบต่าง ๆ ภายในเอกสาร
:root {
  --color-primary: #2563eb;
  --color-text: #0f172a;
  --color-surface: #ffffff;
  --space-md: 16px;
  --radius-md: 12px;
}

.button {
  padding: var(--space-md);
  color: white;
  background: var(--color-primary);
  border-radius: var(--radius-md);
}

.card {
  padding: var(--space-md);
  color: var(--color-text);
  background: var(--color-surface);
  border-radius: var(--radius-md);
}
ตัวอย่างนี้กำหนดสีหลัก สีข้อความ สีพื้นหลัง ระยะห่าง และความโค้งของมุมไว้ที่เดียว ปุ่มและการ์ดจึงสามารถใช้ค่าชุดเดียวกันได้ หากต้องการเปลี่ยนสีหลักของเว็บไซต์ ให้แก้ `--color-primary` เพียงตำแหน่งเดียว องค์ประกอบที่อ้างอิงตัวแปรนี้ก็จะได้รับค่าใหม่โดยอัตโนมัติ

1.1 ทำไมต้องใช้ชื่อที่สื่อความหมาย

ชื่ออย่าง `--color-primary`, `--space-lg` และ `--radius-card` ช่วยให้ผู้พัฒนาเข้าใจวัตถุประสงค์ของค่าได้ง่ายกว่าชื่ออย่าง `--blue1`, `--x2` หรือ `--value3` การตั้งชื่อที่ดีมีความสำคัญเป็นพิเศษเมื่อหลายคนทำงานร่วมกัน หรือเมื่อกลับมาแก้ไขโครงการเดิมหลังจากผ่านไปหลายเดือน
ชื่อที่ควรใช้ความหมายตัวอย่างการนำไปใช้
`--color-primary`สีหลักของแบรนด์หรือการกระทำสำคัญปุ่มหลัก ลิงก์ที่เน้น
`--color-text-muted`สีข้อความรองคำอธิบายหรือข้อความช่วยเหลือ
`--space-lg`ระยะห่างขนาดใหญ่ช่องว่างระหว่างส่วนเนื้อหา
`--radius-card`ความโค้งของการ์ดกรอบการ์ดและกล่องข้อมูล
`--shadow-card`เงามาตรฐานของการ์ดแยกการ์ดออกจากพื้นหลัง

2. เข้าใจขอบเขตและการสืบทอดของตัวแปร

CSS Variables มีขอบเขตตามกฎ Cascade และการสืบทอดของ CSS ตัวแปรที่ประกาศใน `:root` มักใช้ได้ทั่วเอกสาร แต่ถ้าประกาศในองค์ประกอบเฉพาะ ตัวแปรนั้นจะมีผลกับองค์ประกอบและลูกหลานที่อยู่ภายในขอบเขตดังกล่าวเท่านั้น คุณสมบัตินี้ทำให้เราสามารถเปลี่ยนธีมหรือกำหนดค่าที่แตกต่างกันให้กับส่วนใดส่วนหนึ่งของหน้าได้
:root {
  --panel-background: #ffffff;
  --panel-text: #0f172a;
}

.panel {
  color: var(--panel-text);
  background: var(--panel-background);
}

.panel--highlight {
  --panel-background: #eff6ff;
  --panel-text: #1e3a8a;
}

.panel--highlight .panel-title {
  color: var(--panel-text);
}
เมื่อ `.panel--highlight` กำหนดค่าตัวแปรใหม่ องค์ประกอบภายในส่วนนี้จะรับค่าที่ประกาศในขอบเขตใกล้กว่า โดยไม่จำเป็นต้องสร้างชื่อคลาสใหม่สำหรับทุกสี นี่เป็นพื้นฐานที่มีประโยชน์ในการสร้างธีม การ์ดที่มีหลายรูปแบบ และส่วนติดต่อผู้ใช้ที่ปรับแต่งได้

3. ใช้ var() พร้อมค่าเริ่มต้น

ฟังก์ชัน `var()` สามารถรับค่าทดแทนได้ หากตัวแปรที่อ้างอิงไม่มีค่าที่ใช้งานได้ในบริบทนั้น รูปแบบคือ `var(--variable-name, fallback-value)` วิธีนี้ช่วยให้สไตล์มีความยืดหยุ่นมากขึ้น โดยเฉพาะเมื่อสร้าง Component ที่นำไปใช้ในหลายหน้า
.notice {
  color: var(--notice-text, #1e3a8a);
  background: var(--notice-background, #eff6ff);
  border: 1px solid var(--notice-border, #bfdbfe);
  padding: var(--notice-padding, 16px);
  border-radius: var(--notice-radius, 12px);
}

.notice--warning {
  --notice-text: #92400e;
  --notice-background: #fffbeb;
  --notice-border: #fcd34d;
}
ข้อควรระวังเกี่ยวกับ Fallbackค่า Fallback ใน `var()` ไม่ใช่การตรวจสอบชนิดข้อมูลแบบสมบูรณ์ หากตัวแปรมีอยู่แต่มีค่าที่ไม่สามารถใช้กับ Property นั้นได้ เบราว์เซอร์อาจทำให้ Declaration ดังกล่าวไม่ถูกต้องได้ ควรกำหนดค่าตัวแปรให้เหมาะกับ Property ที่นำไปใช้ และตรวจสอบผลในเบราว์เซอร์จริง

4. Design Tokens คืออะไร

Design Tokens คือชุดค่ากลางที่แทนการตัดสินใจด้านการออกแบบ เช่น สีหลัก สีพื้นหลัง ระยะห่าง ความโค้งของมุม ขนาดตัวอักษร และเงา เมื่อนำ Tokens มาเก็บเป็น CSS Variables จะช่วยให้การออกแบบมีความสม่ำเสมอและแก้ไขง่ายขึ้น แทนที่จะเลือกค่าสีใหม่ทุกครั้งที่สร้างปุ่ม เรากำหนดสีที่ต้องการไว้ในระบบก่อน แล้วให้ปุ่ม ลิงก์ ป้ายสถานะ และองค์ประกอบอื่นอ้างอิงค่าเหล่านั้น
กลุ่ม Tokenตัวอย่างหน้าที่
Color`--color-primary`ควบคุมสีที่ใช้เน้น
Typography`--font-body`กำหนดแบบอักษรหลัก
Spacing`--space-1` ถึง `--space-6`ควบคุมระยะห่างให้เป็นระบบ
Radius`--radius-sm`, `--radius-lg`กำหนดความโค้งของขอบ
Shadow`--shadow-card`สร้างระดับความลึกของพื้นผิว
Motion`--motion-fast`, `--motion-normal`กำหนดเวลาการเปลี่ยนสถานะ
:root {
  /* Colors */
  --color-primary: #2563eb;
  --color-primary-hover: #1d4ed8;
  --color-text: #0f172a;
  --color-text-muted: #475569;
  --color-page: #f1f5f9;
  --color-surface: #ffffff;
  --color-border: #dbe4f0;

  /* Typography */
  --font-body: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.25rem;

  /* Spacing */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  /* Shape and motion */
  --radius-sm: 6px;
  --radius-md: 12px;
  --radius-lg: 18px;
  --motion-fast: 160ms;
  --motion-normal: 240ms;
}
ตัวอย่างนี้จัดกลุ่ม Tokens ตามหน้าที่อย่างชัดเจน คุณสามารถเพิ่มค่าตามความต้องการของโครงการได้ แต่ควรหลีกเลี่ยงการสร้างตัวแปรจำนวนมากโดยไม่มีมาตรฐาน ตั้งชื่อให้สม่ำเสมอ และกำหนดแนวทางว่าควรใช้ Token ใดในสถานการณ์ใด

5. สร้าง Component ที่นำกลับมาใช้ซ้ำได้

Component ในบริบทของ HTML และ CSS หมายถึงส่วนติดต่อผู้ใช้ที่มีหน้าที่และรูปแบบชัดเจน เช่น ปุ่ม การ์ดสินค้า ป้ายสถานะ และกล่องแจ้งเตือน เราสามารถใช้คลาสพื้นฐานร่วมกัน แล้วเพิ่ม Modifier Class เพื่อเปลี่ยนลักษณะเฉพาะโดยไม่ต้องเขียน CSS ซ้ำทั้งชุด
<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Reusable Components</title>
  <style>
    :root {
      --primary: #2563eb;
      --danger: #dc2626;
      --surface: #ffffff;
      --text: #0f172a;
      --border: #dbe4f0;
      --radius: 12px;
      --space: 16px;
    }

    body {
      margin: 0;
      padding: 28px;
      font-family: system-ui, sans-serif;
      color: var(--text);
      background: #f1f5f9;
    }

    .component-stack {
      display: grid;
      gap: var(--space);
      max-width: 420px;
    }

    .button {
      display: inline-flex;
      justify-content: center;
      align-items: center;
      min-height: 44px;
      padding: 10px 16px;
      border: 0;
      border-radius: var(--radius);
      background: var(--primary);
      color: #fff;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
      transition: filter 180ms ease, transform 180ms ease;
    }

    .button:hover {
      filter: brightness(0.92);
      transform: translateY(-2px);
    }

    .button--danger {
      background: var(--danger);
    }

    .card {
      padding: 20px;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      background: var(--surface);
    }

    .card h2 {
      margin-top: 0;
    }

    .muted {
      color: #475569;
    }
  </style>
</head>
<body>
  <main class="component-stack">
    <h1>ตัวอย่าง Component</h1>
    <article class="card">
      <h2>การ์ดมาตรฐาน</h2>
      <p class="muted">ใช้สี ระยะห่าง และความโค้งจากตัวแปรกลาง</p>
      <button class="button" type="button">ดำเนินการ</button>
    </article>
    <article class="card">
      <h2>การ์ดสำหรับการดำเนินการสำคัญ</h2>
      <p class="muted">เปลี่ยนเฉพาะ Modifier Class ของปุ่ม</p>
      <button class="button button--danger" type="button">ลบรายการตัวอย่าง</button>
    </article>
  </main>
</body>
</html>
คลาส `.button` กำหนดรูปแบบพื้นฐาน ส่วน `.button--danger` เปลี่ยนสีเพื่อสื่อถึงการกระทำที่มีความเสี่ยง ตัวอย่างนี้แสดงแนวคิดการแยกโครงสร้างออกจากรูปแบบเฉพาะ อย่างไรก็ตาม ในระบบจริงควรใช้สีและข้อความเตือนให้เหมาะสม และไม่ใช้สีเพียงอย่างเดียวในการสื่อความหมาย

5.1 แนวทางตั้งชื่อ Component

เลือกแนวทางตั้งชื่อที่ทีมเข้าใจตรงกัน เช่น `.card`, `.card__title`, `.card__description` สำหรับองค์ประกอบภายใน และ `.card--featured` สำหรับรูปแบบพิเศษ แนวทางนี้คล้ายหลักการของ BEM ซึ่งช่วยแยก Block, Element และ Modifier อย่างเป็นระบบ ไม่จำเป็นต้องใช้ BEM ทุกโครงการ แต่ควรหลีกเลี่ยงชื่อคลาสที่กว้างหรือกำกวมเกินไป
.card {
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

.card__title {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-lg);
}

.card__description {
  margin: 0;
  color: var(--color-text-muted);
}

.card--featured {
  border: 2px solid var(--color-primary);
}

6. สร้าง Light Theme และ Dark Theme

เมื่อใช้ CSS Variables เราสามารถสร้างหลายธีมได้โดยไม่ต้องเขียนสไตล์ทุกองค์ประกอบซ้ำ เช่น กำหนดค่าธีมเริ่มต้นใน `:root` แล้วเขียนค่าที่เปลี่ยนไปภายใต้ `[data-theme="dark"]` จากนั้นให้สีข้อความ พื้นหลัง ขอบ และเงาของ Component อ้างอิงตัวแปรชุดเดียวกัน
:root {
  color-scheme: light;
  --page-bg: #f1f5f9;
  --surface-bg: #ffffff;
  --text-color: #0f172a;
  --muted-color: #475569;
  --border-color: #dbe4f0;
}

[data-theme="dark"] {
  color-scheme: dark;
  --page-bg: #0f172a;
  --surface-bg: #1e293b;
  --text-color: #f8fafc;
  --muted-color: #cbd5e1;
  --border-color: #334155;
}

body {
  color: var(--text-color);
  background: var(--page-bg);
}

.card {
  color: var(--text-color);
  background: var(--surface-bg);
  border: 1px solid var(--border-color);
}

.muted {
  color: var(--muted-color);
}
การเปลี่ยนค่าใน `[data-theme="dark"]` ทำให้ Component ที่อ้างอิง Tokens เปลี่ยนสีไปพร้อมกัน การประกาศ `color-scheme` ยังช่วยให้ส่วนควบคุมมาตรฐานของเบราว์เซอร์บางประเภทเลือกโทนที่สอดคล้องกับธีมได้ แต่ควรตรวจสอบทุกส่วนของหน้าและตรวจสอบความคมชัดของข้อความด้วย

6.1 สลับธีมด้วย JavaScript

CSS ทำหน้าที่กำหนดรูปลักษณ์ของแต่ละธีม ส่วนการสลับค่า `data-theme` สามารถทำได้ด้วย JavaScript ตัวอย่างต่อไปนี้เปลี่ยนธีมบนองค์ประกอบ `document.documentElement` โดยตรง หากจะบันทึกค่าที่ผู้ใช้เลือกไว้ในโปรเจกต์จริง ควรเพิ่มการจัดการข้อผิดพลาดและพิจารณานโยบายความเป็นส่วนตัวก่อนเก็บข้อมูลในพื้นที่จัดเก็บของเบราว์เซอร์
const themeButton = document.querySelector("#theme-toggle");
const root = document.documentElement;

function setTheme(theme) {
  root.dataset.theme = theme;
  themeButton.setAttribute(
    "aria-pressed",
    String(theme === "dark")
  );
  themeButton.textContent = theme === "dark"
    ? "เปลี่ยนเป็นธีมสว่าง"
    : "เปลี่ยนเป็นธีมมืด";
}

themeButton.addEventListener("click", () => {
  const isDark = root.dataset.theme === "dark";
  setTheme(isDark ? "light" : "dark");
});

setTheme("light");
หลักคิดสำคัญแยกความรับผิดชอบให้ชัดเจน: CSS Variables เก็บค่าที่ใช้ร่วมกัน CSS Components ใช้ค่าดังกล่าวเพื่อกำหนดรูปลักษณ์ และ JavaScript เปลี่ยนสถานะธีมเมื่อผู้ใช้สั่ง การแบ่งหน้าที่เช่นนี้ช่วยลดความซ้ำซ้อนและทำให้ทดสอบแต่ละส่วนได้ง่าย

7. จัดระเบียบไฟล์ CSS สำหรับโปรเจกต์จริง

เมื่อโปรเจกต์เติบโต การเขียน CSS ทั้งหมดไว้ในไฟล์เดียวอาจทำให้ค้นหากฎได้ยาก เราสามารถแยกไฟล์ตามความรับผิดชอบ เช่น `tokens.css` สำหรับค่ากลาง `base.css` สำหรับสไตล์พื้นฐาน `components.css` สำหรับ Component และ `utilities.css` สำหรับคลาสช่วยจัดรูปแบบ ทั้งนี้ควรเลือกโครงสร้างให้เหมาะกับขนาดและเครื่องมือของโปรเจกต์
<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CSS Project Structure</title>
  <link rel="stylesheet" href="css/tokens.css">
  <link rel="stylesheet" href="css/base.css">
  <link rel="stylesheet" href="css/components.css">
  <link rel="stylesheet" href="css/utilities.css">
</head>
<body>
  <main class="container">
    <h1 class="page-title">โครงสร้างโปรเจกต์</h1>
    <article class="card">
      <h2 class="card__title">แยกไฟล์ตามหน้าที่</h2>
      <p class="card__description">ช่วยให้ค้นหา แก้ไข และทดสอบสไตล์ได้ง่ายขึ้น</p>
    </article>
  </main>
</body>
</html>
ตัวอย่างนี้แสดงวิธีเชื่อมไฟล์ CSS หลายไฟล์ แต่จะทำงานได้เมื่อสร้างไฟล์ตามตำแหน่งที่ระบุจริง หากใช้เครื่องมือ Build หรือ Framework ควรปฏิบัติตามโครงสร้างและกระบวนการนำเข้าไฟล์ของเครื่องมือนั้น ไม่จำเป็นต้องแยกไฟล์ทุกประเภทในโครงการขนาดเล็ก

8. Design System ไม่ใช่แค่ชุดสี

Design System ที่ดีประกอบด้วยมากกว่าสีและตัวแปร โดยอาจรวมถึงหลักการออกแบบ Typography, Spacing, Layout, Component, สถานะของปุ่ม แนวทาง Accessibility และเอกสารการใช้งาน เป้าหมายคือทำให้หน้าตาและพฤติกรรมของระบบสอดคล้องกัน โดยทีมสามารถสร้างหน้าหรือฟังก์ชันใหม่ได้โดยไม่ต้องตัดสินใจเรื่องพื้นฐานซ้ำทุกครั้ง
ส่วนประกอบคำถามที่ต้องตอบตัวอย่าง
Design Tokensค่าพื้นฐานที่ใช้ร่วมกันคืออะไรสีหลัก ระยะห่าง ความโค้ง
Componentsองค์ประกอบมาตรฐานหน้าตาอย่างไรปุ่ม การ์ด ฟอร์ม ป้ายสถานะ
Statesเมื่อผู้ใช้โต้ตอบแล้วเกิดอะไรขึ้นHover, Focus, Disabled, Error
Accessibilityผู้ใช้ที่มีข้อจำกัดต่างกันใช้งานได้หรือไม่คีย์บอร์ด Focus และความคมชัด
Documentationควรใช้ Component เมื่อใดตัวอย่างการใช้งานและข้อควรระวัง

9. Workshop: สร้างหน้า Dashboard ที่เปลี่ยนธีมได้

ใน Workshop นี้ เราจะรวม CSS Variables, Design Tokens, Grid Layout, Reusable Components, Transition และการสลับธีมไว้ในหน้า Dashboard ตัวอย่างเดียว โค้ดทั้งหมดเป็น HTML แบบเต็มไฟล์ สามารถบันทึกเป็น `index.html` แล้วเปิดด้วยเบราว์เซอร์ได้โดยไม่ต้องติดตั้งไลบรารีภายนอก
<!DOCTYPE html>
<html lang="th" data-theme="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="ตัวอย่าง Dashboard ที่ใช้ CSS Variables และ Design Tokens">
  <title>Design System Dashboard</title>
  <style>
    :root {
      color-scheme: light;
      --color-primary: #2563eb;
      --color-primary-hover: #1d4ed8;
      --color-page: #f1f5f9;
      --color-surface: #ffffff;
      --color-text: #0f172a;
      --color-muted: #475569;
      --color-border: #dbe4f0;
      --color-success: #15803d;
      --color-success-bg: #dcfce7;
      --font-body: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      --space-1: 4px;
      --space-2: 8px;
      --space-3: 12px;
      --space-4: 16px;
      --space-5: 24px;
      --space-6: 32px;
      --radius-sm: 8px;
      --radius-md: 14px;
      --radius-lg: 20px;
      --shadow-card: 0 8px 24px rgb(15 23 42 / 6%);
      --motion-fast: 160ms;
      --motion-normal: 240ms;
    }

    [data-theme="dark"] {
      color-scheme: dark;
      --color-primary: #60a5fa;
      --color-primary-hover: #93c5fd;
      --color-page: #0b1220;
      --color-surface: #172033;
      --color-text: #f8fafc;
      --color-muted: #cbd5e1;
      --color-border: #334155;
      --color-success: #86efac;
      --color-success-bg: #153a2a;
      --shadow-card: 0 8px 24px rgb(0 0 0 / 20%);
    }

    * {
      box-sizing: border-box;
    }

    body {
      min-height: 100vh;
      margin: 0;
      font-family: var(--font-body);
      color: var(--color-text);
      background: var(--color-page);
      transition: color var(--motion-normal) ease, background-color var(--motion-normal) ease;
    }

    button {
      font: inherit;
    }

    .container {
      width: min(1080px, calc(100% - 32px));
      margin-inline: auto;
    }

    .topbar {
      border-bottom: 1px solid var(--color-border);
      background: var(--color-surface);
      transition: background-color var(--motion-normal) ease, border-color var(--motion-normal) ease;
    }

    .topbar-inner {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--space-4);
      padding-block: var(--space-4);
    }

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

    .brand span {
      color: var(--color-primary);
    }

    .button {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: var(--space-2);
      min-height: 44px;
      padding: 10px 14px;
      border: 1px solid transparent;
      border-radius: var(--radius-sm);
      background: var(--color-primary);
      color: #ffffff;
      font-weight: 750;
      cursor: pointer;
      transition: transform var(--motion-fast) ease, background-color var(--motion-fast) ease, border-color var(--motion-fast) ease;
    }

    [data-theme="dark"] .button {
      color: #0b1220;
    }

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

    .button:focus-visible {
      outline: 3px solid var(--color-primary);
      outline-offset: 3px;
    }

    main {
      padding-block: var(--space-6);
    }

    .page-heading {
      display: flex;
      align-items: flex-end;
      justify-content: space-between;
      gap: var(--space-4);
      margin-bottom: var(--space-5);
    }

    .page-heading h1 {
      margin: 0 0 var(--space-2);
      font-size: clamp(1.8rem, 4vw, 2.6rem);
      line-height: 1.15;
      letter-spacing: -0.04em;
    }

    .page-heading p {
      margin: 0;
      color: var(--color-muted);
    }

    .status {
      display: inline-flex;
      align-items: center;
      gap: var(--space-2);
      padding: 6px 10px;
      border-radius: 999px;
      background: var(--color-success-bg);
      color: var(--color-success);
      font-size: 0.85rem;
      font-weight: 750;
      white-space: nowrap;
    }

    .status-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: currentColor;
    }

    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
      gap: var(--space-4);
    }

    .stat-card {
      padding: var(--space-5);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-md);
      background: var(--color-surface);
      box-shadow: var(--shadow-card);
      transition: transform var(--motion-fast) ease, background-color var(--motion-normal) ease, border-color var(--motion-normal) ease;
    }

    .stat-card:hover {
      transform: translateY(-3px);
    }

    .stat-label {
      margin: 0 0 var(--space-2);
      color: var(--color-muted);
      font-size: 0.92rem;
    }

    .stat-value {
      margin: 0;
      font-size: clamp(1.7rem, 4vw, 2.2rem);
      font-weight: 850;
      letter-spacing: -0.04em;
    }

    .stat-note {
      margin: var(--space-2) 0 0;
      color: var(--color-muted);
      font-size: 0.86rem;
    }

    .content-grid {
      display: grid;
      grid-template-columns: minmax(0, 1.4fr) minmax(260px, 0.8fr);
      gap: var(--space-4);
      margin-top: var(--space-4);
    }

    .panel {
      padding: var(--space-5);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-md);
      background: var(--color-surface);
      box-shadow: var(--shadow-card);
      transition: background-color var(--motion-normal) ease, border-color var(--motion-normal) ease;
    }

    .panel h2 {
      margin: 0 0 var(--space-2);
      font-size: 1.15rem;
    }

    .panel-description {
      margin: 0 0 var(--space-4);
      color: var(--color-muted);
      font-size: 0.92rem;
    }

    .activity-list {
      display: grid;
      gap: var(--space-3);
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .activity-item {
      display: flex;
      align-items: flex-start;
      gap: var(--space-3);
      padding-bottom: var(--space-3);
      border-bottom: 1px solid var(--color-border);
    }

    .activity-item:last-child {
      padding-bottom: 0;
      border-bottom: 0;
    }

    .activity-icon {
      display: grid;
      width: 38px;
      height: 38px;
      flex: 0 0 auto;
      place-items: center;
      border-radius: var(--radius-sm);
      background: var(--color-page);
    }

    .activity-item strong {
      display: block;
      margin-bottom: 2px;
    }

    .activity-item p {
      margin: 0;
      color: var(--color-muted);
      font-size: 0.88rem;
    }

    .token-list {
      display: grid;
      gap: var(--space-3);
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .token-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--space-3);
      font-size: 0.9rem;
    }

    .token-value {
      color: var(--color-muted);
      font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
      font-size: 0.8rem;
    }

    .swatch {
      display: inline-block;
      width: 18px;
      height: 18px;
      margin-right: 7px;
      vertical-align: middle;
      border-radius: 5px;
      background: var(--color-primary);
    }

    .footer {
      padding-block: var(--space-5);
      color: var(--color-muted);
      font-size: 0.88rem;
    }

    @media (max-width: 720px) {
      .content-grid {
        grid-template-columns: 1fr;
      }

      .page-heading {
        align-items: flex-start;
        flex-direction: column;
      }

      .topbar-inner {
        flex-wrap: wrap;
      }
    }

    @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;
      }

      .button:hover,
      .stat-card:hover {
        transform: none;
      }
    }
  </style>
</head>
<body>
  <header class="topbar">
    <div class="container topbar-inner">
      <p class="brand">Level<span>UI</span> Studio</p>
      <button class="button" id="theme-toggle" type="button" aria-pressed="false">
        เปลี่ยนเป็นธีมมืด
      </button>
    </div>
  </header>

  <main class="container">
    <section class="page-heading" aria-labelledby="dashboard-title">
      <div>
        <h1 id="dashboard-title">ภาพรวมระบบ</h1>
        <p>ตัวอย่าง Dashboard ที่ใช้ Design Tokens ร่วมกันทั้งหน้า</p>
      </div>
      <span class="status"><span class="status-dot" aria-hidden="true"></span>ระบบตัวอย่างพร้อมใช้งาน</span>
    </section>

    <section class="stats-grid" aria-label="สถิติตัวอย่าง">
      <article class="stat-card">
        <p class="stat-label">ผู้เข้าชมตัวอย่าง</p>
        <p class="stat-value">1,248</p>
        <p class="stat-note">ข้อมูลสมมติสำหรับการเรียนรู้</p>
      </article>
      <article class="stat-card">
        <p class="stat-label">คำสั่งซื้อ</p>
        <p class="stat-value">86</p>
        <p class="stat-note">ข้อมูลสมมติสำหรับการเรียนรู้</p>
      </article>
      <article class="stat-card">
        <p class="stat-label">งานที่เสร็จแล้ว</p>
        <p class="stat-value">92%</p>
        <p class="stat-note">ข้อมูลสมมติสำหรับการเรียนรู้</p>
      </article>
    </section>

    <section class="content-grid" aria-label="รายละเอียดระบบ">
      <article class="panel">
        <h2>กิจกรรมล่าสุด</h2>
        <p class="panel-description">ตัวอย่างรายการที่จัดรูปแบบด้วย Component มาตรฐาน</p>
        <ul class="activity-list">
          <li class="activity-item">
            <span class="activity-icon" aria-hidden="true">✓</span>
            <div>
              <strong>สร้างชุด Design Tokens</strong>
              <p>กำหนดสีหลัก สีพื้นหลัง และระยะห่างสำหรับใช้ร่วมกัน</p>
            </div>
          </li>
          <li class="activity-item">
            <span class="activity-icon" aria-hidden="true">◈</span>
            <div>
              <strong>ปรับปรุง Component การ์ด</strong>
              <p>ใช้ตัวแปรเดียวกันสำหรับความโค้งและเงาของการ์ด</p>
            </div>
          </li>
          <li class="activity-item">
            <span class="activity-icon" aria-hidden="true">☼</span>
            <div>
              <strong>เพิ่มธีมสว่างและมืด</strong>
              <p>เปลี่ยนค่า Custom Properties ผ่าน data-theme</p>
            </div>
          </li>
        </ul>
      </article>

      <aside class="panel">
        <h2>Design Tokens</h2>
        <p class="panel-description">ค่ากลางที่ Component ใช้งานร่วมกัน</p>
        <ul class="token-list">
          <li class="token-item">
            <span><span class="swatch" aria-hidden="true"></span>สีหลัก</span>
            <code class="token-value">--color-primary</code>
          </li>
          <li class="token-item">
            <span>พื้นผิว</span>
            <code class="token-value">--color-surface</code>
          </li>
          <li class="token-item">
            <span>ระยะห่าง</span>
            <code class="token-value">--space-4</code>
          </li>
          <li class="token-item">
            <span>ความโค้ง</span>
            <code class="token-value">--radius-md</code>
          </li>
          <li class="token-item">
            <span>Transition</span>
            <code class="token-value">--motion-normal</code>
          </li>
        </ul>
      </aside>
    </section>

    <footer class="footer">
      <p>ตัวอย่างเพื่อการเรียนรู้เท่านั้น สถิติทั้งหมดเป็นข้อมูลจำลอง ไม่มีการเชื่อมต่อฐานข้อมูลจริง</p>
    </footer>
  </main>

  <script>
    const themeButton = document.querySelector("#theme-toggle");
    const root = document.documentElement;

    function setTheme(theme) {
      root.dataset.theme = theme;
      const isDark = theme === "dark";
      themeButton.setAttribute("aria-pressed", String(isDark));
      themeButton.textContent = isDark
        ? "เปลี่ยนเป็นธีมสว่าง"
        : "เปลี่ยนเป็นธีมมืด";
    }

    themeButton.addEventListener("click", () => {
      const currentTheme = root.dataset.theme || "light";
      setTheme(currentTheme === "dark" ? "light" : "dark");
    });
  </script>
</body>
</html>

10. วิธีตรวจสอบและแก้ปัญหา

ตัวแปร CSS ไม่เปลี่ยนตามธีม
ตรวจสอบว่าตัวแปรถูกประกาศในขอบเขตที่ถูกต้องหรือไม่ ตรวจสอบค่า `data-theme` บนองค์ประกอบที่เกี่ยวข้อง และดูว่า CSS Rule ใดมีลำดับความสำคัญเหนือกว่า หาก Component ใช้ค่าสีแบบเขียนตรงไว้แทน `var()` การเปลี่ยน Tokens จะไม่ส่งผลต่อค่านั้น
เรียกใช้ var() แล้ว Property ไม่ทำงาน
ตรวจสอบการสะกดชื่อ Custom Property เครื่องหมายขีดสองตัว และชนิดค่าที่ Property ต้องการ หากตัวแปรไม่มีอยู่ ให้เพิ่มค่า Fallback เช่น `var(--color-primary, #2563eb)` แต่หากตัวแปรมีค่าไม่ถูกต้องสำหรับ Property นั้น ควรแก้ค่าต้นทาง
สีข้อความใน Dark Theme อ่านยาก
ตรวจสอบทั้งสีข้อความหลัก สีข้อความรอง สีลิงก์ ขอบ และสถานะปุ่ม อย่าเปลี่ยนเฉพาะพื้นหลังโดยไม่ปรับสีองค์ประกอบอื่น ทดสอบความคมชัดของข้อความและตรวจสอบสถานะ Hover, Focus, Disabled และ Error ด้วย
CSS ซ้ำซ้อนเมื่อโปรเจกต์ใหญ่ขึ้น
เริ่มจากรวบรวมค่าที่ใช้ซ้ำเป็น Tokens จากนั้นแยก Component ตามหน้าที่และกำหนดมาตรฐานการตั้งชื่อ อย่าเปลี่ยน CSS ทุกส่วนพร้อมกันโดยไม่ทดสอบ ควรแก้ทีละกลุ่มและตรวจสอบหน้าเว็บที่ใช้ Component นั้นทั้งหมด

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

ฝึกปรับปรุงหน้า Dashboard ใน Workshop เพื่อให้เข้าใจว่าการเปลี่ยนค่ากลางส่งผลต่อองค์ประกอบหลายส่วนอย่างไร จากนั้นลองนำแนวคิดไปใช้กับหน้าสินค้า หน้าโปรไฟล์ หรือหน้าแสดงผลข้อมูลของโครงการที่กำลังพัฒนา
แบบฝึกหัดที่ 1: เปลี่ยนสีแบรนด์
เปลี่ยน `--color-primary` เป็นสีใหม่ แล้วตรวจสอบปุ่ม ลิงก์ และองค์ประกอบที่ใช้ตัวแปรนี้ หากบางส่วนไม่เปลี่ยน ให้ค้นหาค่าที่เขียนตรงไว้ใน CSS และปรับให้ใช้ Token อย่างเหมาะสม
แบบฝึกหัดที่ 2: สร้าง Spacing Scale
กำหนดตัวแปรระยะห่างอย่างน้อยห้าระดับ แล้วนำไปใช้กับ Padding ของการ์ด ช่องว่างระหว่างหัวข้อ และระยะห่างในรายการ พยายามใช้ชุดค่าที่สอดคล้องกันแทนการกำหนดตัวเลขใหม่ทุกตำแหน่ง
แบบฝึกหัดที่ 3: สร้าง Component ป้ายสถานะ
สร้างคลาส `.badge` สำหรับรูปแบบพื้นฐาน แล้วเพิ่ม Modifier เช่น `.badge--success` และ `.badge--warning` โดยใช้ CSS Variables สำหรับสีข้อความและพื้นหลัง อย่าพึ่งพาสีเพียงอย่างเดียวในการสื่อความหมาย ควรมีข้อความกำกับด้วย
แบบฝึกหัดที่ 4: ปรับ Dark Theme
เปลี่ยนสีพื้นหลัง สีข้อความ และสีขอบของธีมมืด จากนั้นทดสอบทุกการ์ดและทุกสถานะของปุ่ม ตรวจสอบว่าข้อความยังอ่านได้และองค์ประกอบที่โฟกัสด้วยคีย์บอร์ดยังเห็นชัดเจน
แบบฝึกหัดที่ 5: แยก CSS เป็นไฟล์
แยกตัวแปรไปไว้ใน `tokens.css` และ Component ไปไว้ใน `components.css` เชื่อมไฟล์เข้ากับ `index.html` แล้วตรวจสอบว่าไม่มีไฟล์หายหรือเส้นทางผิด หากใช้ระบบ Build ให้ทำตามรูปแบบนำเข้าไฟล์ของเครื่องมือนั้น

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

CSS Variables ช่วยเก็บค่ากลางที่นำไปใช้ซ้ำได้ ส่วน Design Tokens ทำให้การตัดสินใจด้านการออกแบบมีมาตรฐานมากขึ้น เมื่อนำไปใช้กับ Component ที่มีหน้าที่ชัดเจน เราจะสามารถเปลี่ยนสี ระยะห่าง ความโค้ง และเวลา Transition ได้จากจุดเดียว การแยกธีมผ่าน Custom Properties ยังช่วยให้การดูแล Light Theme และ Dark Theme เป็นระบบยิ่งขึ้น สิ่งสำคัญคือการตั้งชื่อให้เข้าใจง่าย เลือกค่าที่เหมาะกับโครงการ ทดสอบทุกสถานะ และรักษาความสามารถในการเข้าถึงของผู้ใช้ ไม่ใช่เพียงทำให้หน้าเว็บมีสีสันสวยงามเท่านั้น
Checklist ก่อนนำ Design System ไปใช้จริงกำหนด Tokens ที่จำเป็นให้ชัดเจน ตั้งชื่อสม่ำเสมอ ใช้ `var()` แทนค่าซ้ำเมื่อเหมาะสม แยก Component พื้นฐานออกจากรูปแบบพิเศษ ตรวจสอบ Light Theme และ Dark Theme ทดสอบสถานะ Hover, Focus, Disabled และ Error และตรวจสอบความคมชัดของข้อความก่อนเผยแพร่
ระบบออกแบบที่ดีไม่ได้ทำให้ทุกหน้าเหมือนกันทั้งหมด แต่ช่วยให้ทุกหน้าพูดภาษาเดียวกัน และยังเปิดโอกาสให้แต่ละส่วนมีเอกลักษณ์ตามหน้าที่ของตัวเอง

อ่านเอกสาร CSS Custom Properties จาก MDN

เรียนรู้ CSS Cascade และการสืบทอด


บทถัดไป EP16: จะต่อยอดการออกแบบ Component และ Design Tokens ไปสู่การสร้างฟอร์มที่ใช้งานง่าย การจัดรูปแบบ Input, Label, Validation State และข้อความแจ้งข้อผิดพลาดด้วย HTML และ CSS
อ่านจนถึงระยะ 500px ก่อนท้ายหน้าและอยู่ในหน้านี้อย่างน้อย 5 นาที เพื่อรับ EXP วันละครั้ง

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

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