สร้าง Gruvbox Theme สำหรับ Qwik ด้วย Tailwind CSS v4: คู่มือฉบับสมบูรณ์

1. บทนำ

ในโลกของการพัฒนาเว็บแอปพลิเคชัน การเลือก Color Scheme ที่เหมาะสมไม่เพียงแต่ช่วยให้แอปพลิเคชันดูสวยงาม แต่ยังส่งผลต่อประสบการณ์ผู้ใช้และความสบายตาในการใช้งานเป็นเวลานาน Gruvbox เป็นหนึ่งใน Color Scheme ที่ได้รับความนิยมอย่างมากในหมู่นักพัฒนา ด้วยโทนสีที่อบอุ่น สบายตา และรองรับทั้ง Dark Mode และ Light Mode

บทความนี้จะพาคุณไปสร้าง Gruvbox Theme ที่พร้อมใช้งานสำหรับ Qwik Framework ร่วมกับ Tailwind CSS v4 ซึ่งมีการเปลี่ยนแปลงวิธีการ Configuration ใหม่ทั้งหมด


2. Gruvbox คืออะไร?

Gruvbox เป็น Color Scheme ที่ถูกออกแบบโดย Pavel Pertsev สำหรับ Vim Editor แต่ด้วยความสวยงามและความสบายตา ทำให้ถูกนำไปใช้กับ Terminal, IDE และเว็บแอปพลิเคชันมากมาย

2.1 จุดเด่นของ Gruvbox


3. ทำไมต้อง Tailwind CSS v4?

Tailwind CSS v4 มาพร้อมกับการเปลี่ยนแปลงครั้งใหญ่ที่น่าตื่นเต้น:

Tailwind v3 Tailwind v4
ใช้ tailwind.config.js ใช้ @theme directive ใน CSS
@tailwind base/components/utilities @import "tailwindcss"
JavaScript-based Config CSS-based Config

การเปลี่ยนมาใช้ CSS-based Configuration ทำให้:

  1. ลดความซับซ้อน - ไม่ต้องมีไฟล์ config แยก
  2. IDE Support ดีขึ้น - CSS Variables ทำงานได้ดีกับ IDE
  3. Runtime Theme Switching - เปลี่ยน Theme ได้ง่ายขึ้นผ่าน CSS Variables

4. การติดตั้ง Qwik Project

4.1 ความต้องการเบื้องต้น (Prerequisites)

ก่อนเริ่มต้น ตรวจสอบให้แน่ใจว่าคุณมีสิ่งเหล่านี้ติดตั้งอยู่ในเครื่อง:

ตรวจสอบเวอร์ชัน Node.js:

node --version
# ควรแสดง v18.x.x หรือสูงกว่า

4.2 สร้าง Qwik Project ใหม่

เปิด Terminal และรันคำสั่งต่อไปนี้เพื่อสร้างโปรเจค Qwik ใหม่:

# ใช้ npm
npm create qwik@latest

# หรือใช้ pnpm
pnpm create qwik@latest

# หรือใช้ yarn
yarn create qwik

# หรือใช้ bun
bun create qwik@latest

4.3 ขั้นตอนการตั้งค่าโปรเจค

เมื่อรันคำสั่งข้างต้น ระบบจะถามคำถามหลายข้อ:

┌  Let's create a  Qwik App  ✨
│
◇  Where would you like to create your new project?
│  ./my-qwik-app
│
◇  Select a starter
│  ● Basic App (Qwik City)
│    ○ Empty App
│    ○ Library
│
◇  Would you like to install npm dependencies?
│  Yes
│
◇  Initialize a new git repository?
│  Yes
│
└  Successfully created my-qwik-app! 🎉

คำแนะนำในการเลือก:

  1. Project location - ตั้งชื่อโฟลเดอร์โปรเจค เช่น my-qwik-app
  2. Starter template - เลือก Basic App (Qwik City) สำหรับโปรเจคทั่วไป
  3. Install dependencies - เลือก Yes เพื่อติดตั้ง dependencies อัตโนมัติ
  4. Git repository - เลือก Yes เพื่อ initialize git

