headlessui

Headless UI - Accessible Component Primitives

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "headlessui" with this command: npx skills add bobmatnyc/claude-mpm-skills/bobmatnyc-claude-mpm-skills-headlessui

Headless UI - Accessible Component Primitives

Overview

Headless UI provides completely unstyled, fully accessible UI components designed to integrate beautifully with Tailwind CSS. Built by the Tailwind Labs team, it offers production-ready accessibility without imposing design decisions.

Key Features:

  • Fully unstyled - bring your own styles

  • Complete keyboard navigation

  • Screen reader tested

  • Focus management

  • ARIA attributes handled automatically

  • TypeScript support

  • React 18 and Vue 3 compatible

  • SSR compatible

  • Render props for maximum flexibility

Installation:

React

npm install @headlessui/react

Vue

npm install @headlessui/vue

Component Catalog

Menu (Dropdown)

Accessible dropdown menus with keyboard navigation and ARIA support.

import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react' import { ChevronDownIcon } from '@heroicons/react/20/solid'

function DropdownMenu() { return ( <Menu> <MenuButton className="inline-flex items-center gap-2 rounded-md bg-gray-800 py-1.5 px-3 text-sm/6 font-semibold text-white shadow-inner shadow-white/10 focus:outline-none data-[hover]:bg-gray-700 data-[open]:bg-gray-700 data-[focus]:outline-1 data-[focus]:outline-white"> Options <ChevronDownIcon className="size-4 fill-white/60" /> </MenuButton>

  &#x3C;MenuItems
    transition
    anchor="bottom end"
    className="w-52 origin-top-right rounded-xl border border-white/5 bg-white/5 p-1 text-sm/6 text-white transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] focus:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
  >
    &#x3C;MenuItem>
      &#x3C;button className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 data-[focus]:bg-white/10">
        Edit
      &#x3C;/button>
    &#x3C;/MenuItem>
    &#x3C;MenuItem>
      &#x3C;button className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 data-[focus]:bg-white/10">
        Duplicate
      &#x3C;/button>
    &#x3C;/MenuItem>
    &#x3C;div className="my-1 h-px bg-white/5" />
    &#x3C;MenuItem>
      &#x3C;button className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 data-[focus]:bg-white/10">
        Delete
      &#x3C;/button>
    &#x3C;/MenuItem>
  &#x3C;/MenuItems>
&#x3C;/Menu>

) }

Menu Features:

  • Arrow key navigation

  • Type-ahead search

  • Automatic focus management

  • Escape to close

  • Click outside to close

  • Portal rendering for positioning

  • Anchor positioning API

Listbox (Select)

Custom select/dropdown component with full keyboard support.

import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react' import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid' import { useState } from 'react'

const people = [ { id: 1, name: 'Wade Cooper' }, { id: 2, name: 'Arlene Mccoy' }, { id: 3, name: 'Devon Webb' }, ]

function SelectExample() { const [selected, setSelected] = useState(people[0])

return ( <Listbox value={selected} onChange={setSelected}> <ListboxButton className="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm"> <span className="block truncate">{selected.name}</span> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" /> </span> </ListboxButton>

  &#x3C;ListboxOptions className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
    {people.map((person) => (
      &#x3C;ListboxOption
        key={person.id}
        value={person}
        className="relative cursor-default select-none py-2 pl-10 pr-4 data-[focus]:bg-amber-100 data-[focus]:text-amber-900"
      >
        {({ selected }) => (
          &#x3C;>
            &#x3C;span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
              {person.name}
            &#x3C;/span>
            {selected &#x26;&#x26; (
              &#x3C;span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600">
                &#x3C;CheckIcon className="h-5 w-5" aria-hidden="true" />
              &#x3C;/span>
            )}
          &#x3C;/>
        )}
      &#x3C;/ListboxOption>
    ))}
  &#x3C;/ListboxOptions>
&#x3C;/Listbox>

) }

