headlessui-vue

📁 peixotorms/odinlayer-skills 📅 4 days ago
1
总安装量
1
周安装量
#54298
全站排名
安装命令
npx skills add https://github.com/peixotorms/odinlayer-skills --skill headlessui-vue

Agent 安装分布

amp 1
opencode 1
kimi-cli 1
codex 1
github-copilot 1
claude-code 1

Skill 文档

HeadlessUI Vue

1. Overview

HeadlessUI provides completely unstyled, fully accessible UI components for Vue 3. Components handle all the complex accessibility and interaction logic — you provide all the styling. 30 playground examples are available via MCP.

All HeadlessUI Vue examples are available through the frontend-components MCP server under the headlessui-vue framework.

2. Installation

npm install @headlessui/vue

Optional (for icons):

npm install @heroicons/vue

3. MCP Workflow

3.1 Browse Available Examples

list_components(framework: "headlessui-vue")

Components: combobox, dialog, disclosure, focus-trap, listbox, menu, popover, radio-group, switch, tabs, combinations.

3.2 Get Example Code

get_component(framework: "headlessui-vue", category: "components", component_type: "dialog", variant: "Dialog")
get_component(framework: "headlessui-vue", category: "components", component_type: "menu", variant: "Menu")

3.3 Search

search_components(query: "listbox", framework: "headlessui-vue")
search_components(query: "disclosure", framework: "headlessui-vue")

4. Core API Patterns

4.1 Compound Components

HeadlessUI Vue uses compound component patterns with slots:

<template>
  <Menu>
    <MenuButton class="...">Options</MenuButton>
    <MenuItems class="...">
      <MenuItem v-slot="{ active }">
        <a :class="{ 'bg-gray-100': active }" href="/edit">Edit</a>
      </MenuItem>
      <MenuItem v-slot="{ active }">
        <a :class="{ 'bg-gray-100': active }" href="/delete">Delete</a>
      </MenuItem>
    </MenuItems>
  </Menu>
</template>

<script setup>
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/vue";
</script>

4.2 The as Prop

Change the rendered element:

<MenuButton as="div" class="...">Options</MenuButton>
<MenuItem as="button" class="...">Edit</MenuItem>
<DialogPanel as="form" class="..." @submit.prevent="handleSubmit">

4.3 Slot Props

HeadlessUI Vue exposes state through slot props for v-slot binding:

<MenuItem v-slot="{ active, disabled }">
  <a :class="{ 'bg-blue-100': active, 'opacity-50': disabled }">
    Edit
  </a>
</MenuItem>

<Switch
  v-model="enabled"
  :class="enabled ? 'bg-blue-600' : 'bg-gray-200'"
  class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full"
>
  <span :class="enabled ? 'translate-x-5' : 'translate-x-0.5'" class="..." />
</Switch>

4.4 Data Attributes

Like the React version, state is also exposed via data attributes:

<MenuItem>
  <a class="data-[focus]:bg-blue-100 data-[disabled]:opacity-50" href="/edit">
    Edit
  </a>
</MenuItem>

Key slot props and data attributes:

Slot Prop Data Attribute Components
active data-[focus] MenuItem, ListboxOption, ComboboxOption
selected data-[selected] ListboxOption, ComboboxOption, Tab
checked data-[checked] Switch, RadioGroupOption
open data-[open] Disclosure, Popover, Menu
disabled data-[disabled] Most components

5. Component Reference

5.1 Dialog (Modal)

<template>
  <Dialog :open="isOpen" @close="isOpen = false" class="relative z-50">
    <div class="fixed inset-0 bg-black/30" aria-hidden="true" />
    <div class="fixed inset-0 flex items-center justify-center p-4">
      <DialogPanel class="max-w-lg rounded bg-white p-6 shadow-xl">
        <DialogTitle class="text-lg font-bold">Title</DialogTitle>
        <p>Content here</p>
        <button @click="isOpen = false">Close</button>
      </DialogPanel>
    </div>
  </Dialog>