4.4 เข้าสู่โฟลเดอร์โปรเจค

cd my-qwik-app

4.5 รัน Development Server

# ใช้ npm
npm run dev

# หรือใช้ pnpm
pnpm dev

# หรือใช้ yarn
yarn dev

# หรือใช้ bun
bun dev

เปิด Browser และไปที่ http://localhost:5173 จะเห็นหน้า Welcome ของ Qwik

4.6 โครงสร้างโปรเจค Qwik

my-qwik-app/
├── public/                 # Static assets
├── src/
│   ├── components/         # Reusable components
│   ├── routes/             # File-based routing
│   │   ├── index.tsx       # หน้าแรก (/)
│   │   └── layout.tsx      # Layout หลัก
│   ├── entry.ssr.tsx       # Server-side entry
│   └── root.tsx            # Root component
├── package.json
├── tsconfig.json
└── vite.config.ts

5. การติดตั้ง Tailwind CSS ใน Qwik

Qwik มี Integration สำหรับ Tailwind CSS ที่ติดตั้งได้ง่ายด้วยคำสั่ง qwik add

5.1 ติดตั้ง Tailwind ด้วย Qwik CLI

รันคำสั่งต่อไปนี้ในโฟลเดอร์โปรเจค:

# ใช้ npm
npm run qwik add tailwind

# หรือใช้ pnpm
pnpm qwik add tailwind

# หรือใช้ yarn
yarn qwik add tailwind

# หรือใช้ bun
bun run qwik add tailwind

5.2 ระบบจะติดตั้งและตั้งค่าให้อัตโนมัติ

เมื่อรันคำสั่ง ระบบจะแสดงข้อความดังนี้:

🦋  Add Integration

◇  What integration would you like to add?
│  Tailwind CSS
│
◇  Ready to add tailwind to your app?
│  Yes
│
◇  Installing tailwindcss...
│
◇  Updating files...
│  - vite.config.ts
│  - src/global.css (created)
│  - postcss.config.js (created)
│
└  ✅ Added tailwind!

5.3 ไฟล์ที่ถูกสร้าง/แก้ไข

หลังจากติดตั้ง Tailwind สำเร็จ จะมีไฟล์ที่ถูกสร้างและแก้ไขดังนี้:

ไฟล์ที่สร้างใหม่:

  1. src/global.css - ไฟล์ CSS หลักที่ import Tailwind
@import "tailwindcss";
  1. postcss.config.js - PostCSS configuration
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

ไฟล์ที่แก้ไข:

  1. vite.config.ts - เพิ่ม Tailwind plugin

  2. package.json - เพิ่ม dependencies

{
  "devDependencies": {
    "tailwindcss": "^4.x.x",
    "autoprefixer": "^10.x.x"
  }
}

5.4 ตรวจสอบว่า global.css ถูก import

ตรวจสอบไฟล์ src/root.tsx ว่ามีการ import global.css:

// src/root.tsx
import './global.css';

export default component$(() => {
  return (
    <QwikCityProvider>
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link rel="manifest" href="/manifest.json" />
      </head>
      <body lang="en">
        <RouterOutlet />
        <ServiceWorkerRegister />
      </body>
    </QwikCityProvider>
  );
});

5.5 ทดสอบว่า Tailwind ทำงาน

แก้ไขไฟล์ src/routes/index.tsx เพื่อทดสอบ:

import { component$ } from '@builder.io/qwik';

export default component$(() => {
  return (
    <div class="min-h-screen bg-gray-900 text-white flex items-center justify-center">
      <div class="text-center">
        <h1 class="text-4xl font-bold text-blue-400 mb-4">
          Tailwind CSS ทำงานแล้ว! 🎉
        </h1>
        <p class="text-gray-400">
          พร้อมสำหรับ Gruvbox Theme
        </p>
      </div>
    </div>
  );
});

รัน npm run dev และดูผลลัพธ์ที่ http://localhost:5173


6. เริ่มต้นสร้าง Gruvbox Theme

6.1 โครงสร้างไฟล์