Listbox Features:

  • Single and multiple selection modes

  • Type-ahead search

  • Arrow key navigation

  • Controlled and uncontrolled modes

  • Disabled options support

  • Custom value comparison

Combobox (Autocomplete)

Searchable select component with filtering.

import { Combobox, ComboboxInput, ComboboxOptions, ComboboxOption } from '@headlessui/react' import { useState } from 'react'

const people = [ { id: 1, name: 'Wade Cooper' }, { id: 2, name: 'Arlene Mccoy' }, { id: 3, name: 'Devon Webb' }, { id: 4, name: 'Tom Cook' }, ]

function AutocompleteExample() { const [selected, setSelected] = useState(people[0]) const [query, setQuery] = useState('')

const filtered = query === '' ? people : people.filter((person) => person.name.toLowerCase().includes(query.toLowerCase()) )

return ( <Combobox value={selected} onChange={setSelected}> <ComboboxInput className="w-full rounded-lg border-none bg-white/5 py-1.5 pr-8 pl-3 text-sm/6 text-white focus:outline-none data-[focus]:outline-2 data-[focus]:-outline-offset-2 data-[focus]:outline-white/25" displayValue={(person) => person?.name} onChange={(event) => setQuery(event.target.value)} />

  &#x3C;ComboboxOptions className="w-[var(--input-width)] rounded-xl border border-white/5 bg-white/5 p-1 [--anchor-gap:var(--spacing-1)] empty:invisible">
    {filtered.map((person) => (
      &#x3C;ComboboxOption
        key={person.id}
        value={person}
        className="group flex cursor-default items-center gap-2 rounded-lg py-1.5 px-3 select-none data-[focus]:bg-white/10"
      >
        &#x3C;CheckIcon className="invisible size-4 fill-white group-data-[selected]:visible" />
        &#x3C;div className="text-sm/6 text-white">{person.name}&#x3C;/div>
      &#x3C;/ComboboxOption>
    ))}
  &#x3C;/ComboboxOptions>
&#x3C;/Combobox>

) }

Combobox Features:

  • Text input with filtering

  • Keyboard navigation

  • Nullable/optional selections

  • Custom display values

  • Async data loading support

  • Multiple selection mode

Dialog (Modal)

Accessible modal dialogs with focus trapping.

import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react' import { Fragment, useState } from 'react'

function ModalExample() { const [isOpen, setIsOpen] = useState(false)

return ( <> <button onClick={() => setIsOpen(true)}>Open dialog</button>

  &#x3C;Transition appear show={isOpen} as={Fragment}>
    &#x3C;Dialog as="div" className="relative z-10" onClose={() => setIsOpen(false)}>
      &#x3C;TransitionChild
        as={Fragment}
        enter="ease-out duration-300"
        enterFrom="opacity-0"
        enterTo="opacity-100"
        leave="ease-in duration-200"
        leaveFrom="opacity-100"
        leaveTo="opacity-0"
      >
        &#x3C;div className="fixed inset-0 bg-black/25" />
      &#x3C;/TransitionChild>

      &#x3C;div className="fixed inset-0 overflow-y-auto">
        &#x3C;div className="flex min-h-full items-center justify-center p-4 text-center">
          &#x3C;TransitionChild
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0 scale-95"
            enterTo="opacity-100 scale-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100 scale-100"
            leaveTo="opacity-0 scale-95"
          >
            &#x3C;DialogPanel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
              &#x3C;DialogTitle className="text-lg font-medium leading-6 text-gray-900">
                Payment successful
              &#x3C;/DialogTitle>
              &#x3C;div className="mt-2">
                &#x3C;p className="text-sm text-gray-500">
                  Your payment has been successfully submitted.
                &#x3C;/p>
              &#x3C;/div>

              &#x3C;div className="mt-4">
                &#x3C;button
                  type="button"
                  className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                  onClick={() => setIsOpen(false)}
                >
                  Got it, thanks!
                &#x3C;/button>
              &#x3C;/div>
            &#x3C;/DialogPanel>
          &#x3C;/TransitionChild>
        &#x3C;/div>
      &#x3C;/div>
    &#x3C;/Dialog>
  &#x3C;/Transition>
&#x3C;/>

) }

