การจัดสไตล์เมนูและเมนูดรอปดาวน์แบบ Headless
ใช้คลาสยูทิลิตีของ Tailwind กับคอมโพเนนต์ Menu ของ Headless UI เพื่อสร้างเมนูดรอปดาวน์ที่ดูเรียบร้อย พร้อมการนำทางด้วยแป้นพิมพ์และแอตทริบิวต์ ARIA
การจัดสไตล์เมนูและเมนูดรอปดาวน์แบบ Headless เป็นบทเรียน Tailwind CSS Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Tailwind CSS Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Tailwind CSS Academy มีบทเรียนทั้งหมด 4 บทเรียน
บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ
Anatomy of a Headless UI Menu
The Headless UI Menu is composed of four pieces: Menu (the root wrapper that provides context), Menu.Button (the clickable trigger), Menu.Items (the dropdown panel that shows when open), and one or more Menu.Item elements (individual selectable options). Each piece accepts className or a render function that receives state, giving you full Tailwind control at every level.
import { Menu } from '@headlessui/react';
// Structure overview
<Menu as='div' className='relative inline-block'>
{/* 1. Trigger button */}
<Menu.Button>Open</Menu.Button>
{/* 2. Dropdown panel (shown when open) */}
<Menu.Items>
{/* 3. Individual options */}
<Menu.Item>{({ active }) => <button>Option A</button>}</Menu.Item>
<Menu.Item>{({ active }) => <button>Option B</button>}</Menu.Item>
</Menu.Items>
</Menu>Styling the Menu Trigger Button
The Menu.Button trigger should communicate interactivity and show its open/closed state visually. Access the open render prop to change the button's appearance when the dropdown is visible. Common patterns include rotating a chevron icon, changing the background color to match the open dropdown, and adding a ring for focus visibility.
import { Menu } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/20/solid';
<Menu.Button className='inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500'>
{({ open }) => (
<>
Actions
<ChevronDownIcon
className={cn(
'h-4 w-4 text-gray-400 transition-transform duration-150',
open && 'rotate-180'
)}
/>
</>
)}
</Menu.Button>Positioning the Dropdown Panel
The Menu.Items panel should appear visually anchored below the trigger. Position the Menu root as relative and the panel as absolute. Use top-full and right-0 or left-0 to align the panel to the trigger. Set a z-index to ensure the dropdown appears above other page content, and add mt-1 for a small gap between trigger and panel.
<Menu as='div' className='relative'>
<Menu.Button>...</Menu.Button>
<Menu.Items
className='
absolute right-0 top-full mt-1 z-20
w-56 origin-top-right
rounded-xl bg-white shadow-lg
ring-1 ring-black/5
focus:outline-none
'
>
{/* Items */}
</Menu.Items>
</Menu>Styling Menu Items With Active State
Each Menu.Item receives an active render prop that is true when the item is focused or hovered. Use this to apply a highlighted background — typically a subtle blue or gray. Also use disabled to dim and prevent interaction on unavailable options. Wrap the content in a button or a element to make it keyboard focusable.
<Menu.Items className='absolute right-0 mt-1 w-56 rounded-xl bg-white shadow-lg ring-1 ring-black/5 p-1'>
<Menu.Item>
{({ active }) => (
<button
className={cn(
'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm',
active ? 'bg-blue-50 text-blue-700' : 'text-gray-700'
)}
>
<PencilIcon className='h-4 w-4' />
Edit
</button>
)}
</Menu.Item>
<Menu.Item disabled>
{({ active, disabled }) => (
<button
className={cn(
'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm',
disabled && 'opacity-40 cursor-not-allowed'
)}
>
<ShareIcon className='h-4 w-4' />
Share (unavailable)
</button>
)}
</Menu.Item>
</Menu.Items>Adding Dividers Between Groups
Group related menu items with visual dividers. Since Headless UI does not provide a divider component, simply insert a plain div with a border or a padding wrapper. Organize items into logical sections — primary actions, secondary actions, and a destructive action — separated by thin horizontal rules styled with Tailwind.
<Menu.Items className='absolute right-0 mt-1 w-56 rounded-xl bg-white shadow-lg ring-1 ring-black/5 p-1 focus:outline-none'>
{/* Primary actions */}
<div className='pb-1 border-b border-gray-100'>
<Menu.Item>{({ active }) => <button className={itemCls(active)}>View</button>}</Menu.Item>
<Menu.Item>{({ active }) => <button className={itemCls(active)}>Edit</button>}</Menu.Item>
</div>
{/* Secondary actions */}
<div className='py-1 border-b border-gray-100'>
<Menu.Item>{({ active }) => <button className={itemCls(active)}>Duplicate</button>}</Menu.Item>
</div>
{/* Destructive action */}
<div className='pt-1'>
<Menu.Item>
{({ active }) => (
<button className={cn('flex w-full px-3 py-2 text-sm rounded-lg', active ? 'bg-red-50 text-red-700' : 'text-red-600')}>
Delete
</button>
)}
</Menu.Item>
</div>
</Menu.Items>Dropdown With Icon Decorations
A polished dropdown includes icons alongside each label to help users quickly identify options. Import icons from a library like @heroicons/react (built by the same Tailwind Labs team) and place them as inline elements within menu item buttons. Use consistent icon sizing — h-4 w-4 for small menus — and softer colors that inherit the item's active state.
import {
PencilSquareIcon,
DocumentDuplicateIcon,
TrashIcon
} from '@heroicons/react/20/solid';
const menuItems = [
{ label: 'Edit', icon: PencilSquareIcon, action: handleEdit },
{ label: 'Duplicate', icon: DocumentDuplicateIcon, action: handleDuplicate }
];
menuItems.map(({ label, icon: Icon, action }) => (
<Menu.Item key={label}>
{({ active }) => (
<button
onClick={action}
className={cn(
'flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm',
active ? 'bg-blue-50 text-blue-700' : 'text-gray-700'
)}
>
<Icon className='h-4 w-4 flex-shrink-0' />
{label}
</button>
)}
</Menu.Item>
))Keyboard Navigation in Headless Menu
Headless UI's Menu implements full keyboard support automatically. Pressing Enter or Space on the button opens the menu. Arrow keys navigate between items. Home and End jump to the first and last items. Escape closes the menu and returns focus to the trigger. Tab closes the menu. These behaviors follow the ARIA menu button pattern and require no additional code from you.
/*
Keyboard behavior (automatic with Headless UI):
Trigger button:
- Enter / Space → opens menu, focuses first item
- ArrowDown → opens menu, focuses first item
- ArrowUp → opens menu, focuses last item
Inside menu:
- ArrowDown → next item
- ArrowUp → previous item
- Home → first item
- End → last item
- Enter / Space → activates focused item
- Escape → closes menu, returns focus to trigger
- Tab → closes menu
- Type a character → jump to first matching item
*/Context Menu Pattern
A context menu (right-click menu) uses the same Menu structure but positions the panel relative to the cursor rather than the trigger button. Capture the right-click event, store the cursor position, and render the Menu.Items using fixed positioning at those coordinates. Headless UI still handles focus management and keyboard navigation inside the positioned menu.
function ContextMenuWrapper({ children }) {
const [position, setPosition] = useState(null);
const handleContextMenu = (e) => {
e.preventDefault();
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onContextMenu={handleContextMenu}>
{children}
{position && (
<Menu>
<Menu.Items
static // keep open without button
style={{ top: position.y, left: position.x }}
className='fixed z-50 w-48 rounded-xl bg-white shadow-xl ring-1 ring-black/5 p-1'
>
{/* menu items */}
</Menu.Items>
</Menu>
)}
</div>
);
}Popover vs Menu: When to Use Each
Headless UI provides both Menu and Popover components. Use Menu for action lists where each option performs an action or navigates. Use Popover for richer content panels — like a filter form, notification feed, or settings panel — where the content is not a simple list of actions and may contain interactive elements beyond buttons.
// Menu: for action lists
<Menu>
<Menu.Button>Actions</Menu.Button>
<Menu.Items>
<Menu.Item>Edit</Menu.Item> {/* action */}
<Menu.Item>Delete</Menu.Item> {/* action */}
</Menu.Items>
</Menu>
// Popover: for rich content panels
import { Popover } from '@headlessui/react';
<Popover>
<Popover.Button>Filters</Popover.Button>
<Popover.Panel className='absolute z-10 mt-2 w-72 rounded-xl bg-white shadow-lg p-4'>
{/* Rich content: form inputs, sliders, checkboxes */}
<label>Price range</label>
<input type='range' />
<button>Apply filters</button>
</Popover.Panel>
</Popover>Mobile-Responsive Dropdown
On mobile, a traditional dropdown positioned absolutely can overflow the viewport edges. Apply responsive positioning to the Menu.Items panel: use right-0 on small screens and left-0 on larger ones, or switch to a bottom sheet pattern on mobile using Tailwind's sm: prefix. Test dropdowns at mobile viewport widths to ensure they remain fully visible and usable.
<Menu as='div' className='relative'>
<Menu.Button>...</Menu.Button>
<Menu.Items
className='
absolute z-20 mt-1 w-56
rounded-xl bg-white shadow-lg ring-1 ring-black/5 p-1
/* Mobile: right-aligned to avoid left-edge overflow */
right-0
/* Desktop: left-aligned for wider viewports */
sm:right-auto sm:left-0
'
>
{/* items */}
</Menu.Items>
</Menu>
/* Also consider: on very small screens,
render as a full-width bottom sheet instead */Completing the Full Dropdown Component
Putting it all together: a complete, polished dropdown menu combines all the pieces — positioned container, styled trigger with chevron, grouped items with icons, dividers, and a destructive option. This component is reusable, fully keyboard navigable, and styled entirely with Tailwind — with zero custom CSS written and all accessibility handled by Headless UI.
export function ActionMenu({ onEdit, onDelete }) {
return (
<Menu as='div' className='relative'>
<Menu.Button className='rounded-lg p-2 text-gray-500 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none'>
<EllipsisVerticalIcon className='h-5 w-5' />
</Menu.Button>
<Menu.Items className='absolute right-0 mt-1 w-48 rounded-xl bg-white shadow-lg ring-1 ring-black/5 p-1 focus:outline-none z-20'>
<Menu.Item>
{({ active }) => (
<button onClick={onEdit} className={cn('w-full flex items-center gap-2 px-3 py-2 text-sm rounded-lg', active ? 'bg-gray-100' : 'text-gray-700')}>
<PencilIcon className='h-4 w-4' />
Edit
</button>
)}
</Menu.Item>
<div className='my-1 border-t border-gray-100' />
<Menu.Item>
{({ active }) => (
<button onClick={onDelete} className={cn('w-full flex items-center gap-2 px-3 py-2 text-sm rounded-lg', active ? 'bg-red-50 text-red-700' : 'text-red-600')}>
<TrashIcon className='h-4 w-4' />
Delete
</button>
)}
</Menu.Item>
</Menu.Items>
</Menu>
);
}Quick Check
Test your understanding of Tailwind CSS Mastery concepts from this lesson.
Lesson Recap
In this lesson you learned: Menu.Button triggers the dropdown with the open render prop for visual feedback, Menu.Items is positioned absolutely with Tailwind, and Menu.Item active drives the highlight style for the focused option. Next up we build fully accessible dialogs using Headless UI's Dialog component.
คำถามที่พบบ่อย
บทเรียน “การจัดสไตล์เมนูและเมนูดรอปดาวน์แบบ Headless” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การจัดสไตล์เมนูและเมนูดรอปดาวน์แบบ Headless” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Tailwind CSS Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Tailwind CSS Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การจัดสไตล์เมนูและเมนูดรอปดาวน์แบบ Headless”
ใช้คลาสยูทิลิตีของ Tailwind กับคอมโพเนนต์ Menu ของ Headless UI เพื่อสร้างเมนูดรอปดาวน์ที่ดูเรียบร้อย พร้อมการนำทางด้วยแป้นพิมพ์และแอตทริบิวต์ ARIA คุณปฏิบัติ Tailwind CSS Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Tailwind CSS Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Tailwind CSS Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “การจัดสไตล์เมนูและเมนูดรอปดาวน์แบบ Headless” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Tailwind CSS Academy นี้ได้ไหม
ได้ บทเรียน Tailwind CSS Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- บทนำสู่ Headless UI
- การจัดสไตล์เมนูและเมนูดรอปดาวน์แบบ Headless
- การสร้างไดอะล็อกที่เข้าถึงได้
- การเปลี่ยนผ่านด้วย Headless UI