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 ได้เลย

ลองทำ 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-vitejsnpm installnpm run dev2. ทำการติดตั้ง 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
/** @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 คือส่วนไหน

สร้างไฟล์ TabExample.jsx ขึ้นมา ใน folder components ซึ่งตัวอย่างโค๊ด ก็เป็นตัวอย่างเดียวกับเว็บ headless ui ครับ
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 leading-5 font-medium text-blue-700', 'ring-opacity-60 focus:online-hidden ring-white ring-offset-2 ring-offset-blue-400 focus:ring-2', selected ? 'bg-white shadow' : 'text-blue-100 hover:bg-white/[0.12] hover:text-zinc-50' ) } > {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-opacity-60 focus:online-hidden ring-white ring-offset-2 ring-offset-blue-400 focus:ring-2' )} > <ul> {posts.map((post) => ( <li key={post.id} className="relative rounded-md p-3 hover:bg-zinc-100"> <h3 className="text-left text-sm leading-5 font-medium text-black"> {post.title} </h3>
<ul className="mt-1 flex space-x-1 text-xs leading-4 font-normal text-zinc-500"> <li>{post.date}</li> <li>·</li> <li>{post.commentCount} comments</li> <li>·</li> <li>{post.shareCount} shares</li> </ul>
<a href="#" className={classNames( 'absolute inset-0 rounded-md', 'focus:online-hidden ring-blue-400 focus:z-10 focus:ring-2' )} /> </li> ))} </ul> </Tab.Panel> ))} </Tab.Panels> </Tab.Group> </div> )}แก้ไขไฟล์ App.jsx นิดหน่อย (ลบ state counter ออก) และทำการ import TabExample มาใช้
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="text-lg font-bold text-purple-400">Headlessui Tab Example</p>
<TabExample /> </div> )}
export default Appทดลองรันโปรแกรม
npm run devและเปิดหน้าเว็บขึ้นมาดูผลลัพธ์ http://localhost:5173
References
- Authors
-
Chai Phonbopit
เป็น Web Dev ในบริษัทแห่งหนึ่ง ทำงานมา 10 ปีกว่าๆ ด้วยภาษาและเทคโนโลยี เช่น JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจในเรื่องของ Blockchain และ Crypto กำลังหัดเรียนภาษา Rust