ตัวอย่างการทำ Tab ด้วย Headless UI ของดีจาก Tailwind Labs

Published on
React
headlessui-tabs-example
Discord

Headlessui เป็น UI Components จากทีม Tailwind Labs ปัจจุบัน รองรับ React.js และ Vue.js โดยปัจจุบันมี Component ให้เลือก เช่น Dropdown, Menu, Toggle, Switch, Listbox, Tab, Dialog เป็นต้นครับ

เนื่องจากมันเป็น Headless UI ก็จะไม่มี css หรือ default style มาให้ เราสามารถใช้ CSS อะไรก็ได้ (แต่ตัวอย่างนี้ผมใช้ Tailwind CSS เนื่องจากเป็นทีมเดียวกันกับ headdlessui ครับ)

วันนี้ก็เลยลองเล่นตัว Tabs ซักหน่อย ข้อดีนอกจากมี UI มาให้แล้ว คือมีเรื่องของ ARIA มาให้ด้วย ไม่ต้องกำหนด role และ attribute เพิ่มเอง ทำให้สามารถใช้ Keyboard short cut ได้เลย

Headless UI

ลองทำ headlessui Tabs

ทดลองสร้างโปรเจ็คขึ้นมาเลยดีกว่า โดยตัวโปรเจ็คจะใช้ Vite.js ขึ้นโปรเจ็คเป็น React.js ครับ ซึ่ง Library ต่างๆ ที่ใช้ในบทความประกอบไปด้วย

  • React v18.2.0
  • Headlessui v1.6.6
  • Tailwind v3.1
  • Vite v3.0.7

1. สร้างโปรเจ็คด้วย Vite

npm create vite@latest

จากนั้นตั้งชื่อโปรเจ็คและเลือก react และ react (ใครจะเลือก typesript ก็ได้นะครับ)

✔ Project name: … headlessui-tabs-vitejs
✔ Select a framework: › react
✔ Select a variant: › react

เมื่อ setup Vite เสร็จแล้ว ก็ทำการเปิดโฟลเดอร์ที่เราเพิ่ง init แล้ว install dependencies

cd headlessui-tabs-vitejs
npm install
npm run dev

2. ทำการติดตั้ง tailwindcss

เราจะใช้ tailwind เป็น css หลัก

npm install -D tailwindcss postcss autoprefixer

ต่อมา init tailwindcss จะได้ไฟล์ tailwind.config.js และ postcss.config.js

npx tailwindcss init -p

แก้ไขไฟล์ tailwind.css.js เพื่อให้มันรู้ว่าไฟล์ index.html และไฟล์ต่างๆของ react จะใช้ tailwind

tailwind.css.js{3,6}
/** @type {import('tailwindcss').Config} */ 
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

สุดท้าย เพิ่มตรงนี้ลงไป ที่ไฟล์ index.css

@tailwind base;
@tailwind components;
@tailwind utilities;

3. headlessui tabs

ติดตั้ง headlessui tabs

npm install @headlessui/react

ตัวโครงสร้างในการสร้าง Tab ประกอบไปด้วย Tab.Group, Tab.List, Tab, Tab.Panels, และ Tab.Panel โดย Component เริ่มต้นจะมีหน้าตาประมาณนี้

import { Tab } from '@headlessui/react'

function MyTabs() {
  return (
    <Tab.Group>
      <Tab.List>
        <Tab>Tab 1</Tab>
        <Tab>Tab 2</Tab>
        <Tab>Tab 3</Tab>
      </Tab.List>
      <Tab.Panels>
        <Tab.Panel>Content 1</Tab.Panel>
        <Tab.Panel>Content 2</Tab.Panel>
        <Tab.Panel>Content 3</Tab.Panel>
      </Tab.Panels>
    </Tab.Group>
  )
}

ตัวอย่างภาพ เพื่อให้เห็นภาพมากขึ้น ว่า Tab.List และ Tab.Panels คือส่วนไหน