src/
├── global.css              # Theme หลัก + Components
├── hooks/
│   └── useTheme.ts         # Hook สำหรับ Toggle Theme
└── components/
    └── gruvbox-showcase.tsx # ตัวอย่าง Component

6.2 สร้าง Theme ด้วย @theme Directive

แก้ไขไฟล์ src/global.css โดยลบเนื้อหาเดิมและใส่โค้ดต่อไปนี้:

@import "tailwindcss";

@theme {
  /* Gruvbox Dark Background */
  --color-gb-bg: #282828;
  --color-gb-bg-h: #1d2021;
  --color-gb-bg-s: #32302f;
  --color-gb-bg-0: #282828;
  --color-gb-bg-1: #3c3836;
  --color-gb-bg-2: #504945;
  --color-gb-bg-3: #665c54;
  --color-gb-bg-4: #7c6f64;

  /* Gruvbox Light Background */
  --color-gb-bg-light: #fbf1c7;
  --color-gb-bg-light-h: #f9f5d7;
  --color-gb-bg-light-s: #f2e5bc;
  --color-gb-bg-light-0: #fbf1c7;
  --color-gb-bg-light-1: #ebdbb2;
  --color-gb-bg-light-2: #d5c4a1;
  --color-gb-bg-light-3: #bdae93;
  --color-gb-bg-light-4: #a89984;

  /* Gruvbox Dark Foreground */
  --color-gb-fg: #ebdbb2;
  --color-gb-fg-0: #fbf1c7;
  --color-gb-fg-1: #ebdbb2;
  --color-gb-fg-2: #d5c4a1;
  --color-gb-fg-3: #bdae93;
  --color-gb-fg-4: #a89984;

  /* Gruvbox Light Foreground */
  --color-gb-fg-light: #3c3836;
  --color-gb-fg-light-0: #282828;
  --color-gb-fg-light-1: #3c3836;
  --color-gb-fg-light-2: #504945;
  --color-gb-fg-light-3: #665c54;
  --color-gb-fg-light-4: #7c6f64;

  /* Red */
  --color-gb-red: #cc241d;
  --color-gb-red-bright: #fb4934;
  --color-gb-red-dim: #9d0006;

  /* Green */
  --color-gb-green: #98971a;
  --color-gb-green-bright: #b8bb26;
  --color-gb-green-dim: #79740e;

  /* Yellow */
  --color-gb-yellow: #d79921;
  --color-gb-yellow-bright: #fabd2f;
  --color-gb-yellow-dim: #b57614;

  /* Blue */
  --color-gb-blue: #458588;
  --color-gb-blue-bright: #83a598;
  --color-gb-blue-dim: #076678;

  /* Purple */
  --color-gb-purple: #b16286;
  --color-gb-purple-bright: #d3869b;
  --color-gb-purple-dim: #8f3f71;

  /* Aqua */
  --color-gb-aqua: #689d6a;
  --color-gb-aqua-bright: #8ec07c;
  --color-gb-aqua-dim: #427b58;

  /* Orange */
  --color-gb-orange: #d65d0e;
  --color-gb-orange-bright: #fe8019;
  --color-gb-orange-dim: #af3a03;

  /* Gray */
  --color-gb-gray: #928374;
  --color-gb-gray-bright: #a89984;
  --color-gb-gray-dim: #7c6f64;

  /* Fonts */
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
  --font-sans: 'Inter', system-ui, sans-serif;

  /* Box Shadows */
  --shadow-gb: 0 4px 6px -1px rgba(40, 40, 40, 0.3);
  --shadow-gb-lg: 0 10px 15px -3px rgba(40, 40, 40, 0.4);
}

เมื่อกำหนดสีใน @theme แล้ว Tailwind จะสร้าง Utility Classes ให้อัตโนมัติ เช่น bg-gb-bg, text-gb-red-bright, border-gb-blue เป็นต้น


7. Color Palette Reference

7.1 Background Colors (Dark Mode)

Class Hex Code การใช้งาน
bg-gb-bg-h #1d2021 Hard Background (มืดที่สุด)
bg-gb-bg #282828 Default Background
bg-gb-bg-s #32302f Soft Background
bg-gb-bg-1 #3c3836 Surface Level 1 (Cards)
bg-gb-bg-2 #504945 Surface Level 2 (Elevated)
bg-gb-bg-3 #665c54 Surface Level 3
bg-gb-bg-4 #7c6f64 Surface Level 4

