🔥 Oxide UI v0.1.0 — Dark mode only, copy-paste ready. Get started →
Skip to content

Accordion

<div> + Headless UI Disclosure

Expand and collapse content panels. Each item is independently toggled. Built on Headless UI Disclosure.

Overview

Preview

A dark-mode-only Tailwind CSS component library for Nuxt. Copy-paste components with zero runtime dependencies.

Single item (Disclosure)

Preview
A dark-mode-only Tailwind CSS component library for Nuxt. Copy-paste components with zero runtime dependencies.
html
<!-- Single item, open by default -->
<div class="overflow-hidden rounded-lg border border-zinc-800">
  <button class="flex w-full items-center justify-between px-5 py-4 text-left hover:bg-zinc-900/40 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#D40C37]/40">
    <span class="text-sm font-medium text-zinc-100">Question title</span>
    <svg class="h-4 w-4 text-zinc-500 transition-transform" :class="{ 'rotate-180': open }" ...>▾</svg>
  </button>
  <div v-if="open" class="border-t border-zinc-800 px-5 py-4 text-sm leading-relaxed text-zinc-400">
    Answer content goes here.
  </div>
</div>

Full accordion (Vue)

vue
<script setup>
import { ref } from 'vue'

const items = [
  { q: 'Is it free?', a: 'Yes, all components are free forever.' },
  { q: 'Does it work with Nuxt?', a: 'Yes — built specifically for Nuxt 3 + Vue 3.' },
  { q: 'Can I use it without Headless UI?', a: 'For static components yes. Interactive ones (Dropdown, Modal etc.) use Headless UI.' },
]
const open = ref(0)
</script>

<template>
  <div class="space-y-1.5">
    <div
      v-for="(item, i) in items"
      :key="i"
      class="overflow-hidden rounded-lg border transition-colors"
      :class="open === i ? 'border-zinc-700' : 'border-zinc-800'"
    >
      <button
        @click="open = open === i ? null : i"
        class="flex w-full items-center justify-between px-5 py-4 text-left hover:bg-zinc-900/40 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#D40C37]/40"
      >
        <span class="text-sm font-medium text-zinc-100">{{ item.q }}</span>
        <svg
          class="h-4 w-4 shrink-0 text-zinc-500 transition-transform duration-200"
          :class="{ 'rotate-180': open === i }"
          ...
        >▾</svg>
      </button>
      <Transition
        enter-active-class="transition ease-out duration-150"
        enter-from-class="opacity-0 -translate-y-1"
        enter-to-class="opacity-100 translate-y-0"
      >
        <div v-if="open === i" class="border-t border-zinc-800 px-5 py-4">
          <p class="text-sm leading-relaxed text-zinc-400">{{ item.a }}</p>
        </div>
      </Transition>
    </div>
  </div>
</template>
vue
<script setup>
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
</script>

<template>
  <div class="space-y-1.5">
    <Disclosure v-for="item in items" :key="item.q" v-slot="{ open }" as="div"
      class="overflow-hidden rounded-lg border border-zinc-800">
      <DisclosureButton class="flex w-full items-center justify-between px-5 py-4 text-left hover:bg-zinc-900/40 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#D40C37]/40">
        <span class="text-sm font-medium text-zinc-100">{{ item.q }}</span>
        <svg class="h-4 w-4 shrink-0 text-zinc-500 transition-transform" :class="{ 'rotate-180': open }" ...>▾</svg>
      </DisclosureButton>
      <DisclosurePanel class="border-t border-zinc-800 px-5 py-4">
        <p class="text-sm leading-relaxed text-zinc-400">{{ item.a }}</p>
      </DisclosurePanel>
    </Disclosure>
  </div>
</template>

Accessibility notes

  • DisclosureButton renders a <button> with aria-expanded automatically managed
  • DisclosurePanel has id linked to the button's aria-controls automatically
  • Each item is independently expandable — no coordination between panels unless you implement it explicitly

Released under the MIT License.