Accordion

A collapsible content panel for organizing information in a compact space

Usage

<script setup lang="ts">
import { Accordion } from '@hareui/vue'
</script>

<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'interface FaqItem extends AccordionItem {  icon: string}const items: FaqItem[] = [  {    content: 'Browse our products, add items to your cart, and proceed to checkout. You\'ll need to provide shipping and payment information to complete your purchase.',    icon: 'gravity-ui:shopping-bag',    label: 'How do I place an order?',  },  {    content: 'Yes, you can modify or cancel your order before it\'s shipped. Once your order is processed, you can\'t make changes.',    icon: 'gravity-ui:receipt',    label: 'Can I modify or cancel my order?',  },  {    content: 'We accept all major credit cards, including Visa, Mastercard, and American Express.',    icon: 'gravity-ui:credit-card',    label: 'What payment methods do you accept?',  },  {    content: 'Shipping costs vary based on your location and the size of your order. We offer free shipping for orders over $50.',    icon: 'gravity-ui:box',    label: 'How much does shipping cost?',  },  {    content: 'Yes, we ship to most countries. Please check our shipping rates and policies for more information.',    icon: 'gravity-ui:planet-earth',    label: 'Do you ship internationally?',  },  {    content: 'If you\'re not satisfied with your purchase, you can request a refund within 30 days of purchase. Please contact our customer support team for assistance.',    icon: 'gravity-ui:arrows-rotate-left',    label: 'How do I request a refund?',  },]</script><template>  <Accordion class="w-full max-w-md" :items="items">    <template #leading="{ item }">      <span class="me-3 size-4 shrink-0 text-muted"><Icon :icon="item.icon" height="16" width="16" /></span>    </template>    <template #indicator>      <Icon height="16" icon="gravity-ui:chevron-down" width="16" />    </template>  </Accordion></template>

Anatomy

Accordion renders one item per items entry: a heading with a trigger button (leading content, label, indicator) and a collapsible panel holding the body. HeroUI's compound parts (Accordion.Item, Accordion.Heading, Accordion.Trigger, Accordion.Indicator, Accordion.Panel, Accordion.Body) become items, slots and ui keys:

<template>
  <Accordion :items="items" v-model="expanded">
    <template #leading="{ item, index, open }" />   <!-- before the label (e.g. an icon) -->
    <template #label="{ item, index, open }" />     <!-- replaces item.label -->
    <template #indicator="{ item, index, open }" /> <!-- Accordion.Indicator's children -->
    <template #content="{ item, index, open }" />   <!-- fallback body content -->
    <template #[item.slot]="{ item, index, open }" /> <!-- per-item body content -->
  </Accordion>
</template>
  • A single root element in #indicator receives the indicator classes and data-expanded, like HeroUI's Accordion.Indicator children, so any icon rotates when its item expands.
  • Extra fields on an item (icons, subtitles, …) are passed through to the slots as item.
  • Accordion.Body's className maps to the bodyInner ui key.
ui keydata-slotElement
baseaccordionroot element
itemaccordion-itemone per item
headingaccordion-heading<h3> around the trigger
triggeraccordion-triggerthe expand/collapse button
indicatoraccordion-indicatorchevron (or #indicator content)
panelaccordion-panelcollapsible panel
bodyaccordion-bodycontent body container
bodyInner–padded wrapper inside the body (HeroUI's Accordion.Body className target)

Typing items

items takes the exported AccordionItem type. Extra fields are kept: extend the type, and the slots receive your type, so no casts are needed.

import type { AccordionItem } from '@hareui/vue'

interface MyItem extends AccordionItem {
  icon: string
}

const items: MyItem[] = [/* ... */]
// <template #leading="{ item }"> — item.icon is a string

Examples

Surface

<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'interface FaqItem extends AccordionItem {  icon: string}const items: FaqItem[] = [  {    content: 'Browse our products, add items to your cart, and proceed to checkout. You\'ll need to provide shipping and payment information to complete your purchase.',    icon: 'gravity-ui:shopping-bag',    label: 'How do I place an order?',  },  {    content: 'Yes, you can modify or cancel your order before it\'s shipped. Once your order is processed, you can\'t make changes.',    icon: 'gravity-ui:receipt',    label: 'Can I modify or cancel my order?',  },  {    content: 'We accept all major credit cards, including Visa, Mastercard, and American Express.',    icon: 'gravity-ui:credit-card',    label: 'What payment methods do you accept?',  },  {    content: 'Shipping costs vary based on your location and the size of your order. We offer free shipping for orders over $50.',    icon: 'gravity-ui:box',    label: 'How much does shipping cost?',  },  {    content: 'Yes, we ship to most countries. Please check our shipping rates and policies for more information.',    icon: 'gravity-ui:planet-earth',    label: 'Do you ship internationally?',  },  {    content: 'If you\'re not satisfied with your purchase, you can request a refund within 30 days of purchase. Please contact our customer support team for assistance.',    icon: 'gravity-ui:arrows-rotate-left',    label: 'How do I request a refund?',  },]</script><template>  <Accordion class="w-full max-w-md" :items="items" variant="surface">    <template #leading="{ item }">      <span class="me-3 size-4 shrink-0 text-muted"><Icon :icon="item.icon" height="16" width="16" /></span>    </template>    <template #indicator>      <Icon height="16" icon="gravity-ui:chevron-down" width="16" />    </template>  </Accordion></template>