7.2 Accent Colors

สี Normal Bright Dim การใช้งานแนะนำ
Red #cc241d #fb4934 #9d0006 Errors, Warnings
Green #98971a #b8bb26 #79740e Success, Strings
Yellow #d79921 #fabd2f #b57614 Warnings, Highlights
Blue #458588 #83a598 #076678 Links, Primary Actions
Purple #b16286 #d3869b #8f3f71 Keywords, Special
Aqua #689d6a #8ec07c #427b58 Types, Secondary
Orange #d65d0e #fe8019 #af3a03 Constants, Headings

8. สร้าง Component Classes

8.1 Base Styles

เพิ่มโค้ดต่อไปนี้ใน global.css ต่อจาก @theme:

/* ============================================
   CSS Variables for Runtime Theme Switching
   ============================================ */
:root {
  --gb-bg: #282828;
  --gb-bg-h: #1d2021;
  --gb-bg-s: #32302f;
  --gb-bg-1: #3c3836;
  --gb-bg-2: #504945;
  --gb-bg-3: #665c54;
  --gb-bg-4: #7c6f64;
  
  --gb-fg: #ebdbb2;
  --gb-fg-0: #fbf1c7;
  --gb-fg-1: #ebdbb2;
  --gb-fg-2: #d5c4a1;
  --gb-fg-3: #bdae93;
  --gb-fg-4: #a89984;
  
  --gb-red: #fb4934;
  --gb-green: #b8bb26;
  --gb-yellow: #fabd2f;
  --gb-blue: #83a598;
  --gb-purple: #d3869b;
  --gb-aqua: #8ec07c;
  --gb-orange: #fe8019;
  --gb-gray: #928374;
}

.light {
  --gb-bg: #fbf1c7;
  --gb-bg-h: #f9f5d7;
  --gb-bg-s: #f2e5bc;
  --gb-bg-1: #ebdbb2;
  --gb-bg-2: #d5c4a1;
  --gb-bg-3: #bdae93;
  --gb-bg-4: #a89984;
  
  --gb-fg: #3c3836;
  --gb-fg-0: #282828;
  --gb-fg-1: #3c3836;
  --gb-fg-2: #504945;
  --gb-fg-3: #665c54;
  --gb-fg-4: #7c6f64;
  
  --gb-red: #cc241d;
  --gb-green: #98971a;
  --gb-yellow: #d79921;
  --gb-blue: #458588;
  --gb-purple: #b16286;
  --gb-aqua: #689d6a;
  --gb-orange: #d65d0e;
  --gb-gray: #928374;
}

/* ============================================
   Base Styles
   ============================================ */