Dialog Features:

  • Focus trapping

  • Escape to close

  • Scroll locking

  • Return focus on close

  • Portal rendering

  • Nested dialogs support

  • Initial focus control

Popover

Floating panels for tooltips, dropdowns, and more.

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function PopoverExample() { return ( <Popover className="relative"> <PopoverButton className="inline-flex items-center gap-2 rounded-md bg-gray-800 py-1.5 px-3 text-sm/6 font-semibold text-white shadow-inner shadow-white/10 focus:outline-none data-[hover]:bg-gray-700 data-[focus]:outline-1 data-[focus]:outline-white"> Solutions </PopoverButton>

  &#x3C;PopoverPanel
    transition
    anchor="bottom"
    className="divide-y divide-white/5 rounded-xl bg-white/5 text-sm/6 transition duration-200 ease-in-out [--anchor-gap:var(--spacing-5)] data-[closed]:-translate-y-1 data-[closed]:opacity-0"
  >
    &#x3C;div className="p-3">
      &#x3C;a className="block rounded-lg py-2 px-3 transition hover:bg-white/5" href="#">
        &#x3C;p className="font-semibold text-white">Insights&#x3C;/p>
        &#x3C;p className="text-white/50">Measure actions your users take&#x3C;/p>
      &#x3C;/a>
      &#x3C;a className="block rounded-lg py-2 px-3 transition hover:bg-white/5" href="#">
        &#x3C;p className="font-semibold text-white">Automations&#x3C;/p>
        &#x3C;p className="text-white/50">Create your own targeted content&#x3C;/p>
      &#x3C;/a>
    &#x3C;/div>
  &#x3C;/PopoverPanel>
&#x3C;/Popover>

) }

Popover Features:

  • Anchor positioning

  • Click or hover triggers

  • Close on click outside

  • Nested popovers

  • Focus management

  • Portal rendering

RadioGroup

Accessible radio button groups.

import { RadioGroup, RadioGroupOption, RadioGroupLabel } from '@headlessui/react' import { useState } from 'react'

const plans = [ { name: 'Startup', ram: '12GB', cpus: '6 CPUs', disk: '160 GB SSD disk' }, { name: 'Business', ram: '16GB', cpus: '8 CPUs', disk: '512 GB SSD disk' }, { name: 'Enterprise', ram: '32GB', cpus: '12 CPUs', disk: '1024 GB SSD disk' }, ]

function RadioExample() { const [selected, setSelected] = useState(plans[0])

return ( <RadioGroup value={selected} onChange={setSelected}> <RadioGroupLabel className="sr-only">Server size</RadioGroupLabel> <div className="space-y-2"> {plans.map((plan) => ( <RadioGroupOption key={plan.name} value={plan} className="relative block cursor-pointer rounded-lg bg-white px-6 py-4 shadow-md focus:outline-none data-[focus]:outline-2 data-[focus]:outline-white/75 data-[checked]:bg-sky-900/75" > <div className="flex items-center justify-between"> <div className="flex items-center"> <div className="text-sm"> <RadioGroupLabel as="p" className="font-medium text-white"> {plan.name} </RadioGroupLabel> <div className="flex gap-2 text-white/50"> <div>{plan.ram}</div> <div aria-hidden="true">&middot;</div> <div>{plan.cpus}</div> <div aria-hidden="true">&middot;</div> <div>{plan.disk}</div> </div> </div> </div> </div> </RadioGroupOption> ))} </div> </RadioGroup> ) }

RadioGroup Features:

  • Arrow key navigation

  • Disabled options

  • Custom styling states

  • Controlled mode

  • Description support

Switch (Toggle)