</template>

<script setup>
import { ref } from "vue";
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/vue";

const isOpen = ref(false);
</script>

5.2 Listbox (Select)

<template>
  <Listbox v-model="selected">
    <ListboxButton class="...">{{ selected.name }}</ListboxButton>
    <ListboxOptions anchor="bottom" class="...">
      <ListboxOption
        v-for="option in options"
        :key="option.id"
        :value="option"
        v-slot="{ active, selected }"
        class="cursor-pointer px-4 py-2"
        :class="{ 'bg-blue-100': active }"
      >
        <span :class="{ 'font-bold': selected }">{{ option.name }}</span>
      </ListboxOption>
    </ListboxOptions>
  </Listbox>
</template>

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

const options = [
  { id: 1, name: "Option A" },
  { id: 2, name: "Option B" },
];
const selected = ref(options[0]);
</script>

5.3 Combobox (Autocomplete)

<template>
  <Combobox v-model="selected" @update:modelValue="query = ''">
    <ComboboxInput
      class="..."
      :displayValue="(o) => o?.name"
      @change="query = $event.target.value"
    />
    <ComboboxButton class="...">▼</ComboboxButton>
    <ComboboxOptions anchor="bottom" class="...">
      <ComboboxOption
        v-for="option in filtered"
        :key="option.id"
        :value="option"
        v-slot="{ active }"
        :class="{ 'bg-blue-100': active }"
        class="px-4 py-2"
      >
        {{ option.name }}
      </ComboboxOption>
    </ComboboxOptions>
  </Combobox>
</template>

<script setup>
import { ref, computed } from "vue";
import { Combobox, ComboboxInput, ComboboxButton, ComboboxOptions, ComboboxOption } from "@headlessui/vue";

const query = ref("");
const selected = ref(null);
const options = [/* ... */];
const filtered = computed(() =>
  query.value === ""
    ? options
    : options.filter((o) => o.name.toLowerCase().includes(query.value.toLowerCase()))
);
</script>

5.4 Switch (Toggle)

<template>
  <Switch
    v-model="enabled"
    :class="enabled ? 'bg-blue-600' : 'bg-gray-200'"
    class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition-colors"
  >
    <span
      :class="enabled ? 'translate-x-5' : 'translate-x-0.5'"
      class="pointer-events-none inline-block size-5 rounded-full bg-white shadow transition-transform"
    />
  </Switch>
</template>

<script setup>
import { ref } from "vue";
import { Switch } from "@headlessui/vue";

const enabled = ref(false);
</script>

5.5 Disclosure (Accordion)

<template>
  <Disclosure v-slot="{ open }">
    <DisclosureButton class="flex w-full justify-between rounded-lg bg-gray-100 px-4 py-2">
      Section Title
      <ChevronUpIcon :class="{ 'rotate-180': !open }" class="size-5 transition-transform" />
    </DisclosureButton>
    <DisclosurePanel class="px-4 py-2 text-gray-500">
      Content here
    </DisclosurePanel>
  </Disclosure>
</template>

<script setup>
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue";
</script>

5.6 Popover

<template>
  <Popover class="relative">
    <PopoverButton class="...">Info</PopoverButton>
    <PopoverPanel anchor="bottom" class="...">
      Popover content
    </PopoverPanel>
  </Popover>
</template>

<script setup>
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
</script>

5.7 RadioGroup

<template>
  <RadioGroup v-model="selected">
    <RadioGroupOption
      v-for="option in options"
      :key="option.id"
      :value="option"
      v-slot="{ checked }"
      :class="{ 'bg-blue-50 border-blue-500': checked }"
      class="cursor-pointer rounded-lg border px-5 py-4"
    >
      <RadioGroupLabel class="font-medium">{{ option.name }}</RadioGroupLabel>
      <RadioGroupDescription class="text-gray-500">{{ option.desc }}</RadioGroupDescription>
    </RadioGroupOption>
  </RadioGroup>