@layer base {
  html {
    scroll-behavior: smooth;
  }
  
  body {
    background-color: var(--gb-bg);
    color: var(--gb-fg);
    font-family: var(--font-sans);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
  
  /* Headings */
  h1 {
    font-size: 2.25rem;
    line-height: 2.5rem;
    font-weight: 700;
    color: var(--color-gb-orange-bright);
  }
  
  h2 {
    font-size: 1.875rem;
    line-height: 2.25rem;
    font-weight: 700;
    color: var(--color-gb-yellow-bright);
  }
  
  h3 {
    font-size: 1.5rem;
    line-height: 2rem;
    font-weight: 700;
    color: var(--color-gb-aqua-bright);
  }
  
  h4 {
    font-size: 1.25rem;
    line-height: 1.75rem;
    font-weight: 700;
    color: var(--color-gb-blue-bright);
  }
  
  /* Links */
  a {
    color: var(--color-gb-blue-bright);
    transition: color 0.2s;
  }
  
  a:hover {
    color: var(--color-gb-aqua-bright);
  }
  
  /* Code */
  code {
    font-family: var(--font-mono);
    color: var(--color-gb-orange-bright);
    background-color: var(--color-gb-bg-1);
    padding: 0.125rem 0.375rem;
    border-radius: 0.25rem;
  }
  
  pre {
    font-family: var(--font-mono);
    background-color: var(--color-gb-bg-h);
    padding: 1rem;
    border-radius: 0.5rem;
    overflow-x: auto;
  }
  
  pre code {
    background-color: transparent;
    padding: 0;
  }
  
  /* Selection */
  ::selection {
    background-color: var(--color-gb-blue);
    color: var(--color-gb-bg);
  }
  
  /* Scrollbar */
  ::-webkit-scrollbar {
    width: 0.5rem;
    height: 0.5rem;
  }
  
  ::-webkit-scrollbar-track {
    background-color: var(--color-gb-bg-1);
  }
  
  ::-webkit-scrollbar-thumb {
    background-color: var(--color-gb-bg-3);
    border-radius: 9999px;
  }
  
  ::-webkit-scrollbar-thumb:hover {
    background-color: var(--color-gb-bg-4);
  }
}

8.2 Buttons

@layer components {
  /* Buttons */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.5rem 1rem;
    border-radius: 0.5rem;
    font-weight: 500;
    transition: all 0.2s;
    cursor: pointer;
    border: none;
  }
  
  .btn:focus {
    outline: none;
    box-shadow: 0 0 0 2px var(--color-gb-bg), 0 0 0 4px var(--color-gb-blue);
  }
  
  .btn-primary {
    background-color: var(--color-gb-blue);
    color: var(--color-gb-bg);
  }
  
  .btn-primary:hover {
    background-color: var(--color-gb-blue-bright);
  }
  
  .btn-secondary {
    background-color: var(--color-gb-bg-2);
    color: var(--color-gb-fg);
  }
  
  .btn-secondary:hover {
    background-color: var(--color-gb-bg-3);
  }
  
  .btn-success {
    background-color: var(--color-gb-green);
    color: var(--color-gb-bg);
  }
  
  .btn-success:hover {
    background-color: var(--color-gb-green-bright);
  }
  
  .btn-danger {
    background-color: var(--color-gb-red);
    color: var(--color-gb-bg);
  }
  
  .btn-danger:hover {
    background-color: var(--color-gb-red-bright);
  }
  
  .btn-warning {
    background-color: var(--color-gb-yellow);
    color: var(--color-gb-bg);
  }
  
  .btn-warning:hover {
    background-color: var(--color-gb-yellow-bright);
  }
  
  .btn-ghost {
    background-color: transparent;
    color: var(--color-gb-fg);
  }
  
  .btn-ghost:hover {
    background-color: var(--color-gb-bg-1);
  }
}

8.3 Cards

@layer components {
  /* Cards */
  .card {
    background-color: var(--color-gb-bg-1);
    border-radius: 0.75rem;
    padding: 1.5rem;
    box-shadow: var(--shadow-gb);
  }
  
  .card-hover {
    background-color: var(--color-gb-bg-1);
    border-radius: 0.75rem;
    padding: 1.5rem;
    box-shadow: var(--shadow-gb);
    transition: background-color 0.2s;
    cursor: pointer;
  }
  
  .card-hover:hover {
    background-color: var(--color-gb-bg-2);
  }
  
  .card-bordered {
    background-color: var(--color-gb-bg-1);
    border-radius: 0.75rem;
    padding: 1.5rem;
    box-shadow: var(--shadow-gb);
    border: 1px solid var(--color-gb-bg-3);
  }
}

8.4 Form Inputs

@layer components {
  /* Inputs */
  .input {
    width: 100%;
    padding: 0.5rem 1rem;
    border-radius: 0.5rem;
    background-color: var(--color-gb-bg-h);
    border: 1px solid var(--color-gb-bg-3);
    color: var(--color-gb-fg);
    transition: all 0.2s;
  }
  
  .input::placeholder {
    color: var(--color-gb-fg-4);
  }
  
  .input:focus {
    outline: none;
    border-color: var(--color-gb-blue);
    box-shadow: 0 0 0 1px var(--color-gb-blue);
  }
  
  .input-error {
    width: 100%;
    padding: 0.5rem 1rem;
    border-radius: 0.5rem;
    background-color: var(--color-gb-bg-h);
    border: 1px solid var(--color-gb-red);
    color: var(--color-gb-fg);
    transition: all 0.2s;
  }
  
  .input-error:focus {
    outline: none;
    border-color: var(--color-gb-red);
    box-shadow: 0 0 0 1px var(--color-gb-red);
  }
  
  /* Labels */
  .label {
    display: block;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--color-gb-fg-3);
    margin-bottom: 0.25rem;
  }
}