Without Separator

<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'interface FaqItem extends AccordionItem {  icon: string}const items: FaqItem[] = [  {    content: 'Browse our products, add items to your cart, and proceed to checkout. You\'ll need to provide shipping and payment information to complete your purchase.',    icon: 'gravity-ui:shopping-bag',    label: 'How do I place an order?',  },  {    content: 'Yes, you can modify or cancel your order before it\'s shipped. Once your order is processed, you can\'t make changes.',    icon: 'gravity-ui:receipt',    label: 'Can I modify or cancel my order?',  },  {    content: 'We accept all major credit cards, including Visa, Mastercard, and American Express.',    icon: 'gravity-ui:credit-card',    label: 'What payment methods do you accept?',  },]</script><template>  <Accordion class="w-full max-w-md" :items="items" hide-separator>    <template #leading="{ item }">      <span class="me-3 size-4 shrink-0 text-muted"><Icon :icon="item.icon" height="16" width="16" /></span>    </template>    <template #indicator>      <Icon height="16" icon="gravity-ui:chevron-down" width="16" />    </template>  </Accordion></template>

Multiple Expanded

<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Accordion } from '@hareui/vue'const items: AccordionItem[] = [  {    label: 'Getting Started',    content: 'Learn the basics of HeroUI and how to integrate it into your React project. This section covers installation, setup, and your first component.',  },  {    label: 'Core Concepts',    content: 'Understand the fundamental concepts behind HeroUI, including the compound component pattern, styling with Tailwind CSS, and accessibility features.',  },  {    label: 'Advanced Usage',    content: 'Explore advanced features like custom variants, theme customization, and integration with other libraries in your React ecosystem.',  },  {    label: 'Best Practices',    content: 'Follow our recommended best practices for building performant, accessible, and maintainable applications with HeroUI components.',  },]</script><template>  <Accordion class="w-full max-w-md" :items="items" multiple /></template>

Disabled State

Entire accordion disabled

Individual items disabled

<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Accordion } from '@hareui/vue'const disabledItems: AccordionItem[] = [  { label: 'Disabled Item 1', content: 'This content cannot be accessed when the accordion is disabled.' },  { label: 'Disabled Item 2', content: 'This content cannot be accessed when the accordion is disabled.' },]const items: AccordionItem[] = [  { label: 'Active Item', content: 'This item is active and can be toggled normally.' },  { label: 'Disabled Item', content: 'This content cannot be accessed when the item is disabled.', disabled: true },  { label: 'Another Active Item', content: 'This item is also active and can be toggled.' },]</script><template>  <div class="flex w-full flex-col items-center gap-8">    <div class="w-full max-w-md space-y-2">      <h3 class="text-sm font-medium text-muted">        Entire accordion disabled      </h3>      <Accordion class="w-full max-w-md" disabled :items="disabledItems" />    </div>    <div class="w-full max-w-md space-y-2">      <h3 class="text-sm font-medium text-muted">        Individual items disabled      </h3>      <Accordion class="w-full max-w-md" :items="items" />    </div>  </div></template>

Controlled

Expanded: getting-started

Learn the basics of HeroUI and how to integrate it into your React project. This section covers installation, setup, and your first component.

<script setup lang="ts">import type { AccordionItem, ExpandedValue } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion, Button, useDisclosureGroupNavigation } from '@hareui/vue'import { ref } from 'vue'interface Section extends AccordionItem {  value: string}const items: Section[] = [  {    content: 'Learn the basics of HeroUI and how to integrate it into your React project. This section covers installation, setup, and your first component.',    value: 'getting-started',    label: 'Getting Started',  },  {    content: 'Understand the fundamental concepts behind HeroUI, including the compound component pattern, styling with Tailwind CSS, and accessibility features.',    value: 'core-concepts',    label: 'Core Concepts',  },  {    content: 'Explore advanced features like custom variants, theme customization, and integration with other libraries in your React ecosystem.',    value: 'advanced-usage',    label: 'Advanced Usage',  },]const expandedKeys = ref<ExpandedValue[]>(['getting-started'])const { isNextDisabled, isPrevDisabled, onNext, onPrevious } = useDisclosureGroupNavigation({  expandedKeys,  itemIds: items.map(item => item.value),  onExpandedChange: (keys) => { expandedKeys.value = keys },})</script><template>  <div class="w-full max-w-md">    <div class="mb-4 flex items-center justify-between">      <p class="text-sm text-muted">        Expanded: <strong>{{ expandedKeys.join(', ') || 'none' }}</strong>      </p>      <div class="flex gap-2">        <Button aria-label="Previous item" :disabled="isPrevDisabled" size="sm" variant="secondary" @click="onPrevious">          <Icon class="size-4" icon="gravity-ui:chevron-up" />        </Button>        <Button aria-label="Next item" :disabled="isNextDisabled" size="sm" variant="secondary" @click="onNext">          <Icon class="size-4" icon="gravity-ui:chevron-down" />        </Button>      </div>    </div>    <Accordion v-model="expandedKeys" :items="items" />  </div></template>