Accessible toggle switches.

import { Switch } from '@headlessui/react' import { useState } from 'react'

function SwitchExample() { const [enabled, setEnabled] = useState(false)

return ( <Switch checked={enabled} onChange={setEnabled} className="group inline-flex h-6 w-11 items-center rounded-full bg-gray-200 transition data-[checked]:bg-blue-600" > <span className="size-4 translate-x-1 rounded-full bg-white transition group-data-[checked]:translate-x-6" /> </Switch> ) }

Switch Features:

  • Controlled and uncontrolled

  • Label support

  • Description support

  • Disabled state

  • Keyboard accessible (Space to toggle)

Tab (Tabs)

Accessible tab navigation.

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'

function TabExample() { const categories = [ { name: 'Recent', posts: [ { id: 1, title: 'Does drinking coffee make you smarter?' }, { id: 2, title: "So you've bought coffee... now what?" }, ], }, { name: 'Popular', posts: [ { id: 1, title: 'Is tech making coffee better or worse?' }, { id: 2, title: 'The most innovative things happening in coffee' }, ], }, ]

return ( <TabGroup> <TabList className="flex space-x-1 rounded-xl bg-blue-900/20 p-1"> {categories.map((category) => ( <Tab key={category.name} className="w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-blue-700 ring-white/60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 data-[selected]:bg-white data-[selected]:shadow data-[hover]:bg-white/[0.12] data-[focus]:outline-1" > {category.name} </Tab> ))} </TabList> <TabPanels className="mt-2"> {categories.map((category, idx) => ( <TabPanel key={idx} className="rounded-xl bg-white p-3 ring-white/60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2" > <ul> {category.posts.map((post) => ( <li key={post.id} className="relative rounded-md p-3 hover:bg-gray-100"> <h3 className="text-sm font-medium leading-5">{post.title}</h3> </li> ))} </ul> </TabPanel> ))} </TabPanels> </TabGroup> ) }

Tab Features:

  • Arrow key navigation

  • Default selected tab

  • Manual activation

  • Vertical/horizontal orientation

  • Controlled mode

Disclosure (Accordion)

Expandable content sections.

import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react' import { ChevronUpIcon } from '@heroicons/react/20/solid'