8.5 Badges

@layer components {
  /* Badges */
  .badge {
    display: inline-flex;
    align-items: center;
    padding: 0.125rem 0.625rem;
    border-radius: 9999px;
    font-size: 0.75rem;
    font-weight: 500;
  }
  
  .badge-red {
    background-color: color-mix(in srgb, var(--color-gb-red) 20%, transparent);
    color: var(--color-gb-red-bright);
  }
  
  .badge-green {
    background-color: color-mix(in srgb, var(--color-gb-green) 20%, transparent);
    color: var(--color-gb-green-bright);
  }
  
  .badge-yellow {
    background-color: color-mix(in srgb, var(--color-gb-yellow) 20%, transparent);
    color: var(--color-gb-yellow-bright);
  }
  
  .badge-blue {
    background-color: color-mix(in srgb, var(--color-gb-blue) 20%, transparent);
    color: var(--color-gb-blue-bright);
  }
  
  .badge-purple {
    background-color: color-mix(in srgb, var(--color-gb-purple) 20%, transparent);
    color: var(--color-gb-purple-bright);
  }
  
  .badge-aqua {
    background-color: color-mix(in srgb, var(--color-gb-aqua) 20%, transparent);
    color: var(--color-gb-aqua-bright);
  }
  
  .badge-orange {
    background-color: color-mix(in srgb, var(--color-gb-orange) 20%, transparent);
    color: var(--color-gb-orange-bright);
  }
}

8.6 Alerts

@layer components {
  /* Alerts */
  .alert {
    padding: 1rem;
    border-radius: 0.5rem;
    border-left: 4px solid;
  }
  
  .alert-info {
    background-color: color-mix(in srgb, var(--color-gb-blue) 10%, transparent);
    border-color: var(--color-gb-blue);
    color: var(--color-gb-blue-bright);
  }
  
  .alert-success {
    background-color: color-mix(in srgb, var(--color-gb-green) 10%, transparent);
    border-color: var(--color-gb-green);
    color: var(--color-gb-green-bright);
  }
  
  .alert-warning {
    background-color: color-mix(in srgb, var(--color-gb-yellow) 10%, transparent);
    border-color: var(--color-gb-yellow);
    color: var(--color-gb-yellow-bright);
  }
  
  .alert-error {
    background-color: color-mix(in srgb, var(--color-gb-red) 10%, transparent);
    border-color: var(--color-gb-red);
    color: var(--color-gb-red-bright);
  }
}

9. Theme Switching (Dark/Light Mode)

9.1 useTheme Hook สำหรับ Qwik

สร้างไฟล์ src/hooks/useTheme.ts:

import { $, useOnDocument, useSignal, useVisibleTask$ } from '@builder.io/qwik';

type Theme = 'dark' | 'light' | 'system';

/**
 * useTheme - Hook สำหรับจัดการ Gruvbox Theme (Tailwind v4)
 */