</template>

<script setup>
import { ref } from "vue";
import { RadioGroup, RadioGroupOption, RadioGroupLabel, RadioGroupDescription } from "@headlessui/vue";

const selected = ref(null);
const options = [/* ... */];
</script>

5.8 Tabs

<template>
  <TabGroup>
    <TabList class="flex gap-4">
      <Tab
        v-for="tab in tabs"
        :key="tab"
        v-slot="{ selected }"
        :class="{ 'bg-blue-100 text-blue-700': selected }"
        class="rounded-full px-3 py-1"
      >
        {{ tab }}
      </Tab>
    </TabList>
    <TabPanels class="mt-3">
      <TabPanel v-for="tab in tabs" :key="tab">
        {{ tab }} content
      </TabPanel>
    </TabPanels>
  </TabGroup>
</template>

<script setup>
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@headlessui/vue";

const tabs = ["Tab 1", "Tab 2", "Tab 3"];
</script>

6. Transitions

6.1 TransitionRoot and TransitionChild

<template>
  <TransitionRoot :show="isOpen" as="template">
    <Dialog @close="isOpen = false">
      <TransitionChild
        enter="ease-out duration-300"
        enter-from="opacity-0"
        enter-to="opacity-100"
        leave="ease-in duration-200"
        leave-from="opacity-100"
        leave-to="opacity-0"
      >
        <div class="fixed inset-0 bg-black/25" />
      </TransitionChild>

      <TransitionChild
        enter="ease-out duration-300"
        enter-from="opacity-0 scale-95"
        enter-to="opacity-100 scale-100"
        leave="ease-in duration-200"
        leave-from="opacity-100 scale-100"
        leave-to="opacity-0 scale-95"
      >
        <DialogPanel class="...">Content</DialogPanel>
      </TransitionChild>
    </Dialog>
  </TransitionRoot>
</template>

6.2 Built-in Transition

Components that support transitions can use the transition prop with data attributes:

<MenuItems
  transition
  class="transition duration-100 ease-in data-[closed]:scale-95 data-[closed]:opacity-0"
>

7. Floating UI (Positioning)

Control dropdown positioning with the anchor prop:

<MenuItems anchor="bottom start">    <!-- Below, left-aligned -->
<MenuItems anchor="bottom end">      <!-- Below, right-aligned -->
<ListboxOptions anchor="top start">  <!-- Above, left-aligned -->

Add offset: :anchor="{ to: 'bottom', gap: 4 }".

8. Vue-Specific Patterns

8.1 v-model Support

Components with state support v-model:

<Switch v-model="enabled" />
<Listbox v-model="selected" />
<Combobox v-model="selected" />
<RadioGroup v-model="plan" />
<TabGroup :selectedIndex="index" @change="index = $event" />

8.2 Form Integration

<Listbox v-model="selected" name="country">
  <!-- renders hidden input with selected value -->
</Listbox>

8.3 Multiple Selection

<Listbox v-model="selected" multiple>
  <!-- selected is an array -->
</Listbox>

9. Accessibility Features

HeadlessUI handles automatically:

  • ARIA attributes (role, aria-expanded, aria-haspopup, aria-labelledby)
  • Keyboard navigation (arrow keys, Enter, Space, Escape, Home, End)
  • Focus management (trapping in dialogs, return focus on close)
  • Screen reader announcements
  • Click-outside-to-close

10. Workflow Summary

Step Action
1. Pick component Dialog, Menu, Listbox, Combobox, Switch, etc.
2. Fetch example get_component(framework: "headlessui-vue", ...)
3. Import import { Component } from "@headlessui/vue"
4. Style with Tailwind Use class binding + slot props or data attributes
5. Add transitions Use TransitionRoot/TransitionChild or transition prop
6. Position Use anchor prop for dropdowns/popovers
7. v-model Bind state with v-model for controlled components