Tab - Headlessui

สร้างไฟล์ TabExample.jsx ขึ้นมา ใน folder components ซึ่งตัวอย่างโค๊ด ก็เป็นตัวอย่างเดียวกับเว็บ headless ui ครับ

TabExample.jsx
import { useState } from "react";
import { Tab } from "@headlessui/react";

function classNames(...classes) {
  return classes.filter(Boolean).join(" ");
}

export default function Example() {
  let [categories] = useState({
    Recent: [
      {
        id: 1,
        title: "Does drinking coffee make you smarter?",
        date: "5h ago",
        commentCount: 5,
        shareCount: 2,
      },
      {
        id: 2,
        title: "So you've bought coffee... now what?",
        date: "2h ago",
        commentCount: 3,
        shareCount: 2,
      },
    ],
    Popular: [
      {
        id: 1,
        title: "Is tech making coffee better or worse?",
        date: "Jan 7",
        commentCount: 29,
        shareCount: 16,
      },
      {
        id: 2,
        title: "The most innovative things happening in coffee",
        date: "Mar 19",
        commentCount: 24,
        shareCount: 12,
      },
    ],
    Trending: [
      {
        id: 1,
        title: "Ask Me Anything: 10 answers to your questions about coffee",
        date: "2d ago",
        commentCount: 9,
        shareCount: 5,
      },
      {
        id: 2,
        title: "The worst advice we've ever heard about coffee",
        date: "4d ago",
        commentCount: 1,
        shareCount: 2,
      },
    ],
  });

  return (
    <div className="w-full max-w-md px-2 py-16 sm:px-0">
      <Tab.Group>
        <Tab.List className="flex space-x-1 rounded-xl bg-blue-900/20 p-1">
          {Object.keys(categories).map((category) => (
            <Tab
              key={category}
              className={({ selected }) =>
                classNames(
                  "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-blue-700",
                  "ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2",
                  selected
                    ? "bg-white shadow"
                    : "text-blue-100 hover:bg-white/[0.12] hover:text-white"
                )
              }
            >
              {category}
            </Tab>
          ))}
        </Tab.List>
        <Tab.Panels className="mt-2">
          {Object.values(categories).map((posts, idx) => (
            <Tab.Panel
              key={idx}
              className={classNames(
                "rounded-xl bg-white p-3",
                "ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2"
              )}
            >
              <ul>
                {posts.map((post) => (
                  <li
                    key={post.id}
                    className="relative rounded-md p-3 hover:bg-gray-100"
                  >
                    <h3 className="text-left text-sm font-medium leading-5 text-black">
                      {post.title}
                    </h3>

                    <ul className="mt-1 flex space-x-1 text-xs font-normal leading-4 text-gray-500">
                      <li>{post.date}</li>
                      <li>&middot;</li>
                      <li>{post.commentCount} comments</li>
                      <li>&middot;</li>
                      <li>{post.shareCount} shares</li>
                    </ul>

                    <a
                      href="#"
                      className={classNames(
                        "absolute inset-0 rounded-md",
                        "ring-blue-400 focus:z-10 focus:outline-none focus:ring-2"
                      )}
                    />
                  </li>
                ))}
              </ul>
            </Tab.Panel>
          ))}
        </Tab.Panels>
      </Tab.Group>
    </div>
  );
}

แก้ไขไฟล์ App.jsx นิดหน่อย (ลบ state counter ออก) และทำการ import TabExample มาใช้

App.jsx
import reactLogo from "./assets/react.svg";
import "./App.css";

import TabExample from "./components/TabExample";

function App() {
  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>

      <p className="font-bold text-lg text-purple-400">
        Headlessui Tab Example
      </p>

      <TabExample />
    </div>
  );
}

export default App;

ทดลองรันโปรแกรม

npm run dev

และเปิดหน้าเว็บขึ้นมาดูผลลัพธ์ http://localhost:5173

References

Buy Me A Coffee
Authors
Discord