export const useTheme = () => {
  const theme = useSignal<Theme>('dark');
  const resolvedTheme = useSignal<'dark' | 'light'>('dark');

  // eslint-disable-next-line qwik/no-use-visible-task
  useVisibleTask$(() => {
    const stored = localStorage.getItem('gruvbox-theme') as Theme | null;
    
    if (stored) {
      theme.value = stored;
      updateTheme(stored);
    } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
      theme.value = 'system';
      updateTheme('system');
    }
  });

  useOnDocument(
    'visibilitychange',
    $(() => {
      if (theme.value === 'system') {
        updateTheme('system');
      }
    })
  );

  const updateTheme = (newTheme: Theme) => {
    let resolved: 'dark' | 'light' = 'dark';

    if (newTheme === 'system') {
      resolved = window.matchMedia('(prefers-color-scheme: light)').matches
        ? 'light'
        : 'dark';
    } else {
      resolved = newTheme;
    }

    resolvedTheme.value = resolved;
    
    // Tailwind v4: ใช้ class 'light' สำหรับ light mode (default = dark)
    document.documentElement.classList.remove('light');
    if (resolved === 'light') {
      document.documentElement.classList.add('light');
    }
    
    // Update meta theme-color for mobile browsers
    const metaThemeColor = document.querySelector('meta[name="theme-color"]');
    if (metaThemeColor) {
      metaThemeColor.setAttribute(
        'content',
        resolved === 'dark' ? '#282828' : '#fbf1c7'
      );
    }
  };

  const setTheme = $((newTheme: Theme) => {
    theme.value = newTheme;
    localStorage.setItem('gruvbox-theme', newTheme);
    updateTheme(newTheme);
  });

  const toggleTheme = $(() => {
    const newTheme = resolvedTheme.value === 'dark' ? 'light' : 'dark';
    setTheme(newTheme);
  });

  const cycleTheme = $(() => {
    const themes: Theme[] = ['dark', 'light', 'system'];
    const currentIndex = themes.indexOf(theme.value);
    const nextIndex = (currentIndex + 1) % themes.length;
    setTheme(themes[nextIndex]);
  });

  return {
    theme,
    resolvedTheme,
    setTheme,
    toggleTheme,
    cycleTheme,
    isDark: resolvedTheme.value === 'dark',
    isLight: resolvedTheme.value === 'light',
    isSystem: theme.value === 'system',
  };
};

export default useTheme;

9.2 ตัวอย่างการใช้งาน Theme Toggle

import { component$ } from '@builder.io/qwik';
import { useTheme } from '../hooks/useTheme';

export const ThemeToggle = component$(() => {
  const { toggleTheme, resolvedTheme } = useTheme();

  return (
    <button onClick$={toggleTheme} class="btn btn-ghost">
      {resolvedTheme.value === 'dark' ? '🌙 Dark' : '☀️ Light'}
    </button>
  );
});

9.3 Manual Toggle (ไม่ใช้ Hook)

// เพิ่ม class "light" ที่ <html> สำหรับ light mode
document.documentElement.classList.add('light');

// ลบ class "light" สำหรับ dark mode (default)
document.documentElement.classList.remove('light');

10. ตัวอย่างการใช้งานจริง

10.1 Basic Page Layout

import { component$ } from '@builder.io/qwik';
import { useTheme } from '../hooks/useTheme';

export default component$(() => {
  const { toggleTheme, resolvedTheme } = useTheme();

  return (
    <div class="min-h-screen">
      {/* Header */}
      <header class="bg-gb-bg-1 p-4 flex justify-between items-center">
        <h1>My Gruvbox App</h1>
        <div class="flex items-center gap-4">
          <nav class="flex gap-4">
            <a href="/" class="nav-link">Home</a>
            <a href="/about" class="nav-link">About</a>
            <a href="/contact" class="nav-link">Contact</a>
          </nav>
          <button onClick$={toggleTheme} class="btn btn-ghost">
            {resolvedTheme.value === 'dark' ? '🌙' : '☀️'}
          </button>
        </div>
      </header>
      
      {/* Main Content */}
      <main class="container mx-auto p-8">
        <div class="grid md:grid-cols-2 gap-6">
          <div class="card">
            <h2 class="mb-4">Welcome!</h2>
            <p class="text-gb-fg-3 mb-4">
              สวัสดี! นี่คือ Gruvbox Theme สำหรับ Qwik + Tailwind v4
            </p>
            <div class="flex gap-2">
              <span class="badge badge-green">Active</span>
              <span class="badge badge-blue">New</span>
            </div>
          </div>
          
          <div class="card">
            <h3 class="text-gb-aqua-bright mb-4">Quick Actions</h3>
            <div class="flex flex-col gap-2">
              <button class="btn btn-primary">Get Started</button>
              <button class="btn btn-secondary">Learn More</button>
            </div>
          </div>
        </div>
        
        {/* Alerts Section */}
        <section class="mt-8 space-y-4">
          <div class="alert alert-info">
            <strong>Info:</strong> Welcome to Gruvbox Theme!
          </div>
          <div class="alert alert-success">
            <strong>Success:</strong> Theme loaded successfully.
          </div>
        </section>
      </main>
      
      {/* Footer */}
      <footer class="bg-gb-bg-1 p-4 text-center text-gb-fg-4 mt-auto">
        © 2024 My Gruvbox App
      </footer>
    </div>
  );
});