function DisclosureExample() { return ( <Disclosure> {({ open }) => ( <> <DisclosureButton className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500/75"> <span>What is your refund policy?</span> <ChevronUpIcon className={${open ? 'rotate-180 transform' : ''} h-5 w-5 text-purple-500} /> </DisclosureButton> <DisclosurePanel className="px-4 pb-2 pt-4 text-sm text-gray-500"> If you're unhappy with your purchase for any reason, email us within 90 days and we'll refund you in full, no questions asked. </DisclosurePanel> </> )} </Disclosure> ) }

Disclosure Features:

  • Controlled and uncontrolled

  • Default open state

  • Render props for state access

  • Multiple disclosures (accordion pattern)

  • Smooth animations with Transition

Transition

Animation component for enter/leave transitions.

import { Transition } from '@headlessui/react' import { useState } from 'react'

function TransitionExample() { const [isShowing, setIsShowing] = useState(false)

return ( <> <button onClick={() => setIsShowing(!isShowing)}>Toggle</button> <Transition show={isShowing} enter="transition-opacity duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="transition-opacity duration-200" leaveFrom="opacity-100" leaveTo="opacity-0" > <div className="rounded-md bg-blue-500 p-4 text-white"> I will fade in and out </div> </Transition> </> ) }

Transition Features:

  • CSS class-based transitions

  • Enter/leave lifecycle

  • Nested transitions (child coordination)

  • Appears support (initial mount animation)

  • Works with React 18 concurrent mode

Advanced Patterns

Render Props Pattern

Access component state for custom rendering.

import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react'

function RenderPropsExample() { return ( <Listbox value={selected} onChange={setSelected}> {({ open }) => ( <> <ListboxButton> Options {open ? '▲' : '▼'} </ListboxButton> <ListboxOptions> <ListboxOption value="a"> {({ selected, focus }) => ( <div className={focus ? 'bg-blue-500' : ''}> {selected && '✓'} Option A </div> )} </ListboxOption> </ListboxOptions> </> )} </Listbox> ) }

Controlled Components

Full control over component state.

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react' import { useState } from 'react'

function ControlledTabs() { const [selectedIndex, setSelectedIndex] = useState(0)

return ( <TabGroup selectedIndex={selectedIndex} onChange={setSelectedIndex}> <TabList> <Tab>Tab 1</Tab> <Tab>Tab 2</Tab> <Tab>Tab 3</Tab> </TabList> <TabPanels> <TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel>Content 3</TabPanel> </TabPanels> <button onClick={() => setSelectedIndex(0)}>Reset to first tab</button> </TabGroup> ) }

Portal Rendering

Render components outside DOM hierarchy.

import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react' import { createPortal } from 'react-dom'

function PortalMenu() { return ( <Menu> <MenuButton>Options</MenuButton> {createPortal( <MenuItems> <MenuItem> <button>Edit</button> </MenuItem> <MenuItem> <button>Delete</button> </MenuItem> </MenuItems>, document.body )} </Menu> ) }

Form Integration

Use with form libraries like React Hook Form.

import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react' import { useForm, Controller } from 'react-hook-form'

function FormExample() { const { control, handleSubmit } = useForm()

const onSubmit = (data) => { console.log(data) }

return ( <form onSubmit={handleSubmit(onSubmit)}> <Controller name="country" control={control} rules={{ required: true }} render={({ field }) => ( <Listbox {...field}> <ListboxButton>Select country</ListboxButton> <ListboxOptions> <ListboxOption value="us">United States</ListboxOption> <ListboxOption value="ca">Canada</ListboxOption> <ListboxOption value="mx">Mexico</ListboxOption> </ListboxOptions> </Listbox> )} /> <button type="submit">Submit</button> </form> ) }

Vue Support

Headless UI works identically in Vue 3.

<script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue'

const people = [ { id: 1, name: 'Wade Cooper' }, { id: 2, name: 'Arlene Mccoy' }, { id: 3, name: 'Devon Webb' }, ]

const selectedPerson = ref(people[0]) </script>

<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" v-slot="{ active, selected }" > <li :class="{ 'bg-blue-500': active }"> {{ selected ? '✓' : '' }} {{ person.name }} </li> </ListboxOption> </ListboxOptions> </Listbox> </template>

TypeScript Support

Full type safety with TypeScript.

import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'

interface User { id: number name: string role: 'admin' | 'user' }

interface UserMenuProps { user: User onEdit: (user: User) => void onDelete: (userId: number) => void }

function UserMenu({ user, onEdit, onDelete }: UserMenuProps) { return ( <Menu as="div" className="relative"> <MenuButton className="btn">{user.name}</MenuButton> <MenuItems className="menu"> <MenuItem> {({ focus }) => ( <button className={focus ? 'bg-blue-500' : ''} onClick={() => onEdit(user)} > Edit </button> )} </MenuItem> <MenuItem> {({ focus }) => ( <button className={focus ? 'bg-red-500' : ''} onClick={() => onDelete(user.id)} > Delete </button> )} </MenuItem> </MenuItems> </Menu> ) }

Tailwind CSS Integration

Headless UI is designed for Tailwind CSS.

Data Attributes for States

Headless UI v2 uses data attributes for state styling.

// Modern approach with data attributes <MenuButton className="data-[active]:bg-blue-500 data-[disabled]:opacity-50"> Options </MenuButton>

// Available states // data-[active] - Element is active/focused // data-[selected] - Element is selected // data-[disabled] - Element is disabled // data-[open] - Element/panel is open // data-[focus] - Element has focus // data-[checked] - Element is checked (Switch)

Tailwind Plugin

Configure Tailwind for Headless UI states.

// tailwind.config.js module.exports = { plugins: [ require('@headlessui/tailwindcss') ] }

Now use modifiers:

<MenuButton className="ui-active:bg-blue-500 ui-disabled:opacity-50"> Options </MenuButton>

Accessibility Features

ARIA Attributes

All ARIA attributes managed automatically:

  • aria-expanded on disclosure buttons

  • aria-selected on tab/option elements

  • aria-checked on switches

  • aria-labelledby for associations

  • aria-describedby for descriptions

  • role attributes (menu, listbox, dialog, etc.)

Keyboard Navigation

Full keyboard support built-in:

  • Arrow keys: Navigate options/tabs

  • Enter/Space: Select/activate

  • Escape: Close menus/dialogs

  • Tab: Focus management

  • Home/End: First/last item (where applicable)

  • Type-ahead: Search by typing

Focus Management

Automatic focus handling:

  • Return focus on close (Dialog, Menu, Popover)

  • Focus trap in modals

  • Initial focus control

  • Skip to focused element on open

Screen Reader Support

Tested with:

  • NVDA (Windows)

  • JAWS (Windows)

  • VoiceOver (macOS, iOS)

  • TalkBack (Android)

Server-Side Rendering

Fully compatible with Next.js, Remix, and other SSR frameworks.

// app/page.tsx (Next.js 13+) import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'

export default function Page() { return ( <Menu> <MenuButton>Options</MenuButton> <MenuItems> <MenuItem> <button>Edit</button> </MenuItem> </MenuItems> </Menu> ) }

No special configuration needed - components work identically on server and client.

Best Practices

  • Always provide labels - Use sr-only classes for hidden labels

  • Style all states - Use data attributes for active, selected, disabled states

  • Test keyboard navigation - Verify Tab, arrows, Enter, Escape work

  • Use semantic HTML - Let components render as appropriate elements

  • Provide focus indicators - Always show focus states for keyboard users

  • Test with screen readers - Verify announcements are correct

  • Handle loading states - Show appropriate UI during async operations

  • Use controlled mode when needed - For complex state management

  • Combine with Transition - Add smooth animations to open/close

  • Portal overlays - Use portals for menus/dialogs to avoid z-index issues

Common Pitfalls

❌ Missing Tailwind classes for states:

// WRONG - no visual feedback <MenuButton>Options</MenuButton>

// CORRECT <MenuButton className="data-[active]:bg-blue-500 data-[open]:bg-blue-600"> Options </MenuButton>

❌ Not using Fragment for render props:

// WRONG - adds extra div <Transition show={isOpen}> <div>Content</div> </Transition>

// CORRECT <Transition show={isOpen} as={Fragment}> <div>Content</div> </Transition>

❌ Forgetting to handle controlled state:

// WRONG - onChange does nothing <Listbox value={selected}> <ListboxOptions>...</ListboxOptions> </Listbox>

// CORRECT <Listbox value={selected} onChange={setSelected}> <ListboxOptions>...</ListboxOptions> </Listbox>

Resources

Summary

  • Headless UI provides unstyled, accessible component primitives

  • Zero runtime CSS - bring your own styles with Tailwind or custom CSS

  • Full accessibility - ARIA, keyboard navigation, screen reader support built-in

  • React and Vue - Identical APIs for both frameworks

  • TypeScript - Complete type definitions included

  • Render props - Access component state for custom rendering

  • SSR compatible - Works with Next.js, Remix, Nuxt

  • Perfect for - Custom design systems, Tailwind CSS integration, accessible components

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

drizzle-orm

No summary provided by upstream source.

Repository SourceNeeds Review
General

pydantic

No summary provided by upstream source.

Repository SourceNeeds Review
General

playwright-e2e-testing

No summary provided by upstream source.

Repository SourceNeeds Review
General

tailwind-css

No summary provided by upstream source.

Repository SourceNeeds Review