Custom Indicator

<script setup lang="ts">import type { AccordionItem, ExpandedValue } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'import { ref } from 'vue'interface IndicatorItem extends AccordionItem {  icon?: string}const items: IndicatorItem[] = [  {    value: '1',    label: 'Using Plus/Minus Icon',    content: 'This accordion uses a plus icon that transforms when expanded. The icon automatically rotates 45 degrees to form an X.',  },  {    value: '2',    label: 'Using Caret Icon',    icon: 'gravity-ui:circle-chevron-down',    content: 'This item uses a caret icon for the indicator. The rotation animation is applied automatically.',  },  {    value: '3',    label: 'Using Arrow Icon',    icon: 'gravity-ui:chevrons-down',    content: 'This item uses an arrow icon. Any icon you pass will receive the rotation animation when the item expands.',  },]const expandedKeys = ref<ExpandedValue[]>([''])</script><template>  <Accordion v-model="expandedKeys" class="w-full max-w-md" :items="items" variant="surface">    <template #indicator="{ item, open }">      <Icon v-if="item.icon" height="16" :icon="item.icon" width="16" />      <Icon v-else height="16" :icon="open ? 'gravity-ui:minus' : 'gravity-ui:plus'" width="16" />    </template>  </Accordion></template>

FAQ Layout

Frequently Asked Questions

Everything you need to know about licensing and usage.

General

Licensing

Support

<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'interface FaqCategory {  title: string  items: AccordionItem[]}const categories: FaqCategory[] = [  {    items: [      {        content: 'Browse our products, add items to your cart, and proceed to checkout. You\'ll need to provide shipping and payment information to complete your purchase.',        label: 'How do I place an order?',      },      {        content: 'Yes, you can modify or cancel your order before it\'s shipped. Once your order is processed, you can\'t make changes.',        label: 'Can I modify or cancel my order?',      },    ],    title: 'General',  },  {    items: [      {        content: 'You can purchase a license directly from our website. Select the license type that fits your needs and proceed to checkout.',        label: 'How do I purchase a license?',      },      {        content: 'A standard license is for personal use or small projects, while a pro license includes commercial use rights and priority support.',        label: 'What is the difference between a standard and a pro license?',      },    ],    title: 'Licensing',  },  {    items: [      {        content: 'You can reach our support team through the contact form on our website, or email us directly at [email protected].',        label: 'How do I get support?',      },    ],    title: 'Support',  },]</script><template>  <div class="flex w-full flex-col gap-6">    <div class="flex flex-col gap-1">      <h2 class="text-2xl font-bold">        Frequently Asked Questions      </h2>      <p class="mb-4 text-lg font-medium text-muted">        Everything you need to know about licensing and usage.      </p>    </div>    <div v-for="category in categories" :key="category.title">      <p class="text-md mb-2 font-medium text-muted">        {{ category.title }}      </p>      <Accordion class="w-full" :items="category.items" variant="surface">        <template #indicator>          <Icon height="16" icon="gravity-ui:chevron-down" width="16" />        </template>      </Accordion>    </div>  </div></template>

Customization

Tailwind CSS