10.2 Form Example

import { component$, useSignal } from '@builder.io/qwik';

export const LoginForm = component$(() => {
  const email = useSignal('');
  const password = useSignal('');

  return (
    <div class="card max-w-md mx-auto">
      <h2 class="mb-6">Login</h2>
      
      <div class="space-y-4">
        <div>
          <label class="label">Email</label>
          <input 
            type="email" 
            class="input" 
            placeholder="you@example.com"
            value={email.value}
            onInput$={(e) => email.value = (e.target as HTMLInputElement).value}
          />
        </div>
        
        <div>
          <label class="label">Password</label>
          <input 
            type="password" 
            class="input" 
            placeholder="••••••••"
            value={password.value}
            onInput$={(e) => password.value = (e.target as HTMLInputElement).value}
          />
        </div>
        
        <button type="submit" class="btn btn-primary w-full">
          Sign In
        </button>
      </div>
      
      <p class="text-gb-fg-4 text-sm mt-4 text-center">
        Don't have an account? 
        <a href="/register" class="text-gb-blue-bright ml-1">Sign up</a>
      </p>
    </div>
  );
});

11. Utility Classes เพิ่มเติม

11.1 Gradients

@layer utilities {
  .gb-gradient-primary {
    background: linear-gradient(to right, var(--color-gb-blue), var(--color-gb-aqua));
  }
  
  .gb-gradient-warm {
    background: linear-gradient(to right, var(--color-gb-orange), var(--color-gb-yellow));
  }
  
  .gb-gradient-cool {
    background: linear-gradient(to right, var(--color-gb-purple), var(--color-gb-blue));
  }
}

11.2 Glass Effect

@layer utilities {
  .gb-glass {
    background-color: color-mix(in srgb, var(--color-gb-bg) 80%, transparent);
    backdrop-filter: blur(12px);
  }
}

11.3 Glow Effects

@layer utilities {
  .glow-blue {
    box-shadow: 0 0 20px color-mix(in srgb, var(--color-gb-blue-bright) 30%, transparent);
  }
  
  .glow-orange {
    box-shadow: 0 0 20px color-mix(in srgb, var(--color-gb-orange-bright) 30%, transparent);
  }
  
  .glow-aqua {
    box-shadow: 0 0 20px color-mix(in srgb, var(--color-gb-aqua-bright) 30%, transparent);
  }
}

12. สรุป

การสร้าง Gruvbox Theme สำหรับ Qwik + Tailwind CSS v4 นั้นต้องคำนึงถึงการเปลี่ยนแปลงใน Tailwind v4 ที่ใช้ CSS-based Configuration แทน JavaScript Config File เดิม

12.1 สิ่งที่ได้เรียนรู้

  1. การติดตั้ง Qwik - ใช้ npm create qwik@latest สร้างโปรเจคใหม่
  2. การติดตั้ง Tailwind - ใช้ npm run qwik add tailwind ติดตั้งได้ง่าย
  3. @theme Directive - วิธีกำหนดสีและ Design Tokens ใน Tailwind v4
  4. CSS Variables - ใช้สำหรับ Runtime Theme Switching
  5. Component Classes - สร้าง Reusable Components ด้วย @layer components
  6. useTheme Hook - จัดการ Dark/Light Mode ใน Qwik

12.2 ข้อดีของ Approach นี้


13. Resources


บทความนี้เขียนขึ้นเพื่อเป็นแนวทางในการสร้าง Custom Theme สำหรับ Modern Web Framework โดยใช้ Gruvbox Color Scheme ที่เป็นที่นิยมในหมู่นักพัฒนา