<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'interface SetupItem extends AccordionItem {  iconUrl: string  subtitle: string}const items: SetupItem[] = [  {    content: 'Stay informed about your account activity with real-time notifications.',    iconUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/3dicons/bell-small.png',    subtitle: 'Receive account activity updates',    label: 'Set Up Notifications',  },  {    content: 'Enhance your browsing experience by installing our official browser extension',    iconUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/3dicons/compass-small.png',    subtitle: 'Connect your browser to your account',    label: 'Set up Browser Extension',  },  {    content: 'Begin your journey into the world of digital collectibles by creating your first NFT. ',    iconUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/3dicons/mint-collective-small.png',    subtitle: 'Create your first collectible',    label: 'Mint Collectible',  },]const ui = {  item: "group/item first:**:data-[slot=accordion-trigger]:rounded-t-2xl last:[&:not(:has([data-slot=accordion-trigger][aria-expanded='true']))_[data-slot=accordion-trigger]]:rounded-b-2xl",  trigger: 'group flex items-center gap-2 transition-none hover:bg-surface',  indicator: 'text-muted/50 [&>svg]:size-4',  bodyInner: 'text-muted/80',}</script><template>  <Accordion class="bg-surface-1/10 w-full max-w-md rounded-2xl" :items="items" :ui="ui" variant="surface">    <template #leading="{ item }">      <img        :alt="item.label"        class="h-11 w-11 transition-[scale,rotate] duration-300 ease-out group-hover/item:scale-120 group-hover/item:-rotate-10 group-hover/item:drop-shadow-lg"        :src="item.iconUrl"      >    </template>    <template #label="{ item }">      <div class="flex flex-col gap-0">        <span class="leading-5 font-medium">{{ item.label }}</span>        <span class="leading-6 font-normal text-muted/80">{{ item.subtitle }}</span>      </div>    </template>    <template #indicator>      <Icon height="16" icon="gravity-ui:chevron-down" width="16" />    </template>  </Accordion></template>

Global Configuration

To customize the Accordion classes for the whole app, extend the accordion theme in createHareUI:

app.use(createHareUI({
  ui: {
    accordion: {
      slots: {
        base: 'rounded-xl bg-gray-50',
        trigger: 'font-semibold text-lg',
      },
      variants: {
        variant: {
          surface: { base: 'shadow-lg border-2' },
        },
      },
    },
  },
}))

Styling Reference

Slots

  • base → [data-slot="accordion"] – base accordion container
  • body → [data-slot="accordion-body"] – content body container
  • bodyInner – padded wrapper inside the body
  • heading → [data-slot="accordion-heading"] – heading wrapper
  • indicator → [data-slot="accordion-indicator"] – expand/collapse indicator icon
  • item → [data-slot="accordion-item"] – individual accordion item
  • panel → [data-slot="accordion-panel"] – collapsible panel container
  • trigger → [data-slot="accordion-trigger"] – clickable trigger button

Variants

  • variant="surface" – surface background with rounded corners

State Attributes

  • [data-slot="accordion-trigger"][aria-expanded="true"] – expanded state
  • [data-slot="accordion-panel"][aria-hidden="false"] – panel visible state

Interactive States

The component supports both CSS pseudo-classes and data attributes for flexibility:

  • Hover: :hover on trigger
  • Focus: :focus-visible on trigger
  • Disabled: :disabled on trigger, [data-disabled="true"] on item
  • Expanded: [aria-expanded="true"] on trigger, [data-expanded="true"] on item, panel and indicator

API Reference

Props

PropTypeDefaultDescription
itemsAccordionItem[]-The items to render.
modelValueExpandedValue[]undefinedThe expanded item values. Use v-model to control them.
defaultValueExpandedValue[]-The values expanded on first render (uncontrolled).
multiplebooleanfalseWhether multiple items can be expanded at once (HeroUI's allowsMultipleExpanded).
disabledbooleanfalseWhether the entire accordion is disabled.
variant"surface" | "default"'default'The visual variant of the accordion.
hideSeparatorbooleanfalseHide separator lines between accordion items.
uiComponentSlots<{ slots: { base: string; body: string; bodyInner: string; heading: string; indicator: string[]; item: string[]; panel: string[]; trigger: string[]; }; variants: { variant: { default: {}; surface: { base: string; trigger: string; item: string[]; }; }; }; defaultVariants: { variant: string; }; }>-Per-slot class overrides.

Slots

SlotPropsDescription
leadingItemSlotPropsRendered in the trigger before the label (e.g. an icon).
labelItemSlotPropsReplaces the trigger's label text.
indicatorItemSlotPropsReplaces the chevron (HeroUI's Accordion.Indicator children). A single root element receives the themed indicator classes and data-expanded, so it rotates like the default.
contentItemSlotPropsBody content for every item, overridden by [item.slot].

Emits

EventPayloadDescription
update:modelValue[value: ExpandedValue[]]-

useDisclosureGroupNavigation

Previous/next navigation for a controlled Accordion or DisclosureGroup (used by the Controlled example).

OptionTypeDefaultDescription
expandedKeysMaybeRefOrGetter<ExpandedValue[]>-The current v-model value
itemIdsMaybeRefOrGetter<ExpandedValue[]>-Every item's value, in display order
onExpandedChange(keys: ExpandedValue[]) => void-Called with the new expanded values
multipleMaybeRefOrGetter<boolean>falseKeep previously expanded items open while navigating

Returns currentIndex, isPrevDisabled, isNextDisabled (computed refs) and onPrevious, onNext.

On this page

No Headings